Cheat sheets

Vision Pipelines and Edge Deployment

Measure, quantise, export and serve a vision pipeline. Size LLM decode on edge targets by memory bandwidth.

  • Concepts and terms
  • Tasks and commands
  • Annotated code
  • Diagrams
  • Defaults and numbers
  • Troubleshooting
  • Questions and tips

Source

Vision pipeline stages#

clientHTTP requestimage bytesprocessDecodepreprocessgpuDetectorYOLO or Mask R-CNNprocessCrop and resizeeach detectiongpuClassifierConvNeXt-TinyprocessAggregatedetections and classesprocessPydanticvalidationclientJSON responseclientHTTP requestimage bytesprocessDecodepreprocessgpuDetectorYOLO or Mask R-CNNprocessCrop and resizeeach detectiongpuClassifierConvNeXt-TinyprocessAggregatedetections and classesprocessPydanticvalidationclientJSON response

From Lesson 04.16

Tasks in code#

TaskCode
Latency percentilesmeasure_latency(model, input_shape)
Count parametersparameter_count(model)
Estimate FLOPsflops_estimate(model, input_shape)
INT8 static quantisationquantise_ptq(model, calibration_loader)
Export to ONNXexport_onnx(model, sample_input)
Compare 4 backbonescompare_backbones(resolution=160)
Run the pipelinepipe.run(img, image_id="demo")
Time each stagebenchmark(pipe, num_runs=10)

From Lesson 04.15, 04.15 code/main.py, 04.16 code/main.py

Measure latency correctly#

