From FP32 to INT8 on the Edge: A Production Pipeline for Quantization-Aware Vision Models
Quantization is not a checkbox at export time — it is an engineering pipeline that spans training, calibration, compilation and on-device validation.
Engineering
How to size, build and benchmark an edge GPU pipeline that sustains many camera streams at a known latency and accuracy, not just a fast single-camera demo.
Most edge video analytics demos show one camera, one model and a smooth overlay. Production systems are judged on a different number: how many streams a single device can sustain at a defined latency and accuracy, for weeks, inside a hot enclosure. That number is set less by the detector than by everything around it: decode, colour conversion, memory movement, batching, scheduling and the inference engine build. This post walks through how we design multi-stream pipelines on edge GPUs (Jetson-class modules and small discrete GPUs), where the time goes, what breaks, and how to benchmark them so the result means something.
Before choosing a network, write down the arithmetic. Sixteen cameras at 15 fps is 240 frames per second; if every frame goes through the detector, the GPU has on average about 4.2 ms per frame for everything it touches. That budget is shared by preprocessing, inference, post-processing and any secondary models, and it shrinks when the device thermally throttles. Doing this sum first tells you immediately whether you need a smaller model, a lower inference rate, region-of-interest cropping or a bigger device. It also turns the vague goal of "real time" into testable service-level objectives: maximum end-to-end latency at p99, maximum frame-drop rate, and minimum accuracy relative to a reference.
A sustainable pipeline keeps pixels on the accelerator from the moment they are decoded until only metadata leaves. The stages are:
On Jetson-class devices the CPU and GPU share physical DRAM, so "zero-copy" means passing buffer handles (for example NVMM/dmabuf surfaces) rather than calling cudaMemcpy. On discrete GPUs the goal is the same: decode into device memory and keep it there. Every unnecessary copy costs memory bandwidth, which on embedded modules is usually the binding constraint before raw compute.
Batching is where throughput is won and latency is lost. Larger batches improve GPU utilisation, but a frame waiting for a batch to fill is a frame getting older. We use a time-bounded batch former: emit a batch when it reaches the engine's optimal size or when the oldest frame has waited longer than a deadline, whichever comes first.
loop:
wait until queue.size >= max_batch or now - queue.oldest_ts >= max_wait_ms
batch = take_round_robin(queue, max_batch) # fairness across streams
if batch.size < engine.opt_batch: pad or use a smaller profile
submit(batch, cuda_stream[next]) # double/triple buffered
Three details matter. First, fairness: pull round-robin across streams so a high-motion camera cannot starve others. Second, engine profiles: a dynamic-shape engine built with optimisation profiles for, say, batch 1, 4 and 8 avoids wasting work on padding when traffic is light. Third, overlap: use multiple CUDA streams so preprocessing of batch n+1 overlaps inference of batch n and post-processing of batch n-1.
The inference engine build is where most of the per-frame speed comes from, and where many accuracy regressions are introduced.
Engine builders fuse convolution, batch-norm and activation into single kernels, merge element-wise ops and choose tactics (kernel implementations) by timing them on the target GPU. We push this further by moving preprocessing into the graph: mean/std normalisation and channel reordering become a cheap first layer instead of a separate pass over memory. Box decoding and NMS run as an engine plugin (for example an efficient-NMS plugin) so candidate boxes never leave the GPU.
FP16 is usually close to free on accuracy. INT8 needs care. Post-training quantisation with a calibration set works when the calibration data matches deployment conditions: night scenes, rain, glare, the actual camera models and compression levels. When it does not, we use quantisation-aware training and export explicit Q/DQ nodes so the engine honours the scales learned during training rather than inventing its own. Sensitive layers, often the first convolution and detection heads, can be kept at FP16 in a mixed-precision engine.
Compiled engines are specific to the GPU architecture and the inference library version. They are build artefacts, not source. We treat the ONNX graph as the portable artefact and build engines per target in CI, recording the toolkit versions, the build flags and a hash of the calibration cache alongside the engine.
The cheapest inference is the one you skip. Techniques that compound:
A single "frames per second" figure hides almost everything that matters. The method we use:
Our engagements on multi-stream edge video follow a consistent engineering path:
The outcome we aim for is not the fastest demo; it is a pipeline whose capacity, latency and accuracy are known, measured and stable enough to plan a deployment around.
This post describes real client work. Bring us yours