Lab 02 — Port PyTorch → ONNX → TensorRT
Goal: take a PyTorch model, export it to ONNX, compile it to a TensorRT engine, validate numerical correctness against the FP16 reference, and keep a log of every op/shape that broke and how you fixed it. That log is the deliverable — it's the JD's "model conversion workflows" and "issue triage" made tangible.
Knowledge prereq: K03 — Model Conversion & Compilers.
Stack: torch, onnx, onnxruntime, onnxsim, tensorrt + trtexec (NVIDIA), Netron (graph viewer). The ONNX half runs anywhere; TensorRT needs NVIDIA.
Steps
-
Pick a model with some teeth — not a toy MLP. A vision model (ResNet/YOLO) is the gentle path; a small transformer (BERT, or a tiny Llama) exposes the dynamic-shape pain that's the real lesson.
-
Export to ONNX:
torch.onnx.export(..., opset_version=17, dynamic_axes={...})ortorch.onnx.dynamo_export. Record the opset and which axes you marked dynamic. -
Inspect in Netron: open the
.onnx, eyeball the graph. Runonnx.checker.check_modelandonnxsim(simplify/constant-fold). Note anything weird (extra Cast/Transpose nodes, unfused attention). -
Validate the ONNX against PyTorch: run both on the same inputs via ONNX Runtime; assert
np.allclose(rtol=1e-3, atol=1e-3). Fix any divergence here before TensorRT — the bug is usually in the export, not the compiler (K03 §10). -
Compile to TensorRT:
trtexec --onnx=model.onnx --saveEngine=model.plan --fp16(add--int8+ a calibration cache for the quantization variant). For dynamic shapes, pass--minShapes/--optShapes/--maxShapes(optimization profiles). -
Hit the breakages (K03 §6) and log each one:
Symptom Class (op / shape / numeric) Root cause Fix e.g. "unsupported op ScatterND"op custom indexing rewrite with gather/slicee.g. "engine fails on batch>1" shape static export add optimization profile e.g. "logits differ by 0.3" numeric FP16 overflow in a layer keep that layer FP32 -
Validate the engine: compare TensorRT outputs vs the FP16 PyTorch reference (max abs error, cosine of logits). If it diverges, bisect: FP32 engine first (isolates a real bug from FP16 drift), then layer-by-layer.
-
Measure the win: latency/throughput of PyTorch-eager vs
torch.compilevs TensorRT. Confirm it's actually faster (sometimes a fallback-to-CPU op makes it slower — measure, don't assume). -
Write the port report (K03 §10): ops fixed, max error, accuracy delta, speedup — the customer-ready artifact.
What you must be able to explain
- Why an engine built on one GPU SKU may not load on another (K03 §4).
- Why dynamic shapes are the hard part and how optimization profiles solve them.
- The difference between "it runs" and "it's correct" — and how you proved correct.
Result / résumé bullet
"Ported a transformer from PyTorch to TensorRT via ONNX: resolved 3 unsupported-op and dynamic-shape failures, validated numerical parity (cosine 0.9998 vs FP16 reference), and delivered a 2.4× latency reduction with a documented conversion-issue log and port report."