model = model.to(device).eval()
with torch.no_grad():
for _ in range(warmup):5 in code, 10 in the docs
model(x)
torch.cuda.synchronize()before and after, on cuda only
t0 = time.perf_counter()
model(x)
times.append((time.perf_counter() - t0) * 1000)milliseconds
times.sort()
p50 = times[len(times) // 2]
p95 = times[int(len(times) * 0.95)]report percentiles, not the mean

From 04.15 code/main.py, Lesson 04.15

Quantise and export#

import torch.ao.quantization as tq
model.qconfig = tq.get_default_qconfig(backend)backend="x86"
tq.prepare(model, inplace=True)inserts observers
for x, _ in calibration_loader:
model(x)calibrate with real data
tq.convert(model, inplace=True)fuse and quantise
torch.onnx.export(model, sample_input, path,
input_names=["input"], output_names=["output"],
dynamic_axes={"input": {0: "batch"}, "output": {0: "batch"}},any batch size
opset_version=17)safe default in 2026

From Lesson 04.15

Serve and run#

CommandWhat it does
uvicorn main:app --host 0.0.0.0 --port 8000Starts the FastAPI service
curl -F 'file=@dog.jpg' http://localhost:8000/detectSends one image upload
rustc --edition 2021 -O code/main.rsBuilds the Rust edge benchmark
code/main.py (lesson 17.12)Prints decode ceilings per edge target
trtexecTensorRT standalone CLI numbers
benchmark_appOpenVINO standalone CLI numbers

From Lesson 04.16, Lesson 04.15, 04.15 code/main.rs, Lesson 17.12

Defaults and numbers#

SettingDefaultNote
opset_version17ONNX export
Latency loopwarmup=10, iters=50Docs. Code uses 5 and 20
min_crop32Docs. Code uses 16. Smaller boxes skip the classifier
Crop resize224Docs. Code uses 64
backendx86PTQ qconfig
Micro-batch window20msCollect requests, then batch
INT8 accuracy loss0.1-1Percentage points with static PTQ
CPU stage times300-500 msDetect. Preprocess about 3 ms

From Lesson 04.15, 04.15 code/main.py, Lesson 04.16, 04.16 code/main.py

Measurement discipline#

1Warm uprun 5-10 dummy forward passes. Ignore them.2Synchronisecall torch.cuda.synchronize() before and afterthe timed block.3Fix input sizeuse the production resolution. 224x224 is not512x512.4Timeuse time.perf_counter() around each pass.5Reportgive p50, p95 and p99. The mean hides the tail.6Decideuse FLOPs for architecture search. Use on-devicelatency to ship.

From Lesson 04.15

Edge targets and formats#

Apple ANE
38 TOPS, unified memory. INT4 weights + FP16 activations through Core ML.
Qualcomm Hexagon
45 TOPS. QNN INT8 / INT4 through AI Hub converters.
WebGPU / WebLLM
Q4 MLC (q4f16_1) with mlc_llm convert_weight. GGUF is not supported.
Jetson Orin Nano
Q4 GGUF or TRT-LLM INT4. Memory-bound.
Jetson AGX / Thor
NVFP4 + FP8 KV on the Edge-LLM path.
Vision runtimes
export PyTorch to ONNX, then pick ONNX Runtime, TensorRT, Core ML or TFLite.

From Lesson 17.12

Bandwidth sets the decode ceiling#

Ceiling
\text{tok/s} = \frac{\text{bandwidth (GB/s)}}{\text{model size (GB)}}. Decode reads all weights per token.
Mobile
3.5 GB at 50 GB/s takes 70 ms. The ceiling is ~14 tok/s.
Datacenter
HBM3 at 3 TB/s clears 3.5 GB in 1.2 ms. The ceiling is 830 tok/s.
Gap
datacenter to edge bandwidth is 30-50x.
Phone context
4 GB model + 2 GB KV cache for 32K tokens on 8 GB RAM = OOM. Keep 4K-8K.
Conv FLOPs
2 \, c_{in} \, c_{out} \, k_h \, k_w \, h \, w for one conv layer.
INT8
model size and memory bandwidth drop 4x. Compute drops 2-4x.

From Lesson 17.12, 17.12 code/main.py, Lesson 04.15

Troubleshooting#

SymptomCauseFix
First timings are slowCold caches and JIT compilationWarm up 5-10 passes
GPU latency looks tinyYou measure kernel dispatchSynchronise around the timed block
Crops come back emptyBoxes in (cx, cy, w, h)Validate (x1, y1, x2, y2) with Pydantic
Crop fails at the edgeOut-of-bounds boxesClamp to the image size
Corrupt upload gives 500No decode checkReturn 400 with a named error
First request failsModel loads lateLoad at service startup
Phone OOM on long contextKV cache for 32K tokensKeep context at 4K-8K
PTQ accuracy loss too highStatic PTQ is not enoughUse QAT with labelled data

From Lesson 04.15, Lesson 04.16, Lesson 17.12

Abbreviations#

PTQ
post-training quantisation. Quantise a trained model without retraining.
QAT
quantisation-aware training. Best accuracy. Needs labelled data.
ONNX
the neutral model format. Every mainstream runtime reads it.
FLOPs
floating-point operations per forward pass. A rough proxy for compute.
ANE
Apple Neural Engine. On-device NPU with unified memory.
QNN
Qualcomm Neural Network SDK. Gives access to Hexagon.
Unified memory
CPU and NPU see the same RAM. No copy overhead.

From Lesson 04.15, Lesson 17.12

Questions#

Which model for under 3M params?
MobileNetV3-Small. It compiles everywhere.
Which model for 10-20M params?
ConvNeXt-Tiny. It is CPU-friendly.
Why can more FLOPs run faster?
Depthwise convs compile well. Large 7x7 convs do not.
Where does pipeline latency go?
Preprocessing on CPU. The detector takes 70-90% of GPU time.
Why is edge decode slow?
Decode is memory-bound. Compute is secondary.

From Lesson 04.15, Lesson 04.16, Lesson 17.12

Tips#

  • Quantise every edge model to INT8 unless you have a specific reason not to.
  • Measure every table cell on the target device, not the workstation.
  • Log the model name and weights hash in each response.
  • Log every stage timing with a per-request trace ID.
  • If the classifier times out, return detections without classifications.

From Lesson 04.15, Lesson 04.16