Skip to content

Repository files navigation

Mobile ML Optimization — On-Device Intent Classifier

Take a compact NLP intent classifier from an fp32 baseline to a set of mobile-ready inference artifacts, and make the deployment choice with a reproducible quality vs latency vs size vs memory vs energy benchmark — not a guess.

Problem

A support / assistant / productivity mobile app needs to classify short user phrases (e.g. "I am still waiting on my card?"card_arrival) on device, without a mandatory cloud round-trip. Even an accurate model is often unshippable on mobile because it is too large, too slow, memory-hungry, or battery-hungry, and forcing cloud inference hurts latency, cost and privacy.

This project shows the engineering path from "a model that works" to "a model you can actually ship on device", and — crucially — a measured recommendation for which optimized variant to deploy.

  • Use case: on-device intent classification for short user messages.
  • Dataset: Banking77 — 77 fine-grained banking-support intents, ~13k short utterances. Single-domain and clean, so the optimization story is the star, not a fight for SOTA accuracy.
  • Users: ML / mobile-ML / edge-AI engineers, applied scientists, and platform teams responsible for inference efficiency and on-device UX.

Approach — optimization pipeline

                         ┌──────────────┐
   Banking77  ──▶ train  │ baseline fp32│  BERT-tiny (2L, H128, 4.4M params)
                         └──────┬───────┘
                                │
        ┌───────────┬───────┬───────┬───────────────┬──────────────┬──────────────┐
        ▼           ▼       ▼       ▼               ▼              ▼              ▼
   ONNX export  ONNX int8 torch  unstructured   distillation  structured    structured
   (fp32 graph) (dynamic) int8   prune (50%)    (mini→tiny)   FFN prune     + ONNX int8
        │           │       │     + recover                   512→256       (mobile pick)
        │           │       │                                 + recover
        └───────────┴───────┴───────────────┴────────────────┴──────────────┴──────────────┘
                                │
                                ▼
              benchmark suite  →  quality / latency / size / memory / energy
                                →  deployment recommendation

Every variant is measured on the same axes, single-threaded on CPU to emulate on-device single-core inference.

What is measured

Axis Metric
Quality test accuracy, macro-F1 (77 classes)
Size deployable artifact size (MB)
Load cold model/session load time (s)
Latency single-sample p50 / p95 / mean (ms, 1 thread)
Memory steady-state process RSS (MB)
Energy CPU-seconds per inference — compute proxy, honestly labeled

Energy is a proxy. Without on-device power measurement, CPU-time per inference is used as a transparent compute/energy proxy. It is labeled as such everywhere and not presented as a true battery measurement.

Results

Single-thread CPU, Banking77 test set (3076 samples, 77 classes). Latency is single-sample (batch=1).

variant accuracy macro-F1 size (MB) load (s) p50 (ms) p95 (ms) RSS (MB) energy-proxy (cpu-ms/infer)
baseline_fp32_torch 0.8644 0.8610 16.77 6.08 1.501 1.963 426 1.641
onnx_fp32 0.8644 0.8610 16.82 5.20 0.541 0.609 481 0.547
onnx_int8 0.8638 0.8601 4.29 4.76 0.396 0.467 472 0.391
torch_int8_dynamic 0.8628 0.8592 15.60 4.99 2.048 2.372 439 2.109
pruned_fp32_torch (unstructured 50%) 0.8807 0.8801 16.77 5.48 1.536 1.912 428 1.641
distilled_fp32_torch 0.8748 0.8744 16.77 5.81 1.593 2.141 427 1.719
structured_pruned_torch (FFN 512→256) 0.8872 0.8872 16.27 5.72 1.489 2.119 425 1.562
structured_onnx_fp32 0.8872 0.8872 16.32 5.32 0.575 0.660 468 0.625
structured_onnx_int8 0.8885 0.8885 4.17 4.82 0.367 0.427 455 0.312

Quality vs latency trade-off

structured_onnx_int8 sits top-left — highest quality at the lowest latency.

Artifact size by variant p50/p95 latency by variant

Deployment pick (mobile): structured_onnx_int8 — macro-F1 0.8885 (the best of all variants), 4.0× smaller (4.17 MB) and 4.1× faster (p50 0.367 ms) than the fp32 baseline. It Pareto-dominates every other variant: nothing is smaller, faster, and more accurate. Structured FFN pruning (with a recovery fine-tune) + dynamic int8.

Reading the table

  • ONNX fp32 reproduces the torch baseline's F1 exactly (0.8610) → the export is correct; it is also ~3× faster than eager PyTorch purely from graph optimization.
  • Dynamic int8 is the size lever — 4× smaller at ≤0.2 F1-point cost.
  • torch dynamic int8 is slower than fp32 here — at hidden size 128, int8 dynamic-quant overhead on CPU outweighs the compute saving. Honest counter-example: quantization is not automatically faster; the ONNX int8 path is the one that pays off.
  • Unstructured pruning ≠ latency. 50% of Linear weights zeroed, quality kept — but dense CPU/ONNX kernels don't exploit unstructured sparsity, so size and latency are unchanged. Its payoff is only size-if-stored-sparse.
  • Structured pruning is the right latency technique — it physically removes FFN neurons (512→256), so the matrices really shrink. But at this tiny scale single-sample latency is overhead-bound, not compute-bound: the fp32 p50 (0.575 ms) is within measurement noise of the unpruned ONNX (0.541 ms). The FFN FLOP saving only clearly surfaces in the int8 path, where structured_onnx_int8 (0.367 ms) edges plain onnx_int8 (0.396 ms). The bigger, reliable payoff of structured pruning here was a quality gain from the recovery fine-tune — which is why it also tops the F1 column.
  • Size stays embedding-bound. BERT-tiny is 3.9M of 4.4M params in the word-embedding table, so no amount of FFN/attention pruning moves fp32 size much (16.8 → 16.3 MB); quantization is what collapses size (→4.2 MB). The two levers are complementary.
  • RSS is process-level (framework + libs dominate the ~400–480 MB, not the 4–17 MB model); onnxruntime rows sit higher from the ORT library footprint, so RSS is a weak cross-framework signal — size and latency are the real mobile axes.

