es-ude / es-ude/OnDeviceTraining
har_classifier: map the quantization design space (epic)
- Dominant language
- C
- Stars
- 1
- Forks
- 3
- Avg merge
- 1d 1h
- Merged PRs (30d)
- 8
Description
## Goal
Use `examples/har_classifier` as the reference vehicle to **map the full quantization design space of the framework, axis by axis** — so we can state with evidence what works, what works under constraints (LR windows, width caps), and what is currently blocked (and by which issue). Output = a decision map for future MCU-port and API work, not a pile of one-off demos.
Why HAR: robust classifier (absorbs float noise, so quantization effects are attributable), fast enough for offline sweeps, and already fully instrumented — memory profiling (`ODT_MEM_PROFILE`, PR #281), extended RunLog schema, `run_matrix.py` sweep driver, `compare_memory.py`, plotting.
## Baseline in flight (axis 1 — do not disturb)
A 60-run sweep is currently running under `examples/har_classifier/logs/`: configs {float, sym@12, sym@10, sym@8, sym@6, sym@4} × seeds 1..10, 50 epochs, momentum 0.9, **shared LR 0.01 for all configs** (the `train_c_sym.c` default was moved from 0.03 to 0.01; the `run_matrix.py` docstring still saying "FLOAT32 0.01, SYM 0.03" is stale). It quantizes **parameters only**: packed SYM@x weights+bias storage (post-build `requantizeTensorInPlace`; #270 blocks native SYM factory init), forward `ARITH_SYM_INT32`, all backward math FLOAT32, FLOAT32 grads, FLOAT32 wires, FLOAT32 momentum, param write-back rounding SR_HALF_AWAY (env `SYM_ROUNDING=det` for the deterministic A/B).
⚠️ **Flag for ratification:** the 2026-07-04 spike (5–10 epochs) put SYM's usable LR window at ~0.02–0.05 @ mom 0.9 and found 0.01 slow to start — "a shared LR would unfairly break SYM". The running sweep intentionally(?) shares 0.01 across all configs; whether 50 epochs neutralizes the slow start is exactly what the sweep data will show. Evaluate this when ratifying the baseline aggregate — if SYM is systematically handicapped, axis 1 needs an LR-tuned re-run before it can serve as the epic's baseline.
Per-axis studies below hold the other axes at the ratified baseline's best (expected: one fixed sym width, e.g. sym8) and vary one axis at a time.
## Axes
Each axis gets its own sub-issue when picked up; the epic tracks them. Per axis the deliverable is: ≥10-seed sweep, aggregate table (accuracy mean±std, memory breakdown per category), and a verdict — *works / works with constraints (documented) / blocked by #NN*.
1. **Parameter storage width** — packed SYM@x weights+bias. **IN FLIGHT** (sweep above). Known boundaries: #189 int32-overflow LR ceiling; sub-grid update stall sets an LR floor (by design — no float master copy, memory-over-accuracy); #270 `requireFloat32` factory gate.
2. **Update-path rounding** — HALF_AWAY vs SR_HALF_AWAY on param (and later momentum) write-back. The knob exists (`SYM_ROUNDING`); a dedicated A/B at fixed width is the per-axis follow-through of the #279 dead-zone acceptance (mechanism shipped in #277/PR #284; ≥10-seed convergence acceptance still open). **Attach a small weight-decay arm** (both HAR trainers pin wd=0.0): per-step decay shrink lr·wd·param is typically below one SYM level, so whether L2 works at all on packed params depends on this rounding choice — invisible at wd=0.
3. **Gradient storage** — `weightGradStorage`/`biasGradStorage` = packed SYM@x (and ASYM) instead of the FLOAT32 default (PR #276 primitives; SYM_INT32 is banned as grad storage, #261). Factors: accumulate mode (`OUT_ACC_DYNAMIC_RESCALE` vs `OUT_ACC_FIXED_SCALE`), the fit-preserving pack overflow abort as a measured boundary, and **the grad-storage quantization's own roundingMode** (HALF_AWAY vs SR_HALF_AWAY inside `quantizationInitSym(bits, rounding)` for the grad configs — independent of axis 2's param write-back knob; deterministic rounding biases every per-macro-batch accumulate, the canonical FQT concern per Deutel arXiv:2407.10734).
4. **Optimizer-state (momentum) storage** — momentum has its own quantization config since #277/PR #284 (no longer forced to `getTensorLike(param)`); the HAR SYM trainer currently pins it FLOAT32 with a rationale comment (packed-SYM momentum would reinstate a dead-zone in the accumulator). Sweep packed SYM@x momentum × rounding; this is the "quantized optimizer momentum" example explicitly wanted.
5. **Backward arithmetic** — `weightGradMath`/`biasGradMath`/`propLossMath` = `ARITH_SYM_INT32` (baseline keeps all backward FLOAT32). Constraints to respect and measure: int32-only accumulators (no int64 in SYM kernels), int12 operand contract / int16 knob (#227), which ops lack SYM backward arms today.
6. **Wire storage** — activations (`outputQ`) and dx (`propLossQ`). **Scoping memo first, code later:** SYM_INT32 wires buy zero memory (4 B/elem, same as FLOAT32) and are blocked by MaxPool/AvgPool backward being non-funnel (raw `float*` casts) and ReLU-backward misreading SYM codes (2026-07-04 spike). Sub-byte *packed* wires would buy real memory but need those backward paths funnel-ized first. **The memo must also name the loss-head boundary:** `crossEntropyForward` hard-exits on non-FLOAT32 input, the fused CE backward has FLOAT32 + ASYM arms only, and the topmost loss-grad wire is generated FLOAT32 with no knob — end-to-end quantized wires on any classifier are pinned at the head regardless of the pool/ReLU fixes. Deliverable: cost/benefit memo + go/no-go, not necessarily an implementation.
7. **ASYM variants** — ASYM grad storage (executeOp arm exists, DYN-rescale only) and the ASYM-via-conversion-layers approach (project vision; Quant-layer ASYM/sub-byte input support restored via #266, closed). Note `crossEntropySoftmaxBackwardAsym` already exists in the loss path — a concrete candidate for the required "at least one ASYM config measured".
8. **Heterogeneous per-layer widths** — everything above assumes ONE uniform width; `layerQuant_t` is already per-layer and `mixed_width_mlp` is the in-repo precedent (weight@8/bias@16/wire@12). Two arms: (a) classic QAT edges-wide/middle-narrow (e.g. sym8 conv1+fc, sym4 middle) vs uniform — the accuracy-per-byte frontier point uniform sweeps cannot reach; (b) **bias width decoupled from weight width** (bias is ~1% of HAR param memory — bias@16 while weights sweep costs nothing and attributes how much axis-1 degradation is bias-induced; exercises the SYM bias-rescale contract directly).
9. **Input/dataset storage quantization** — the model input is the one tensor category no other axis covers (axis 6 = produced wires only; the input sample is dataset-owned). The input `[9,128]` is the largest single activation in the model and `dataset_b` is a first-class category in the memory report the trainer already emits. Deployment-realistic (MCU accelerometer data arrives as integer ADC samples, not float). Investigable today: `requantizeTensorInPlace` on samples post-load; the `ARITH_SYM_INT32` forward prologue converts any operand dtype.
10. **Quantized-checkpoint serialization round-trip** — train packed-SYM HAR → `serializeModel` → rebuild skeleton → `deserializeModel` → verify identical test accuracy. Packed SYM/ASYM payloads are unit-round-trip-tested, but **zero examples call the serial path**, and persist-then-reload of a packed model IS the MCU deployment step. Known hazards to probe and document: deserialize fills a pre-built model without validating file dtype vs destination allocation (a FLOAT32-built skeleton loading a SYM@8 checkpoint is a silent-corruption candidate — if confirmed, file as its own issue); the #270 build-FLOAT32-then-requantize dance as deployment-workflow friction; host-native wire format (size_t width/endianness) as a known cross-arch gap.
11. **Composed profile** — combine the best per-axis settings into a maximally-quantized HAR config; full memory breakdown (weights/grads/momentum/activations/dataset) vs accuracy; the accuracy-vs-memory frontier plot across all axes' results.
## Methodology (non-negotiable, applies to every axis)
- **≥10 seeds per config; claims only from aggregates** (established repo rule). Equal epochs across compared configs.
- Per-config LR tuning is allowed and documented (2026-07-04 spike: shared LR can unfairly break SYM — see the axis-1 flag above); stay clear of the #189 ceiling and note where the resolution floor bites.
- Macro-batch size is currently a `#define BATCH 64` — treat it as a **declared factor** where it plausibly interacts (axis 2: gradient noise dithers the dead-zone; axis 3: accumulate count drives pack-overflow and requant error). One extra level (e.g. B ∈ {16, 64, 256}) on those sweeps, not a global axis.
- Memory numbers via `ODT_MEM_PROFILE` builds (`examples_memprofile` preset); record the reconciliation gap, never massage it.
- Offline sweeps (not CI); CI keeps only the fast FLOAT32 BIT_PARITY gate. Logs + plots committed like the axis-1 sweep.
- Every framework boundary hit (abort, overflow, unsupported arm) is a **finding** — documented in the axis result and, where actionable, filed as its own issue; never worked around silently.
- Trajectory-level comparisons against the PyTorch twin are subject to the known bit-parity-invisible training-divergence phenomenon (#299) — the float twin is a sanity reference, not a gate.
## Out of scope
- Fixing the boundaries this epic documents (each gets its own issue: e.g. pool/ReLU backward funnel-ization, #189 widening, #270 native-SYM factories).
- Replicating the map on other examples (kws_raw, MNIST) — only after the HAR map exists.
- The vestigial `sgdMCreateOptim` qType parameter is being removed independently (#283); the HAR trainers are callers and will be updated there.
## Ordering
1. Wait for the in-flight axis-1 sweep to finish; **ratify its aggregate as the baseline — including the shared-LR-0.01 question flagged above.**
2. Axes 2–5 and 8–10 are then largely independent (order by interest; 10 is cheap and deployment-critical); axis 6/7 scoping memos can happen anytime; axis 11 last.
Contributor guide
Research direction
Start with the in-flight axis-1 logs under examples/har_classifier/logs/ and read the run_matrix.py and compare_memory.py sweep tooling. Run or inspect the ODT_MEM_PROFILE outputs, then ratify the baseline before taking an axis sub-issue. The epic is done when all axis studies have aggregate results, memory breakdowns, documented constraints or blocking issues, and a composed frontier map.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c, python
- Domain
- embedded-iot, machine-learning, performance
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100