YAML Metadata Warning:empty or missing yaml metadata in repo card

Check out the documentation for more information.

VGG16 - TensorRT benchmark (Jetson Orin Nano)

PyTorch -> ONNX (FP32/FP16) -> ONNX Runtime -> TensorRT, plus INT8 and INT4 PTQ through NVIDIA ModelOpt. Every backend is compared for accuracy on ImageNet val images and for numerical parity against one PyTorch FP32 reference.

Run it

source env.sh          # source it - do NOT pipe it, the exports are lost in a subshell
bash run_all.sh

Or step by step (each step writes its own results/*.json, so anything can be re-run in isolation):

python 0_prepare_data.py                                          # splits + uint8 cache
python 1_pytorch_baseline.py                                      # writes results/ref_logits.npy
python 2_export_onnx.py                                           # FP32 + FP16 ONNX
python 3_onnx_eval.py --onnx artifacts/vgg16_fp32.onnx --tag fp32
python 3_onnx_eval.py --onnx artifacts/vgg16_fp16.onnx --tag fp16
python 4_make_calib.py                                            # calibration npz
bash   5_quantize.sh                                              # INT8 + INT8(hp32) + INT4
python 3_onnx_eval.py --onnx artifacts/vgg16_int8.onnx --tag int8 # SIMULATED quant accuracy
python 3_onnx_eval.py --onnx artifacts/vgg16_int4.onnx --tag int4
bash   6_build_engines.sh                                         # trtexec
python 7_trt_eval.py --engine engines/vgg16_fp32.plan --tag fp32 --layer-precisions
python 8_report.py

Engines are not portable: a .plan built on the x86 box will not deserialize on the Jetson. Steps 6-7 must run on the Orin Nano itself. Steps 0-5 produce only ONNX and .npy/.npz, which are portable.

What is load-bearing

Precision comes from the ONNX file, not from trtexec flags. TensorRT 11 builds strongly-typed networks only and has removed --fp16 / --int8 / --best / --calib. 6_build_engines.sh passes --stronglyTyped, which is required on TRT 10 (JetPack, where the builder would otherwise pick its own precisions and ignore the ONNX dtypes) and a documented no-op on TRT 11. So:

ONNX engine
vgg16_fp32.onnx FP32
vgg16_fp16.onnx FP16 (fp16 weights, fp32 IO)
vgg16_int8.onnx INT8 (explicit Q/DQ from ModelOpt)
vgg16_int4.onnx INT4 weight-only (Q/DQ on the classifier Gemms)

One preprocessing path. Images are cached as uint8 after resize(256) + centercrop(224) but before normalization, and every backend reads the same bytes. Preprocessing drift between PyTorch/ORT/TRT is a common source of fake accuracy deltas; here it is structurally impossible.

Eval and calibration images are disjoint. Eval takes from the front of each class list, calibration from the back, asserted in step 0.

VGG16 is the easy INT8 case - and that is the point

The MobileNetV3-Small pipeline in this repo needs a hand-tuned recipe (exclude 11 depthwise convs, quantize Conv/Gemm only) or INT8 collapses from ~68 % to 0.1 % top-1. Two things cause that: depthwise convs, whose per-channel input ranges span orders of magnitude against TensorRT's ONE per-tensor activation scale, and squeeze-excite Mul outputs with max/p99.9 ratios up to 13x.

VGG16 has neither. It is 13 plain dense convs (group=1), ReLU, MaxPool and three Gemms. The default recipe - quantize everything, entropy calibration - is the correct one. Measured on 200 held-out val images, ORT-simulated:

model top-1 agreement vs FP32 cosine sim
INT8 (default recipe) 91.0 % 0.9870
INT4 (weight-only) 98.0 % 0.9994

So do not pre-emptively exclude nodes here: every exclusion is a layer left in float and speedup you paid for and did not get. 5_quantize.sh int8_max and 5_quantize.sh int8_safe exist as fallbacks if your numbers disappoint, not as the default.

Where VGG16's weights actually live

python qdq_tools.py summary --onnx artifacts/vgg16_fp32.onnx:

  Conv       14.715 M  (10.6 %)
  Gemm      123.643 M  (89.4 %)
  total     138.358 M

ModelOpt's ONNX INT4 path is weight-only and quantizes Gemm/MatMul only - it is an LLM-oriented path. On MobileNet that reaches almost nothing. On VGG16 it reaches 89 % of the weights, so the file shrinks hard (528 MB -> 119 MB measured). But those Gemms are ~1 % of VGG16's 15.5 GFLOPs - the convs do the work - so expect the size win and essentially no speedup. That is the honest result, not a misconfiguration.

INT4: the CLI works here (it does not for MobileNetV3)

TensorRT requires the INT4 block size to divide the blocked axis exactly, or it refuses to parse the model with Assertion failed: inputSize % scaleSize == 0: Inferred block size is not an integer. ModelOpt's CLI does not expose --block_size and always uses 128.

VGG16's blocked axes are 25088 / 4096 / 4096 - all clean multiples of 128 (196 / 32 / 32) - so the plain CLI INT4 path works and is the default here. 5_quantize.sh verifies this with qdq_tools.py check-block-size before quantizing rather than assuming it. 5b_quantize_int4.py remains for sweeping block sizes or for a model where 128 stops dividing.

One workaround is still required. ModelOpt 0.45.0 emits blocked INT4 DequantizeLinear nodes with axis=0 while the scale tensor is laid out for axis=1:

classifier.0.weight: weight(4096, 25088) scale(4096, 196) block=128  axis 0 -> 1

ORT and TRT both reject that (x_scale must be ceil(Di/block_size) on the quantize axis). qdq_tools.py fix-int4-axis patches it and 5_quantize.sh runs it automatically after every INT4 quantization.

Memory on the Orin Nano

VGG16 is ~250x the FLOPs of MobileNetV3-Small and its largest activation is 12.8 MB per image, against 8 GB shared between CPU and GPU. Defaults are set for that, not for the dev box:

  • eval batch 16 everywhere (C.DEFAULT_BATCH, override with EVAL_BATCH)
  • calibration batch 10 -> 100 iterations over 1000 images
  • engine profile min/opt/max = 1/1/16, workspace 2048 MiB on Jetson

7_trt_eval.py reads the engine's profile back and clamps --batch to it, so a mismatch is a clear message instead of a TensorRT assertion mid-run.

Builds are the slow part. OPTLEVEL=1 bash run_all.sh cuts build time a lot at some engine quality; WORKSPACE=1024 MAXB=8 bash 6_build_engines.sh if memory is tight.

Files

file role
env.sh venv + LD_LIBRARY_PATH + trtexec. Portable x86 / Jetson, every fix conditional
common.py dataset splits, preprocessing, metrics - the single source of truth
check_dataset.py images-per-class distribution of the val folder
0_prepare_data.py disjoint eval/calib splits + uint8 cache
1_pytorch_baseline.py FP32 baseline, writes results/ref_logits.npy
2_export_onnx.py ONNX FP32 + FP16 (keep_io_types=True)
3_onnx_eval.py ORT accuracy/latency/parity - works for every precision
4_make_calib.py calibration npz, key validated against the real graph
5_quantize.sh ModelOpt PTQ, CLI only
5b_quantize_int4.py INT4 with an explicit block size (off the default path)
qdq_tools.py graph summary, block-size check, sensitive-node list, INT4 axis fix
6_build_engines.sh trtexec engine builds + trtexec's own profiling
7_trt_eval.py TRT accuracy/latency/parity + per-layer precision histogram
8_report.py collects results/*.json into one table + results/REPORT.md

Reading the results

  • ORT Mean ms includes host<->device copies; TRT Mean ms is GPU compute only. They are not comparable - use TRT for the real latency story.
  • CosSim / Agree % are against results/ref_logits.npy, the PyTorch FP32 logits. Accuracy alone can hide a broken backend: two models can score the same top-1 while disagreeing on a third of the images.
  • --layer-precisions shows what TensorRT actually chose. A high reformat_layers count means the recipe left too many layers in float and TRT is paying to convert back and forth. On VGG16 that count should be low - the network is one long chain of convs, so an INT8 region can stay INT8 end to end.
Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support