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
Sources
Original source
---
id: vision-edge-deployment
title: Vision Pipelines and Edge Deployment
code: VP
subtitle: Measure, quantise, export and serve a vision pipeline. Size LLM decode on edge targets by memory bandwidth.
hue: 25
sources: phases/04-computer-vision/15-real-time-edge, phases/04-computer-vision/16-vision-pipeline-capstone, phases/17-infrastructure-and-production/12-edge-inference
source_hash: bbe79d292f291682
---
## Vision pipeline stages
type: architecture
from: phases/04-computer-vision/16-vision-pipeline-capstone/docs/en.md
```arch
node | req | 1,1 | HTTP request | image bytes | client
node | load | 2,1 | Decode | preprocess | process
node | det | 3,1 | Detector | YOLO or Mask R-CNN | gpu
node | crop | 4,1 | Crop and resize | each detection | process
node | cls | 4,2 | Classifier | ConvNeXt-Tiny | gpu
node | agg | 3,2 | Aggregate | detections and classes | process
node | schema | 2,2 | Pydantic | validation | process
node | resp | 1,2 | JSON response | | client
edge | req | load |
edge | load | det |
edge | det | crop |
edge | crop | cls |
edge | cls | agg |
edge | agg | schema |
edge | schema | resp |
```
## Tasks in code
type: task-table
from: phases/04-computer-vision/15-real-time-edge/docs/en.md, phases/04-computer-vision/15-real-time-edge/code/main.py, phases/04-computer-vision/16-vision-pipeline-capstone/code/main.py
| Task | Code |
|---|---|
| Latency percentiles | `measure_latency(model, input_shape)` |
| Count parameters | `parameter_count(model)` |
| Estimate FLOPs | `flops_estimate(model, input_shape)` |
| INT8 static quantisation | `quantise_ptq(model, calibration_loader)` |
| Export to ONNX | `export_onnx(model, sample_input)` |
| Compare 4 backbones | `compare_backbones(resolution=160)` |
| Run the pipeline | `pipe.run(img, image_id="demo")` |
| Time each stage | `benchmark(pipe, num_runs=10)` |
## Measure latency correctly
type: annotated-code
lang: python
from: phases/04-computer-vision/15-real-time-edge/code/main.py, phases/04-computer-vision/15-real-time-edge/docs/en.md
```python
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
```
## Quantise and export
type: annotated-code
lang: python
from: phases/04-computer-vision/15-real-time-edge/docs/en.md
```python
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
```
## Serve and run
type: command-table
from: phases/04-computer-vision/16-vision-pipeline-capstone/docs/en.md, phases/04-computer-vision/15-real-time-edge/docs/en.md, phases/04-computer-vision/15-real-time-edge/code/main.rs, phases/17-infrastructure-and-production/12-edge-inference/docs/en.md
| Command | What it does |
|---|---|
| `uvicorn main:app --host 0.0.0.0 --port 8000` | Starts the FastAPI service |
| `curl -F 'file=@dog.jpg' http://localhost:8000/detect` | Sends one image upload |
| `rustc --edition 2021 -O code/main.rs` | Builds the Rust edge benchmark |
| `code/main.py` (lesson 17.12) | Prints decode ceilings per edge target |
| `trtexec` | TensorRT standalone CLI numbers |
| `benchmark_app` | OpenVINO standalone CLI numbers |
## Measurement discipline
type: flow
from: phases/04-computer-vision/15-real-time-edge/docs/en.md
1. **Warm up**: run 5-10 dummy forward passes. Ignore them.
2. **Synchronise**: call `torch.cuda.synchronize()` before and after the timed block.
3. **Fix input size**: use the production resolution. 224x224 is not 512x512.
4. **Time**: use `time.perf_counter()` around each pass.
5. **Report**: give p50, p95 and p99. The mean hides the tail.
6. **Decide**: use FLOPs for architecture search. Use on-device latency to ship.
## Defaults and numbers
type: defaults
from: phases/04-computer-vision/15-real-time-edge/docs/en.md, phases/04-computer-vision/15-real-time-edge/code/main.py, phases/04-computer-vision/16-vision-pipeline-capstone/docs/en.md, phases/04-computer-vision/16-vision-pipeline-capstone/code/main.py
| Setting | Default | Note |
|---|---|---|
| `opset_version` | 17 | ONNX export |
| Latency loop | `warmup=10, iters=50` | Docs. Code uses 5 and 20 |
| `min_crop` | 32 | Docs. Code uses 16. Smaller boxes skip the classifier |
| Crop resize | 224 | Docs. Code uses 64 |
| `backend` | x86 | PTQ qconfig |
| Micro-batch window | 20ms | Collect requests, then batch |
| INT8 accuracy loss | 0.1-1 | Percentage points with static PTQ |
| CPU stage times | 300-500 ms | Detect. Preprocess about 3 ms |
## Edge targets and formats
type: concepts
from: phases/17-infrastructure-and-production/12-edge-inference/docs/en.md
- **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.
## Bandwidth sets the decode ceiling
type: concepts
from: phases/17-infrastructure-and-production/12-edge-inference/docs/en.md, phases/17-infrastructure-and-production/12-edge-inference/code/main.py, phases/04-computer-vision/15-real-time-edge/docs/en.md
- **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.
## Troubleshooting
type: troubleshooting
from: phases/04-computer-vision/15-real-time-edge/docs/en.md, phases/04-computer-vision/16-vision-pipeline-capstone/docs/en.md, phases/17-infrastructure-and-production/12-edge-inference/docs/en.md
| Symptom | Cause | Fix |
|---|---|---|
| First timings are slow | Cold caches and JIT compilation | Warm up 5-10 passes |
| GPU latency looks tiny | You measure kernel dispatch | Synchronise around the timed block |
| Crops come back empty | Boxes in (cx, cy, w, h) | Validate (x1, y1, x2, y2) with Pydantic |
| Crop fails at the edge | Out-of-bounds boxes | Clamp to the image size |
| Corrupt upload gives 500 | No decode check | Return 400 with a named error |
| First request fails | Model loads late | Load at service startup |
| Phone OOM on long context | KV cache for 32K tokens | Keep context at 4K-8K |
| PTQ accuracy loss too high | Static PTQ is not enough | Use QAT with labelled data |
## Abbreviations
type: glossary
from: phases/04-computer-vision/15-real-time-edge/docs/en.md, phases/17-infrastructure-and-production/12-edge-inference/docs/en.md
- **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.
## Questions
type: qa
from: phases/04-computer-vision/15-real-time-edge/docs/en.md, phases/04-computer-vision/16-vision-pipeline-capstone/docs/en.md, phases/17-infrastructure-and-production/12-edge-inference/docs/en.md
- **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.
## Tips
type: tips
from: phases/04-computer-vision/15-real-time-edge/docs/en.md, phases/04-computer-vision/16-vision-pipeline-capstone/docs/en.md
- 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.