Honest trade-off notes

  • ONNX dynamic int8 is the size lever: ~4× smaller artifact at a small, measured quality cost.
  • Unstructured magnitude pruning zeroes 50% of Linear weights and keeps quality after a short recovery fine-tune — but on dense CPU/ONNX kernels it does not reduce latency or size; its payoff is only size-if-stored-sparse. Reported as such.
  • Structured FFN pruning physically removes intermediate neurons (512→256), so it is the correct latency technique — but at hidden=128 single-sample inference is overhead-bound, so the FFN FLOP saving is within noise on fp32 and only edges ahead in the int8 path. Its clearest win here was a quality gain from the recovery fine-tune.
  • Distillation keeps the compact student architecture but transfers signal from a larger teacher — any quality gain at equal size is a clean win.
  • Size vs latency are separate levers: structured pruning targets compute/latency; quantization targets size (embedding-bound). Combining them gives the Pareto-best artifact.
  • The pruned / structured / distilled variants receive extra training epochs (recovery / distill), so their quality is not strictly epoch-matched to the baseline; the point is the deployment trade-off, not a controlled training-budget ablation.

Repro

py -3.11 -m venv .venv
.venv/Scripts/python -m pip install -r requirements.txt

.venv/Scripts/python training/prepare_data.py        # splits
.venv/Scripts/python training/train_baseline.py      # baseline fp32
.venv/Scripts/python optimization/export_onnx.py     # ONNX fp32 + int8
.venv/Scripts/python optimization/prune.py           # unstructured pruned variant
.venv/Scripts/python optimization/distill.py         # distilled variant (optional, slow)
.venv/Scripts/python optimization/structured_prune.py # structured FFN prune + ONNX export
.venv/Scripts/python benchmarks/run_all.py           # benchmark + report
.venv/Scripts/python benchmarks/report.py            # re-render report from cached CSV

Try it

After the repro steps above, classify any phrase with the champion model:

.venv/Scripts/python predict.py "I am still waiting on my card?"
query: "I am still waiting on my card?"   (model: structured_pruned)
  card_arrival                 0.940
  card_delivery_estimate       0.025
  lost_or_stolen_card          0.008

Business impact

  • Lower serving cost & cloud dependency — inference moves on-device; the backend stops paying per request for a task that fits in ~4 MB.
  • Better UX — lower, more predictable latency (p50/p95 reported) means the assistant feels instant even offline.
  • Stronger privacy — user phrases are classified locally, never leaving the device.

What this proves about me

  • I optimize models against real deployment constraints, not just accuracy.
  • I understand quality / latency / size / memory trade-offs and can measure them.
  • I take an ML artifact all the way to a production-friendly, exportable form.
  • I can defend an optimization decision in the language of product and business.

CV bullets

  • Built a reproducible model-optimization lab taking a compact BERT intent classifier from an fp32 baseline through ONNX export, dynamic int8, unstructured + structured pruning and distillation, benchmarked on a single-thread CPU across quality/latency/size/memory/energy-proxy (9 variants).
  • Produced a Pareto-dominant on-device artifact (structured FFN pruning + ONNX int8): 4.0× smaller, 4.1× faster, and higher macro-F1 than the fp32 baseline.
  • Diagnosed why techniques did or didn't pay off — unstructured pruning can't speed up dense kernels, sub-ms inference at hidden=128 is overhead- not compute-bound, and model size is embedding-bound so quantization (not pruning) is the size lever.
  • Implemented an honest energy/compute proxy and bounded quality regression, avoiding the common "optimize without benchmarking" trap; packaged it tying efficiency metrics to serving cost, UX latency and on-device privacy.

Interview talking points

  • Why on-device intent classification is a size/latency problem before it is an accuracy problem.
  • Unstructured vs structured pruning: why the first can't speed up dense kernels and the second can — and why even structured pruning was latency-neutral here because sub-millisecond inference at hidden=128 is overhead-bound, not compute-bound.
  • Why size stayed embedding-bound (3.9M/4.4M params) so quantization — not pruning — is the size lever, making the two techniques complementary rather than competing.
  • How dynamic int8 quantization trades a controlled quality delta for 4× size, and why torch dynamic-quant was slower while ONNX int8 was faster.
  • How to build an energy proxy you can defend instead of overclaiming battery impact.

About

On-device NLP intent classifier optimized from fp32 baseline to a mobile-ready artifact — quantization, structured/unstructured pruning & distillation, benchmarked on quality/latency/size/energy. Best variant: 4x smaller, 4x faster, higher F1.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages