TensorRT engines, quantization, pruning and the deployment settings that decide inference throughput on Jetson devices.
What optimization changes
A model that runs well on a server still has to fit a small, battery-powered device in the field. Optimization makes it:
- Smaller: less memory usage
- Faster: more frames per second
- Cheaper: less power consumption
The four techniques this post uses:
| Technique | What It Does | Real-World Analogy |
|---|---|---|
| FP16 (Half Precision) | Uses smaller numbers | Using rounded numbers instead of exact decimals |
| INT8 (Quantization) | Uses even smaller numbers | Rounding $19.99 to $20 |
| Pruning | Removes unnecessary parts | Trimming dead branches from a tree |
| Knowledge Distillation | Smaller model learns from bigger one | A student learning from a teacher |
A typical result across the full pipeline:
| Before Optimization | After Optimization |
|---|---|
| 5 frames/second | 75 frames/second |
| 500MB model size | 50MB model size |
| 30 watts power | 10 watts power |
Jetson devices combine GPU acceleration, dedicated Deep Learning Accelerators (DLAs) and power efficiency. Deploying neural networks on them takes optimization work that goes well beyond standard training practice.
What follows is the pipeline from model architecture selection through TensorRT engine deployment, with code and benchmarks: engine building, quantization, pruning and distillation, memory management, batch sizing, and DLA offload. It applies to the entry-level Jetson Nano (128 CUDA cores, 4GB unified memory) and to the Jetson AGX Orin (2048 CUDA cores, 64GB memory, dual DLAs).
1. TensorRT optimization techniques: INT8 and FP16 quantization
Understanding precision modes
TensorRT supports multiple precision modes that trade accuracy for performance. The Jetson Orin series, with its Tensor Core architecture, particularly benefits from reduced precision inference.
| Precision | Memory Bandwidth | Compute Throughput | Accuracy Impact |
|---|---|---|---|
| FP32 | Baseline | Baseline | None |
| FP16 | 2x reduction | 2x speedup | Minimal (<0.5%) |
| INT8 | 4x reduction | 4x speedup | 1-3% (with calibration) |
Research on NVIDIA Jetson AGX Orin demonstrates that quantized TensorRT engines achieve approximately 14.87x speedup compared to PyTorch models in full precision for architectures like MobileNet and SqueezeNet.
Explicit vs. implicit quantization
TensorRT distinguishes between explicit and implicit quantization approaches:
Implicit Quantization (Deprecated): TensorRT treats the model as floating-point and opportunistically uses INT8 where beneficial. This approach is deprecated and no longer recommended.
Explicit Quantization: Uses IQuantizeLayer and IDequantizeLayer nodes (Q/DQ nodes) to explicitly define quantization points in the graph. This is the recommended approach for production deployments.
import tensorrt as trt
def build_int8_engine(onnx_path, calibrator, workspace_size=1<<30):
"""Build TensorRT INT8 engine with explicit quantization."""
logger = trt.Logger(trt.Logger.WARNING)
builder = trt.Builder(logger)
network = builder.create_network(
1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
)
parser = trt.OnnxParser(network, logger)
# Parse ONNX model
with open(onnx_path, 'rb') as f:
if not parser.parse(f.read()):
for error in range(parser.num_errors):
print(parser.get_error(error))
return None
# Configure builder
config = builder.create_builder_config()
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, workspace_size)
# Enable INT8 mode with calibrator
config.set_flag(trt.BuilderFlag.INT8)
config.int8_calibrator = calibrator
# Also enable FP16 for layers that benefit from it
config.set_flag(trt.BuilderFlag.FP16)
# Build engine
serialized_engine = builder.build_serialized_network(network, config)
return serialized_engineINT8 calibration implementation
Post-Training Quantization (PTQ) requires a calibration step that executes the model with sample data to determine optimal scaling factors. TensorRT supports multiple calibration algorithms:
- Absolute-Max: Simple threshold-based scaling (fastest, less accurate)
- KL-Entropy (Entropy): Minimizes Kullback-Leibler divergence (default, recommended)
- MSE: Minimizes mean-squared error (best for error-sensitive applications)
import tensorrt as trt
import numpy as np
from PIL import Image
import os
class ImageCalibrator(trt.IInt8EntropyCalibrator2):
"""Custom calibrator for INT8 quantization on Jetson."""
def __init__(self, calibration_dir, batch_size=8,
input_shape=(3, 224, 224), cache_file="calibration.cache"):
super().__init__()
self.batch_size = batch_size
self.input_shape = input_shape
self.cache_file = cache_file
# Collect calibration images
self.image_paths = [
os.path.join(calibration_dir, f)
for f in os.listdir(calibration_dir)
if f.endswith(('.jpg', '.png', '.jpeg'))
][:512] # Limit to 512 images for efficiency
self.current_index = 0
self.device_input = None
# Allocate device memory
import pycuda.driver as cuda
import pycuda.autoinit
self.device_input = cuda.mem_alloc(
batch_size * np.prod(input_shape) * np.dtype(np.float32).itemsize
)
def get_batch_size(self):
return self.batch_size
def get_batch(self, names):
if self.current_index >= len(self.image_paths):
return None
batch_images = []
for i in range(self.batch_size):
if self.current_index + i >= len(self.image_paths):
break
# Load and preprocess image (match your inference preprocessing)
img = Image.open(self.image_paths[self.current_index + i])
img = img.resize((self.input_shape[2], self.input_shape[1]))
img = np.array(img).astype(np.float32) / 255.0
img = (img - [0.485, 0.456, 0.406]) / [0.229, 0.224, 0.225]
img = img.transpose(2, 0, 1)
batch_images.append(img)
self.current_index += self.batch_size
if not batch_images:
return None
batch = np.stack(batch_images).astype(np.float32)
import pycuda.driver as cuda
cuda.memcpy_htod(self.device_input, batch.ravel())
return [int(self.device_input)]
def read_calibration_cache(self):
if os.path.exists(self.cache_file):
with open(self.cache_file, 'rb') as f:
return f.read()
return None
def write_calibration_cache(self, cache):
with open(self.cache_file, 'wb') as f:
f.write(cache)Performance benchmarks: FP16 vs INT8
On Jetson AGX Xavier with FP16 quantization, real-time processing at 48 FPS is achievable for object detection tasks. However, certain architectures like YOLOv5 may not show significant INT8 improvements over FP16 on Jetson Orin Nano because the 16-bit compute path is already highly optimized on this hardware.
Best Practice: Always benchmark both FP16 and INT8 on your target Jetson device. Use per-tensor quantization for activations and per-channel quantization for weights for optimal accuracy.
2. Model pruning and knowledge distillation
Structured pruning techniques
Pruning removes redundant weights, layers, or attention heads to reduce model size. Two primary approaches exist:
- Depth Pruning: Removes entire layers from the network
- Width Pruning: Removes neurons, attention heads, or embedding channels
The NVIDIA Model Optimizer library implements both, with a choice of ranking criteria:
from modelopt.torch.prune import prune
# Example: Structured pruning with LAMP algorithm
pruned_model = prune(
model,
mode="mse", # or "fisher", "lamp"
target_sparsity=0.5, # Remove 50% of parameters
granularity="channel" # Structured pruning for efficient inference
)
# Fine-tune pruned model
optimizer = torch.optim.Adam(pruned_model.parameters(), lr=1e-5)
for epoch in range(fine_tune_epochs):
train_epoch(pruned_model, train_loader, optimizer)Results from YOLO optimization show that the LAMP pruning algorithm can achieve:
- 74.7% parameter reduction
- 50.5% FLOPs reduction
- 66.5% inference time reduction
- 73.8% model size reduction
All while maintaining nearly lossless accuracy.
Knowledge distillation for edge deployment
Knowledge distillation transfers learning from a large "teacher" model to a smaller "student" model optimized for edge deployment.
Response Knowledge Distillation: Matches output probability distributions using KL divergence loss.
Feature Knowledge Distillation: Matches intermediate feature representations using MSE loss.
import torch
import torch.nn.functional as F
class DistillationLoss(torch.nn.Module):
"""Combined distillation loss for Jetson-optimized students."""
def __init__(self, alpha=0.5, temperature=4.0):
super().__init__()
self.alpha = alpha
self.temperature = temperature
self.ce_loss = torch.nn.CrossEntropyLoss()
def forward(self, student_logits, teacher_logits, labels):
# Hard label loss (standard cross-entropy)
hard_loss = self.ce_loss(student_logits, labels)
# Soft label loss (knowledge distillation)
soft_student = F.log_softmax(student_logits / self.temperature, dim=1)
soft_teacher = F.softmax(teacher_logits / self.temperature, dim=1)
soft_loss = F.kl_div(soft_student, soft_teacher, reduction='batchmean')
soft_loss = soft_loss * (self.temperature ** 2)
# Combined loss
return self.alpha * hard_loss + (1 - self.alpha) * soft_loss
# Training loop
teacher_model.eval()
student_model.train()
for images, labels in train_loader:
with torch.no_grad():
teacher_logits = teacher_model(images)
student_logits = student_model(images)
loss = distillation_loss(student_logits, teacher_logits, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()After distillation and TensorRT optimization, a ResNet18 student can achieve 4.2x faster inference than the original OpenCLIP teacher while using 3.45x less memory.
3. ONNX conversion and optimization
PyTorch to ONNX export with dynamic axes
ONNX is the interchange format between training frameworks and TensorRT. Export configuration decides whether dynamic batching works downstream.
import torch
import torch.onnx
def export_model_to_onnx(model, dummy_input, output_path, dynamic_batch=True):
"""Export PyTorch model to ONNX with Jetson-optimized settings."""
model.eval()
# Define dynamic axes for flexible batch sizes
dynamic_axes = None
if dynamic_batch:
dynamic_axes = {
'input': {0: 'batch_size'},
'output': {0: 'batch_size'}
}
torch.onnx.export(
model,
dummy_input,
output_path,
export_params=True,
opset_version=17, # Use latest supported opset
do_constant_folding=True,
input_names=['input'],
output_names=['output'],
dynamic_axes=dynamic_axes,
verbose=False
)
# Verify and simplify ONNX model
import onnx
from onnxsim import simplify
model_onnx = onnx.load(output_path)
onnx.checker.check_model(model_onnx)
# Simplify for better TensorRT compatibility
model_simplified, check = simplify(model_onnx)
if check:
onnx.save(model_simplified, output_path)
print(f"Simplified ONNX model saved to {output_path}")
return output_path
# Example usage
dummy_input = torch.randn(1, 3, 640, 640)
export_model_to_onnx(yolo_model, dummy_input, "yolov8n.onnx")trtexec command-line optimization
The trtexec tool provides the fastest path to TensorRT engine creation with extensive profiling capabilities.
# FP16 optimization (recommended baseline)
/usr/src/tensorrt/bin/trtexec \
--onnx=yolov8n.onnx \
--saveEngine=yolov8n_fp16.engine \
--fp16 \
--workspace=4096 \
--verbose
# INT8 with calibration cache
/usr/src/tensorrt/bin/trtexec \
--onnx=yolov8n.onnx \
--saveEngine=yolov8n_int8.engine \
--int8 \
--calib=calibration.cache \
--workspace=4096
# Dynamic shapes with optimization profile
/usr/src/tensorrt/bin/trtexec \
--onnx=model_dynamic.onnx \
--saveEngine=model_dynamic.engine \
--minShapes=input:1x3x224x224 \
--optShapes=input:8x3x224x224 \
--maxShapes=input:32x3x224x224 \
--fp16
# Best performance mode (all precisions enabled)
/usr/src/tensorrt/bin/trtexec \
--onnx=model.onnx \
--saveEngine=model_best.engine \
--best \
--useDLACore=0 \
--allowGPUFallbackONNX Runtime with TensorRT execution provider
For applications requiring ONNX Runtime compatibility with TensorRT acceleration:
import onnxruntime as ort
def create_trt_session(onnx_path, use_fp16=True, use_int8=False):
"""Create ONNX Runtime session with TensorRT EP for Jetson."""
providers = [
('TensorrtExecutionProvider', {
'device_id': 0,
'trt_max_workspace_size': 4 * 1024 * 1024 * 1024, # 4GB
'trt_fp16_enable': use_fp16,
'trt_int8_enable': use_int8,
'trt_engine_cache_enable': True,
'trt_engine_cache_path': './trt_cache/',
'trt_timing_cache_enable': True,
}),
('CUDAExecutionProvider', {
'device_id': 0,
'arena_extend_strategy': 'kNextPowerOfTwo',
'gpu_mem_limit': 2 * 1024 * 1024 * 1024, # 2GB limit
'cudnn_conv_algo_search': 'EXHAUSTIVE',
}),
'CPUExecutionProvider'
]
session_options = ort.SessionOptions()
session_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
return ort.InferenceSession(onnx_path, session_options, providers=providers)4. cuDNN optimization strategies
Convolution algorithm selection
cuDNN provides multiple convolution algorithms optimized for different scenarios. TensorRT automatically selects the optimal algorithm, but understanding the options helps with debugging:
import torch.backends.cudnn as cudnn
# Enable cuDNN auto-tuner for optimal kernel selection
cudnn.benchmark = True # Crucial for consistent input sizes
cudnn.deterministic = False # Allows non-deterministic algorithms for speed
# For variable input sizes, disable benchmark
# cudnn.benchmark = FalseFP16 arithmetic on Jetson
cuDNN 4+ supports FP16 arithmetic for convolutions. On Tegra chips (including all Jetson devices), FP16 delivers up to 2x performance compared to FP32 with minimal accuracy loss.
Research comparing cuDNN and TensorRT runtimes on Jetson AGX Orin shows that while TensorRT introduces initial optimization overhead, it significantly outperforms cuDNN for sustained inference. For ResNet50, processing time drops from 8-9ms per image to 2-3ms depending on precision.
5. Memory optimization for limited VRAM
Jetson unified memory architecture
Unlike desktop GPUs with dedicated VRAM, Jetson devices use unified memory shared between CPU and GPU. This architecture enables zero-copy memory transfers but requires careful management.
import numpy as np
import pycuda.driver as cuda
import pycuda.autoinit
class UnifiedMemoryManager:
"""Manage unified memory for efficient Jetson inference."""
@staticmethod
def allocate_managed(shape, dtype=np.float32):
"""Allocate CUDA managed memory accessible by both CPU and GPU."""
size = int(np.prod(shape) * np.dtype(dtype).itemsize)
# Use managed memory for automatic migration
mem = cuda.managed_empty(shape, dtype=dtype, mem_flags=cuda.mem_attach_flags.GLOBAL)
return mem
@staticmethod
def allocate_pinned(shape, dtype=np.float32):
"""Allocate pinned host memory for faster transfers."""
return cuda.pagelocked_empty(shape, dtype=dtype)Zero-copy memory programming
Zero-copy avoids explicit memory transfers between CPU and GPU:
import ctypes
import numpy as np
def create_zero_copy_buffer(size_bytes):
"""Create zero-copy buffer for CPU-GPU shared access."""
import pycuda.driver as cuda
# Allocate host-mapped memory
host_ptr = cuda.pagelocked_empty(size_bytes, dtype=np.uint8)
device_ptr = np.intp(host_ptr.ctypes.data)
return host_ptr, device_ptrImportant: Zero-copy introduces higher latency per access since GPU reads traverse the memory bus. For small, frequently accessed data, use explicit GPU memory. For large buffers with sequential access patterns, unified memory with prefetching is optimal.
Memory-efficient inference pipeline
import tensorrt as trt
import pycuda.driver as cuda
class JetsonInferencePipeline:
"""Memory-optimized inference pipeline for Jetson."""
def __init__(self, engine_path, max_batch_size=8):
self.logger = trt.Logger(trt.Logger.WARNING)
# Load engine
with open(engine_path, 'rb') as f:
runtime = trt.Runtime(self.logger)
self.engine = runtime.deserialize_cuda_engine(f.read())
self.context = self.engine.create_execution_context()
# Allocate buffers once
self.inputs = []
self.outputs = []
self.bindings = []
self.stream = cuda.Stream()
for i in range(self.engine.num_io_tensors):
name = self.engine.get_tensor_name(i)
dtype = trt.nptype(self.engine.get_tensor_dtype(name))
shape = self.engine.get_tensor_shape(name)
# Replace -1 (dynamic) with max batch size
shape = [max_batch_size if s == -1 else s for s in shape]
size = int(np.prod(shape))
# Allocate page-locked host memory and device memory
host_mem = cuda.pagelocked_empty(size, dtype)
device_mem = cuda.mem_alloc(host_mem.nbytes)
self.bindings.append(int(device_mem))
if self.engine.get_tensor_mode(name) == trt.TensorIOMode.INPUT:
self.inputs.append({'host': host_mem, 'device': device_mem, 'name': name})
else:
self.outputs.append({'host': host_mem, 'device': device_mem, 'name': name})
def infer(self, input_data):
"""Run inference with minimal memory copies."""
# Copy input to page-locked memory
np.copyto(self.inputs[0]['host'], input_data.ravel())
# Transfer to GPU asynchronously
cuda.memcpy_htod_async(
self.inputs[0]['device'],
self.inputs[0]['host'],
self.stream
)
# Set tensor addresses
for inp in self.inputs:
self.context.set_tensor_address(inp['name'], inp['device'])
for out in self.outputs:
self.context.set_tensor_address(out['name'], out['device'])
# Execute inference
self.context.execute_async_v3(stream_handle=self.stream.handle)
# Transfer output back
cuda.memcpy_dtoh_async(
self.outputs[0]['host'],
self.outputs[0]['device'],
self.stream
)
self.stream.synchronize()
return self.outputs[0]['host'].copy()Swap space configuration
For memory-constrained Jetson Nano (4GB), adding swap enables larger model loading:
# Create 8GB swap file
sudo fallocate -l 8G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# Make persistent
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstabNote: Swap cannot extend GPU memory. It only helps with CPU memory pressure during model loading and preprocessing.
6. Batch size optimization for real-time inference
Tensor Core utilization
To maximize Tensor Core utilization on Jetson Orin:
- FP16: Batch sizes should be multiples of 8
- INT8: Batch sizes should be multiples of 16
- General rule: Multiples of 32 provide optimal performance
def optimize_batch_size(model_memory_mb, available_memory_mb, input_size):
"""Calculate optimal batch size for Jetson device."""
# Reserve memory for TensorRT workspace and system
reserved_mb = 512
usable_memory = available_memory_mb - reserved_mb - model_memory_mb
# Calculate per-sample memory (input + activations estimate)
sample_memory_mb = (
np.prod(input_size) * 4 / (1024 * 1024) + # FP32 input
np.prod(input_size) * 2 # Approximate activation memory
)
max_batch = int(usable_memory / sample_memory_mb)
# Round down to nearest multiple of 8 for Tensor Core efficiency
optimal_batch = (max_batch // 8) * 8
return max(1, optimal_batch)
# Example for Jetson Orin Nano (8GB)
batch_size = optimize_batch_size(
model_memory_mb=200,
available_memory_mb=8192,
input_size=(3, 640, 640)
)
print(f"Optimal batch size: {batch_size}")Throughput vs. latency trade-offs
| Batch Size | Latency | Throughput | Use Case |
|---|---|---|---|
| 1 | Lowest | Lowest | Real-time robotics, safety-critical |
| 4-8 | Medium | Good | Video analytics, balanced applications |
| 16-32 | Higher | Maximum | Batch processing, non-real-time |
Research shows that batched inference achieves 2.4x throughput improvement on devices like Jetson Xavier NX compared to single-sample inference.
7. Dynamic vs. static batching
Static batching
Static batching fixes the batch size at engine build time, which lets TensorRT specialize every kernel:
# Build static batch engine
trtexec --onnx=model.onnx --saveEngine=model_static.engine \
--explicitBatch --shapes=input:16x3x224x224Advantages: Minimal latency variance, fully optimized kernels Disadvantages: Inflexible, padding waste for partial batches
Dynamic batching with Triton Inference Server
For production deployments, NVIDIA Triton Inference Server provides server-side dynamic batching:
# config.pbtxt for Triton
name: "yolov8"
platform: "tensorrt_plan"
max_batch_size: 32
dynamic_batching {
preferred_batch_size: [ 4, 8, 16 ]
max_queue_delay_microseconds: 100
}
instance_group [
{
count: 1
kind: KIND_GPU
}
]Triton on Jetson supports concurrent model execution, dynamic batching and DLA offloading. The dynamic batcher combines incoming requests into batches up to the queue delay you set, so throughput rises without breaking the latency target.
8. Model architecture optimization
MobileNet family
MobileNet architectures use depthwise separable convolutions that are highly efficient on Jetson:
import torch
import torch.nn as nn
class OptimizedMobileNetV3Block(nn.Module):
"""Jetson-optimized MobileNetV3 block with SE attention."""
def __init__(self, in_channels, out_channels, kernel_size=3,
stride=1, expand_ratio=4, se_ratio=0.25):
super().__init__()
hidden_dim = int(in_channels * expand_ratio)
layers = []
# Expansion
if expand_ratio != 1:
layers.extend([
nn.Conv2d(in_channels, hidden_dim, 1, bias=False),
nn.BatchNorm2d(hidden_dim),
nn.Hardswish(inplace=True) # Jetson-friendly activation
])
# Depthwise convolution
layers.extend([
nn.Conv2d(hidden_dim, hidden_dim, kernel_size, stride,
kernel_size // 2, groups=hidden_dim, bias=False),
nn.BatchNorm2d(hidden_dim),
nn.Hardswish(inplace=True)
])
# Squeeze-and-Excitation
if se_ratio:
se_channels = max(1, int(in_channels * se_ratio))
layers.append(SEModule(hidden_dim, se_channels))
# Projection
layers.extend([
nn.Conv2d(hidden_dim, out_channels, 1, bias=False),
nn.BatchNorm2d(out_channels)
])
self.block = nn.Sequential(*layers)
self.use_residual = stride == 1 and in_channels == out_channels
def forward(self, x):
if self.use_residual:
return x + self.block(x)
return self.block(x)EfficientNet scaling
EfficientNet provides the best accuracy-efficiency trade-off through compound scaling of depth, width, and resolution:
| Model | Resolution | Params | FLOPs | Top-1 Acc | Jetson Nano FPS |
|---|---|---|---|---|---|
| EfficientNet-B0 | 224 | 5.3M | 0.39B | 77.1% | 45 |
| EfficientNet-B1 | 240 | 7.8M | 0.70B | 79.1% | 28 |
| EfficientNet-B2 | 260 | 9.2M | 1.0B | 80.1% | 18 |
YOLO variants for edge
For real-time object detection on Jetson, YOLO variants offer the best speed-accuracy trade-off:
# YOLOv8 Nano optimized for Jetson
from ultralytics import YOLO
# Load and export with Jetson-optimized settings
model = YOLO('yolov8n.pt')
# Export to TensorRT with INT8
model.export(
format='engine',
device=0,
half=True, # FP16
int8=True, # INT8 quantization
data='coco128.yaml', # Calibration data
workspace=4, # GB
batch=8,
dynamic=True
)YOLOv8 Performance on Jetson Orin Nano:
- YOLOv8n FP16: ~7.2ms latency (139 FPS)
- YOLOv8n INT8: ~3.2ms latency (313 FPS)
The hybrid MOLO architecture (MobileNetV2 backbone + YOLOv8 head) offers smaller size than YOLOv8-s with better accuracy than YOLOv8-n.
9. Jetson-specific optimization flags and settings
nvpmodel power modes
Jetson devices support multiple power modes via nvpmodel. The optimal mode depends on your thermal and power constraints.
# Check current power mode
sudo nvpmodel -q
# Set MAXN mode for maximum performance
sudo nvpmodel -m 0
# Set 15W mode for power-constrained deployments
sudo nvpmodel -m 2
# JetPack 6.2+ MAXN SUPER mode (Orin Nano/NX)
# Requires flashing with jetson-orin-nano-devkit-super configuration
sudo nvpmodel -m 0 # MAXN_SUPER when availablejetson_clocks for maximum performance
# Lock clocks to maximum frequency
sudo jetson_clocks
# Store current settings
sudo jetson_clocks --store
# Restore previous settings
sudo jetson_clocks --restore
# Show current status
sudo jetson_clocks --showImportant: After running jetson_clocks, power mode cannot be changed without a reboot.
DLA (Deep Learning Accelerator) configuration
The DLA takes inference off the GPU so both can run at once:
# Build engine with DLA support
trtexec --onnx=model.onnx \
--saveEngine=model_dla.engine \
--useDLACore=0 \
--allowGPUFallback \
--int8 \
--fp16
# Use both DLA cores for maximum throughput
trtexec --onnx=model.onnx \
--saveEngine=model_dla0.engine --useDLACore=0 --allowGPUFallback &
trtexec --onnx=model.onnx \
--saveEngine=model_dla1.engine --useDLACore=1 --allowGPUFallback &DLA delivers approximately 2.5x better power efficiency compared to GPU inference.
DLA-Supported Layers:
- Convolution, Deconvolution
- Fully Connected
- Activation (ReLU, Sigmoid, Tanh, Clipped ReLU, LeakyReLU)
- Pooling (Max, Average)
- Batch Normalization
- Element-wise operations
- Softmax, LRN
Unsupported: GroupNorm, certain custom layers (require GPU fallback)
Environment variables for optimization
# CUDA settings for Jetson
export CUDA_DEVICE_ORDER=PCI_BUS_ID
export CUDA_VISIBLE_DEVICES=0
# TensorRT settings
export TRT_LOGGER_LEVEL=WARNING
# cuDNN settings
export CUDNN_LOGINFO_DBG=0
# Memory settings
export PYTORCH_CUDA_ALLOC_CONF=max_split_size_mb:512
# For debugging memory issues
export CUDA_LAUNCH_BLOCKING=110. Complete optimization workflow
The full workflow from PyTorch model to production Jetson deployment:
#!/usr/bin/env python3
"""
Complete Neural Network Optimization Pipeline for NVIDIA Jetson
"""
import torch
import onnx
from onnxsim import simplify
import tensorrt as trt
import numpy as np
import subprocess
import os
class JetsonOptimizationPipeline:
"""End-to-end optimization pipeline for Jetson deployment."""
def __init__(self, model, input_shape, output_dir="./optimized"):
self.model = model
self.input_shape = input_shape # (batch, channels, height, width)
self.output_dir = output_dir
os.makedirs(output_dir, exist_ok=True)
def step1_export_onnx(self, opset_version=17):
"""Export PyTorch model to ONNX format."""
print("[Step 1] Exporting to ONNX...")
self.model.eval()
dummy_input = torch.randn(self.input_shape)
onnx_path = os.path.join(self.output_dir, "model.onnx")
torch.onnx.export(
self.model,
dummy_input,
onnx_path,
opset_version=opset_version,
input_names=['input'],
output_names=['output'],
dynamic_axes={
'input': {0: 'batch'},
'output': {0: 'batch'}
}
)
# Simplify ONNX
model_onnx = onnx.load(onnx_path)
model_simplified, check = simplify(model_onnx)
if check:
onnx.save(model_simplified, onnx_path)
print(f" Saved: {onnx_path}")
return onnx_path
def step2_build_tensorrt_engine(self, onnx_path, precision="fp16",
calibrator=None, use_dla=False):
"""Build TensorRT engine with specified precision."""
print(f"[Step 2] Building TensorRT engine ({precision})...")
engine_path = os.path.join(
self.output_dir,
f"model_{precision}{'_dla' if use_dla else ''}.engine"
)
cmd = [
"/usr/src/tensorrt/bin/trtexec",
f"--onnx={onnx_path}",
f"--saveEngine={engine_path}",
"--workspace=4096",
f"--minShapes=input:1x{self.input_shape[1]}x{self.input_shape[2]}x{self.input_shape[3]}",
f"--optShapes=input:8x{self.input_shape[1]}x{self.input_shape[2]}x{self.input_shape[3]}",
f"--maxShapes=input:32x{self.input_shape[1]}x{self.input_shape[2]}x{self.input_shape[3]}",
]
if precision == "fp16":
cmd.append("--fp16")
elif precision == "int8":
cmd.extend(["--int8", "--fp16"])
if calibrator:
cmd.append(f"--calib={calibrator}")
elif precision == "best":
cmd.append("--best")
if use_dla:
cmd.extend(["--useDLACore=0", "--allowGPUFallback"])
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error: {result.stderr}")
return None
print(f" Saved: {engine_path}")
return engine_path
def step3_benchmark(self, engine_path, iterations=100):
"""Benchmark the TensorRT engine."""
print(f"[Step 3] Benchmarking engine...")
cmd = [
"/usr/src/tensorrt/bin/trtexec",
f"--loadEngine={engine_path}",
f"--iterations={iterations}",
"--warmUp=1000",
"--duration=0"
]
result = subprocess.run(cmd, capture_output=True, text=True)
# Parse results
for line in result.stdout.split('\n'):
if 'mean' in line.lower() or 'throughput' in line.lower():
print(f" {line.strip()}")
return result.stdout
def run_full_pipeline(self, precision="fp16", use_dla=False):
"""Execute complete optimization pipeline."""
print("=" * 60)
print("JETSON NEURAL NETWORK OPTIMIZATION PIPELINE")
print("=" * 60)
onnx_path = self.step1_export_onnx()
engine_path = self.step2_build_tensorrt_engine(
onnx_path, precision, use_dla=use_dla
)
if engine_path:
self.step3_benchmark(engine_path)
print("=" * 60)
print("Pipeline complete!")
return engine_path
# Usage example
if __name__ == "__main__":
import torchvision.models as models
# Load model
model = models.mobilenet_v3_small(pretrained=True)
# Run optimization pipeline
pipeline = JetsonOptimizationPipeline(
model=model,
input_shape=(1, 3, 224, 224),
output_dir="./jetson_optimized"
)
# Build FP16 engine
pipeline.run_full_pipeline(precision="fp16")
# Build INT8 engine with DLA
pipeline.run_full_pipeline(precision="int8", use_dla=True)The order to apply this in
Architecture selection, quantization, pruning and platform tuning each move a different bottleneck. Apply them in this order:
Start with the right architecture: MobileNet, EfficientNet and YOLO-Nano variants are designed for edge efficiency.
Quantize aggressively: FP16 offers 2x speedup with minimal accuracy loss. INT8 can achieve 4x speedup with proper calibration.
Use the accelerators you have: run CNN inference on the DLA to free the GPU for other tasks, at 2.5x better power efficiency.
Optimize batch sizes: use multiples of 32 for Tensor Core utilization, and balance latency against throughput for your use case.
Profile extensively: use
trtexec, Nsight Systems and tegrastats to find the bottleneck.Configure power modes: set nvpmodel and jetson_clocks for your thermal and power budget.
Production deployments reach real-time performance (30+ FPS) for complex vision models on the entry-level Jetson Nano, and hundreds of FPS on Jetson AGX Orin.
Sources
- NVIDIA TensorRT Documentation - Working with Quantized Types
- NVIDIA TensorRT Best Practices
- NVIDIA Developer Blog - Post-Training Quantization
- NVIDIA Jetson AI Lab - Knowledge Distillation Tutorial
- GitHub: NVIDIA-AI-IOT/jetson-intro-to-distillation
- GitHub: NVIDIA/Model-Optimizer
- NVIDIA Developer Blog - Pruning and Distilling LLMs
- NVIDIA TensorRT Quick Start Guide
- ONNX Runtime TensorRT Execution Provider
- NVIDIA Developer Blog - End-to-End AI with ONNX Runtime
- NVIDIA Developer Forums - Jetson Orin FP16/INT8 Performance
- NVIDIA Triton Inference Server - Dynamic Batching on Jetson
- NVIDIA Developer Blog - Getting Started with DLA on Jetson Orin
- GitHub: NVIDIA-AI-IOT/jetson_dla_tutorial
- NVIDIA Jetson Linux Developer Guide - Power and Performance
- NVIDIA Developer Blog - JetPack 6.2 Super Mode
- arXiv: Benchmarking Deep Learning Models on NVIDIA Jetson Nano
- ACM: TensorRT-Based Framework for Deep Learning Inference on Jetson
- Torch-TensorRT PTQ Documentation
- PyTorch ONNX Export Documentation
- Seeed Studio: YOLOv8 Performance Benchmarks on Jetson
- KeyValue Systems: TensorRT Video Analytics Optimization