Thomas Piacentini
← Workshop

Cheat sheet

Vision Pipelines and Edge Deployment

Original text · English

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

Vision pipeline stages

HTTP requestimage bytesclientDecodepreprocessprocessDetectorYOLO or Mask R-CNNgpuCrop and resizeeach detectionprocessClassifierConvNeXt-TinygpuAggregatedetections and classesprocessPydanticvalidationprocessJSON responseclient
Nodes and connections
NodeRoleKind
HTTP requestimage bytesclient
Decodepreprocessprocess
DetectorYOLO or Mask R-CNNgpu
Crop and resizeeach detectionprocess
ClassifierConvNeXt-Tinygpu
Aggregatedetections and classesprocess
Pydanticvalidationprocess
JSON responseclient
FromToConnection
HTTP requestDecode
DecodeDetector
DetectorCrop and resize
Crop and resizeClassifier
ClassifierAggregate
AggregatePydantic
PydanticJSON response
Sources

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)
Sources

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
Sources

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

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
Sources

Measurement discipline

  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.
Sources

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
Sources

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.
Sources

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.
Sources

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
Sources

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.
Sources

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.
Sources

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.
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.
Code and artifacts 1
What you take away

Saved in your notebook on this device.

Sources and limits

Reading checkpoints are self-reported. Test results are recorded separately and apply to the executed cases.

AI Engineering from Scratch · 3f9715c · MIT

Download this file ↓