diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 19202cba316..db6c2d4a479 100755 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,7 @@ Changelog *Quantization* - Add an end-to-end PETRv1 and PETRv2 ONNX PTQ example with nuScenes calibration and accuracy evaluation, INT8 and FP8 backbone quantization, FP16 heads, and TensorRT engine benchmarking. +- Add an end-to-end BEVFormer ONNX PTQ example with temporal calibration data generation, INT8 and FP8 quantization, TensorRT engine building, and nuScenes accuracy evaluation. See `examples/onnx_ptq/bevformer/README.md `_ for details. - Add the ``nvfp4_act_headroom`` calibration algorithm for NVFP4 **activation** global scales. Instead of setting the global scale from the largest per-block amax seen during calibration (plain ``max``, which leaves no room above it so any larger activation saturates), it anchors the scale to a low percentile of the per-block amax distribution, leaving the rest of the FP8 block-scale range as headroom: ``amax = max(rho * anchor, upper)``, where ``anchor`` and ``upper`` are the per-block amaxes at ``anchor_percentile`` (default 1) and ``upper_percentile`` (default 99.99; set to 100 to never clip calibration data), and ``rho`` (default 16384) is the headroom factor. Applies only to NVFP4 dynamic-block input quantizers; ``SequentialQuantizer`` activation quantizers raise. Weight scales are an orthogonal axis selected by a nested ``weight_scale_algorithm`` (``max`` by default, or ``mse`` / ``local_hessian``), so one recipe can combine a weight calibration with this activation policy in a single pass. Ships ``modelopt_recipes/general/ptq/nvfp4_act_headroom-kv_fp8_cast.yaml``, which mirrors ``nvfp4_default-kv_fp8_cast`` with only the calibration algorithm swapped and exports a standard NVFP4 checkpoint. *Megatron Framework (M-LM / M-Bridge)* diff --git a/examples/onnx_ptq/README.md b/examples/onnx_ptq/README.md index bfabb023d45..905f73ac2a5 100644 --- a/examples/onnx_ptq/README.md +++ b/examples/onnx_ptq/README.md @@ -133,6 +133,10 @@ Inference latency of the model is ms The [FAR3D example](./far3d/) demonstrates an end-to-end workflow that exports and quantizes the FAR3D ONNX image encoder, builds TensorRT engines, and evaluates 3D object detection mAP on the Argoverse 2 validation set. +### BEVFormer 3D object detection + +The [BEVFormer example](./bevformer/) exports BEVFormer-tiny to ONNX, generates temporal calibration data, quantizes the model to INT8 or FP8, builds TensorRT engines, and evaluates NDS and mAP on the nuScenes validation set. + ### PETR 3D object detection The [PETR example](./petr/) demonstrates an end-to-end workflow that exports and quantizes PETRv1 and PETRv2 ONNX backbones and heads, builds TensorRT engines, and evaluates 3D object detection mAP on the nuScenes validation set. diff --git a/examples/onnx_ptq/bevformer/Dockerfile b/examples/onnx_ptq/bevformer/Dockerfile new file mode 100644 index 00000000000..35a462166f9 --- /dev/null +++ b/examples/onnx_ptq/bevformer/Dockerfile @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM nvcr.io/nvidia/pytorch:26.01-py3 + +ARG NUM_JOBS=8 +ARG DL4AGX_COMMIT=9f7b29104c253d5bc68334e7b83b3eecb72d4572 +ARG BEVFORMER_TRT_COMMIT=303d3140c14016047c07f9db73312af364f0dd7c +ARG TENSORRT_COMMIT=3b4ddc1d45f11167f2ea53dc6046bba8df800e2d +ARG MMCV_COMMIT=235c0253ab8806a2a2ee6954b4258a95358497ac +ARG MMDET_COMMIT=3b72b12fe9b14de906d1363982b9fba05e7d47c1 + +ENV FORCE_CUDA=1 +ENV TORCH_CUDA_ARCH_LIST="7.5;8.0;8.6;8.9+PTX" +ENV TRT_LIBPATH=/usr/lib/x86_64-linux-gnu +ENV LD_LIBRARY_PATH=/usr/local/cuda/compat/lib:/usr/local/nvidia/lib:/usr/local/nvidia/lib64:/usr/lib/x86_64-linux-gnu:/usr/local/cuda-13.1/targets/x86_64-linux/lib:${LD_LIBRARY_PATH} + +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + libgl1 \ + libglib2.0-0 && \ + rm -rf /var/lib/apt/lists/* + +COPY . /opt/Model-Optimizer +COPY examples/onnx_ptq/bevformer/requirements.txt /tmp/bevformer-requirements.txt +# Stable ONNX Runtime wheels do not support the base image's CUDA 13 runtime. +RUN env -u PIP_CONSTRAINT python -m pip install --no-cache-dir \ + --upgrade pip setuptools wheel && \ + env -u PIP_CONSTRAINT python -m pip install --no-cache-dir cuda-python==13.1.0 && \ + env -u PIP_CONSTRAINT python -m pip install --no-cache-dir \ + -e "/opt/Model-Optimizer[onnx]" && \ + env -u PIP_CONSTRAINT python -m pip install --no-cache-dir --pre \ + --index-url https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/ort-cuda-13-nightly/pypi/simple/ \ + --force-reinstall --no-deps \ + onnxruntime-gpu==1.24.0.dev20260123002 && \ + env -u PIP_CONSTRAINT python -m pip install --no-cache-dir \ + -r /tmp/bevformer-requirements.txt + +RUN git clone https://github.com/NVIDIA/DL4AGX.git /workspace/DL4AGX && \ + git -C /workspace/DL4AGX checkout ${DL4AGX_COMMIT} && \ + git clone https://github.com/DerryHub/BEVFormer_tensorrt.git \ + /workspace/BEVFormer_tensorrt && \ + git -C /workspace/BEVFormer_tensorrt checkout ${BEVFORMER_TRT_COMMIT} && \ + git -C /workspace/BEVFormer_tensorrt apply \ + /workspace/DL4AGX/AV-Solutions/bevformer-int8-eq/bevformer_trt10.patch + +RUN git clone https://github.com/NVIDIA/TensorRT.git /workspace/TensorRT && \ + git -C /workspace/TensorRT checkout ${TENSORRT_COMMIT} && \ + cd /workspace/TensorRT/tools/pytorch-quantization && \ + MAX_JOBS=${NUM_JOBS} env -u PIP_CONSTRAINT python setup.py install + +RUN git clone https://github.com/open-mmlab/mmcv.git \ + /workspace/BEVFormer_tensorrt/third_party/mmcv && \ + git -C /workspace/BEVFormer_tensorrt/third_party/mmcv checkout ${MMCV_COMMIT} && \ + sed -i 's/c++14/c++17/g' \ + /workspace/BEVFormer_tensorrt/third_party/mmcv/setup.py && \ + env -u PIP_CONSTRAINT python -m pip install --no-cache-dir \ + -r /workspace/BEVFormer_tensorrt/third_party/mmcv/requirements/optional.txt && \ + cd /workspace/BEVFormer_tensorrt/third_party/mmcv && \ + MAX_JOBS=${NUM_JOBS} MMCV_WITH_OPS=1 \ + env -u PIP_CONSTRAINT python -m pip install --no-cache-dir -v -e . \ + --no-build-isolation + +RUN git clone https://github.com/open-mmlab/mmdetection.git \ + /workspace/BEVFormer_tensorrt/third_party/mmdetection && \ + git -C /workspace/BEVFormer_tensorrt/third_party/mmdetection checkout ${MMDET_COMMIT} && \ + env -u PIP_CONSTRAINT python -m pip install --no-cache-dir -v \ + /workspace/BEVFormer_tensorrt/third_party/mmdetection --no-build-isolation && \ + cd /workspace/BEVFormer_tensorrt/third_party/bev_mmdet3d && \ + MAX_JOBS=${NUM_JOBS} env -u PIP_CONSTRAINT \ + python -m pip install --no-cache-dir -v -e . \ + --no-build-isolation + +WORKDIR /workspace/BEVFormer_tensorrt diff --git a/examples/onnx_ptq/bevformer/README.md b/examples/onnx_ptq/bevformer/README.md new file mode 100644 index 00000000000..ba4bfa85399 --- /dev/null +++ b/examples/onnx_ptq/bevformer/README.md @@ -0,0 +1,208 @@ +# BEVFormer ONNX PTQ and nuScenes evaluation + +This example exports BEVFormer-tiny to ONNX, prepares temporal calibration data, quantizes the model to INT8 or FP8 with Model Optimizer, builds TensorRT engines, and evaluates NDS and mAP on the nuScenes validation set. It extends the [NVIDIA DL4AGX BEVFormer INT8 workflow](https://github.com/NVIDIA/DL4AGX/tree/9f7b29104c253d5bc68334e7b83b3eecb72d4572/AV-Solutions/bevformer-int8-eq) with FP8 and reuses the calibration reader shared by the Model Optimizer FAR3D and PETR examples. + +The container pins [BEVFormer_tensorrt](https://github.com/DerryHub/BEVFormer_tensorrt/tree/303d3140c14016047c07f9db73312af364f0dd7c) and applies the TensorRT 10 compatibility patch from DL4AGX. The exported model uses custom TensorRT plugins, so export, calibration, engine building, and evaluation must use the plugin library built in the container. + +## 1. Build and start the container + +Build from the Model Optimizer repository root: + +```bash +docker build \ + -f examples/onnx_ptq/bevformer/Dockerfile \ + -t bevformer-modelopt . +``` + +Download the nuScenes v1.0 trainval set and CAN bus expansion data. Use the dataset, CAN bus data, and checkpoint only under their upstream terms, including the [nuScenes terms of use](https://www.nuscenes.org/terms-of-use). Start the container with the dataset and an artifact directory mounted: + +```bash +mkdir -p bevformer_artifacts +docker run --rm -it --gpus=all --network=host --shm-size=20g \ + -v /path/to/nuscenes:/workspace/BEVFormer_tensorrt/data/nuscenes \ + -v /path/to/can_bus:/workspace/BEVFormer_tensorrt/data/can_bus \ + -v "$(pwd)/bevformer_artifacts:/artifacts" \ + bevformer-modelopt +``` + +The remaining commands run inside the container: + +```bash +export BEVFORMER_ROOT=/workspace/BEVFormer_tensorrt +export DL4AGX_ROOT=/workspace/DL4AGX +export MODELOPT_ROOT=/opt/Model-Optimizer +export PLUGIN_PATH=${BEVFORMER_ROOT}/TensorRT/lib/libtensorrt_ops.so +export CONFIG=${BEVFORMER_ROOT}/configs/bevformer/plugin/bevformer_tiny_trt_p2.py +cd ${BEVFORMER_ROOT} +``` + +Build the custom plugins inside the GPU-enabled container. The build targets the compute capability of its active GPU: + +```bash +cmake -S ${BEVFORMER_ROOT}/TensorRT \ + -B ${BEVFORMER_ROOT}/TensorRT/build \ + -DCMAKE_TENSORRT_PATH=/usr +cmake --build ${BEVFORMER_ROOT}/TensorRT/build --parallel +cmake --install ${BEVFORMER_ROOT}/TensorRT/build +``` + +Generate the temporal train and validation metadata required by BEVFormer: + +```bash +bash samples/bevformer/create_data.sh +``` + +This creates the following files in the mounted nuScenes directory: + +```text +nuscenes/ +├── nuscenes_infos_temporal_train.pkl +└── nuscenes_infos_temporal_val.pkl +``` + +## 2. Export BEVFormer to ONNX + +Download the published BEVFormer-tiny checkpoint: + +```bash +wget --continue \ + -O /artifacts/bevformer_tiny_epoch_24.pth \ + https://github.com/zhiqi-li/storage/releases/download/v1.0/bevformer_tiny_epoch_24.pth + +echo "7305046dbaa4fe8b1fa6d6acb9e0e3d605a70a3c473f763e936103428d2b2f12 /artifacts/bevformer_tiny_epoch_24.pth" | \ + sha256sum --check +``` + +Export the `nv_half2` plugin variant at opset 13 and copy it to the artifact directory: + +```bash +python tools/pth2onnx.py \ + ${CONFIG} \ + /artifacts/bevformer_tiny_epoch_24.pth \ + --opset_version=13 \ + --cuda \ + --flag=cp2_op13 + +cp checkpoints/onnx/bevformer_tiny_epoch_24_cp2_op13.onnx /artifacts/ +export ONNX_PATH=/artifacts/bevformer_tiny_epoch_24_cp2_op13.onnx +``` + +Post-process a copy for ONNX Runtime. This copy is used only while generating calibration data; quantization uses the original export: + +```bash +python ${DL4AGX_ROOT}/AV-Solutions/bevformer-int8-eq/tools/onnx_postprocess.py \ + --onnx=${ONNX_PATH} \ + --trt_plugins=${PLUGIN_PATH} + +export CALIBRATION_ONNX=/artifacts/bevformer_tiny_epoch_24_cp2_op13_post.onnx +``` + +## 3. Prepare calibration data + +Generate 600 temporal samples from the nuScenes training split: + +```bash +PYTHONPATH=${BEVFORMER_ROOT} \ +python ${MODELOPT_ROOT}/examples/onnx_ptq/bevformer/prepare_calibration.py \ + ${CONFIG} \ + --onnx=${CALIBRATION_ONNX} \ + --trt-plugin=${PLUGIN_PATH} \ + --output-dir=/artifacts/calibration \ + --num-samples=600 +``` + +The script runs the post-processed model with ONNX Runtime and carries `prev_bev` across frames while resetting it between scenes. It saves one NPZ file per sample instead of building one large archive in memory. The output directory must be empty. + +## 4. Quantize to INT8 and FP8 + +INT8 uses entropy calibration by default. All `MatMul` nodes remain in FP16, matching the DL4AGX recommendation: + +```bash +python ${MODELOPT_ROOT}/examples/onnx_ptq/bevformer/quantize.py \ + --onnx=${ONNX_PATH} \ + --calibration-dir=/artifacts/calibration \ + --trt-plugins=${PLUGIN_PATH} \ + --quantization-mode=int8 \ + --output=/artifacts/bevformer_tiny_epoch_24_cp2_op13.int8.onnx +``` + +FP8 uses max calibration by default and requires a GPU with compute capability 8.9 or later: + +```bash +python ${MODELOPT_ROOT}/examples/onnx_ptq/bevformer/quantize.py \ + --onnx=${ONNX_PATH} \ + --calibration-dir=/artifacts/calibration \ + --trt-plugins=${PLUGIN_PATH} \ + --quantization-mode=fp8 \ + --output=/artifacts/bevformer_tiny_epoch_24_cp2_op13.fp8.onnx +``` + +The custom TensorRT plugins and all `MatMul` nodes remain at higher precision. Model Optimizer automatically upgrades the FP8 model to the required ONNX opset. + +## 5. Build TensorRT engines + +Build the FP16 baseline from the original ONNX model: + +```bash +trtexec \ + --onnx=${ONNX_PATH} \ + --saveEngine=/artifacts/bevformer_tiny_epoch_24_cp2_op13.fp16.engine \ + --staticPlugins=${PLUGIN_PATH} \ + --fp16 \ + --skipInference +``` + +Build strongly typed engines from the explicitly quantized models: + +```bash +for precision in int8 fp8; do + trtexec \ + --onnx=/artifacts/bevformer_tiny_epoch_24_cp2_op13.${precision}.onnx \ + --saveEngine=/artifacts/bevformer_tiny_epoch_24_cp2_op13.${precision}.engine \ + --staticPlugins=${PLUGIN_PATH} \ + --stronglyTyped \ + --skipInference +done +``` + +Serialized TensorRT engines are specific to the TensorRT version and GPU architecture used to build them. + +## 6. Evaluate accuracy and latency + +Evaluate all 6,019 nuScenes validation samples: + +```bash +for precision in fp16 int8 fp8; do + python tools/bevformer/evaluate_trt.py \ + ${CONFIG} \ + /artifacts/bevformer_tiny_epoch_24_cp2_op13.${precision}.engine \ + --trt_plugins=${PLUGIN_PATH} | \ + tee /artifacts/evaluate_${precision}.log +done +``` + +Measure TensorRT GPU compute time independently of data loading and post-processing: + +```bash +for precision in fp16 int8 fp8; do + trtexec \ + --loadEngine=/artifacts/bevformer_tiny_epoch_24_cp2_op13.${precision}.engine \ + --staticPlugins=${PLUGIN_PATH} \ + --warmUp=1000 \ + --duration=10 \ + --iterations=100 | \ + tee /artifacts/trtexec_${precision}.log +done +``` + +## Results on nuScenes validation + +Measurements use an NVIDIA RTX 6000 Ada Generation GPU (compute capability 8.9), TensorRT 10.14.1.48, CUDA 13.1, and ONNX Runtime 1.24.0.dev20260123002. Quantization uses 600 training samples. NDS and mAP are produced after all 6,019 validation samples complete; GPU compute time is the median reported by the command above and excludes data loading and post-processing. + +The INT8 and FP8 rows are mixed-precision graphs. Quantized operators use the listed format, while the custom TensorRT plugins, excluded `MatMul` nodes, and other unsupported paths remain in FP16. External inputs and outputs remain FP32. + +| Precision | TensorRT GPU compute time (median, ms) | NDS | mAP | +| --- | ---: | ---: | ---: | +| FP16 | 4.597 | 0.3546 | 0.2515 | +| INT8/FP16 | 3.180 | 0.3512 | 0.2505 | +| FP8/FP16 | 4.106 | 0.3526 | 0.2489 | diff --git a/examples/onnx_ptq/bevformer/prepare_calibration.py b/examples/onnx_ptq/bevformer/prepare_calibration.py new file mode 100644 index 00000000000..24806d67ae5 --- /dev/null +++ b/examples/onnx_ptq/bevformer/prepare_calibration.py @@ -0,0 +1,136 @@ +# Adapted from https://github.com/NVIDIA/DL4AGX/blob/9f7b29104c253d5bc68334e7b83b3eecb72d4572/AV-Solutions/bevformer-int8-eq/tools/calib_data_prep.py. +# +# SPDX-FileCopyrightText: Copyright (c) 2024, 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import copy +from pathlib import Path + +import numpy as np +import onnxruntime as ort + + +def parse_args(): + parser = argparse.ArgumentParser(description="Prepare BEVFormer calibration batches") + parser.add_argument("config", help="Path to the BEVFormer TensorRT configuration") + parser.add_argument("--onnx", required=True, type=Path, help="Post-processed ONNX model") + parser.add_argument("--trt-plugin", required=True, type=Path) + parser.add_argument("--output-dir", required=True, type=Path) + parser.add_argument("--num-samples", type=int, default=600) + parser.add_argument("--workers", type=int, default=6) + return parser.parse_args() + + +def create_session(onnx_path, trt_plugin): + if "TensorrtExecutionProvider" not in ort.get_available_providers(): + raise RuntimeError("ONNX Runtime TensorRTExecutionProvider is unavailable") + options = ort.SessionOptions() + options.log_severity_level = 1 + providers = [ + ( + "TensorrtExecutionProvider", + {"device_id": 0, "trt_extra_plugin_lib_paths": str(trt_plugin)}, + ), + ("CUDAExecutionProvider", {"device_id": 0}), + "CPUExecutionProvider", + ] + return ort.InferenceSession(str(onnx_path), sess_options=options, providers=providers) + + +def build_inputs(data, prev_bev, previous_frame): + image = data["img"][0].data[0].numpy().astype(np.float32, copy=False) + metadata = data["img_metas"][0].data[0][0] + use_prev_bev = np.array( + [metadata["scene_token"] == previous_frame["scene_token"]], dtype=np.float32 + ) + previous_frame["scene_token"] = metadata["scene_token"] + position = copy.deepcopy(metadata["can_bus"][:3]) + angle = copy.deepcopy(metadata["can_bus"][-1]) + if use_prev_bev[0]: + metadata["can_bus"][:3] -= previous_frame["position"] + metadata["can_bus"][-1] -= previous_frame["angle"] + else: + metadata["can_bus"][:3] = 0 + metadata["can_bus"][-1] = 0 + + inputs = { + "image": image, + "prev_bev": prev_bev, + "use_prev_bev": use_prev_bev, + "can_bus": metadata["can_bus"].astype(np.float32), + "lidar2img": np.expand_dims(np.stack(metadata["lidar2img"], axis=0), axis=0).astype( + np.float32 + ), + } + previous_frame["position"] = position + previous_frame["angle"] = angle + return inputs + + +def main(): + from mmcv import Config + from third_party.bev_mmdet3d.datasets.builder import build_dataloader, build_dataset + + args = parse_args() + if args.num_samples < 1 or args.workers < 0: + raise ValueError("Sample count must be positive and workers must be non-negative") + if not args.onnx.is_file(): + raise FileNotFoundError(args.onnx) + if not args.trt_plugin.is_file(): + raise FileNotFoundError(args.trt_plugin) + args.output_dir.mkdir(parents=True, exist_ok=True) + if any(args.output_dir.iterdir()): + raise FileExistsError(f"{args.output_dir} must be empty") + + config = Config.fromfile(args.config) + dataset = build_dataset(cfg=config.data.quant) + loader = build_dataloader( + dataset, + samples_per_gpu=1, + workers_per_gpu=args.workers, + shuffle=False, + dist=False, + ) + session = create_session(args.onnx, args.trt_plugin) + input_names = {value.name for value in session.get_inputs()} + if input_names != set(config.input_shapes): + raise ValueError( + f"Configuration inputs {sorted(config.input_shapes)} do not match ONNX inputs " + f"{sorted(input_names)}" + ) + output_names = [value.name for value in session.get_outputs()] + if "bev_embed" not in output_names: + raise ValueError("ONNX model has no bev_embed output") + + prev_bev = np.zeros((config.bev_h_ * config.bev_w_, 1, config._dim_), dtype=np.float32) + previous_frame = {"scene_token": None, "position": 0, "angle": 0} + saved = 0 + for data in loader: + inputs = build_inputs(data, prev_bev, previous_frame) + outputs = dict(zip(output_names, session.run(output_names, inputs), strict=True)) + np.savez(args.output_dir / f"batch_{saved:04d}.npz", **inputs) + prev_bev = outputs["bev_embed"] + saved += 1 + if saved == args.num_samples: + break + + if saved != args.num_samples: + raise RuntimeError(f"Prepared {saved} of {args.num_samples} requested samples") + print(f"Saved {saved} calibration batches to {args.output_dir}") + + +if __name__ == "__main__": + main() diff --git a/examples/onnx_ptq/bevformer/quantize.py b/examples/onnx_ptq/bevformer/quantize.py new file mode 100644 index 00000000000..bcf6664fc40 --- /dev/null +++ b/examples/onnx_ptq/bevformer/quantize.py @@ -0,0 +1,68 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +import sys +from pathlib import Path + +from modelopt.onnx.quantization import quantize + +sys.path.insert(0, str(Path(__file__).resolve().parents[3])) +from examples.onnx_ptq.quantization_utils import NpzCalibrationReader + + +def parse_args(): + parser = argparse.ArgumentParser(description="Quantize the BEVFormer ONNX model") + parser.add_argument("--onnx", required=True, type=Path) + parser.add_argument("--calibration-dir", required=True, type=Path) + parser.add_argument("--trt-plugins", required=True, nargs="+", type=Path) + parser.add_argument("--quantization-mode", choices=("int8", "fp8"), default="int8") + parser.add_argument("--calibration-method", choices=("entropy", "max")) + parser.add_argument("--max-batches", type=int, default=600) + parser.add_argument("--output", type=Path) + return parser.parse_args() + + +def main(): + args = parse_args() + if not args.onnx.is_file(): + raise FileNotFoundError(args.onnx) + for plugin in args.trt_plugins: + if not plugin.is_file(): + raise FileNotFoundError(plugin) + if args.output is None: + args.output = args.onnx.with_name(f"{args.onnx.stem}.{args.quantization_mode}.onnx") + + calibration_method = args.calibration_method or ( + "entropy" if args.quantization_mode == "int8" else "max" + ) + quantize( + onnx_path=str(args.onnx), + quantize_mode=args.quantization_mode, + calibration_data_reader=NpzCalibrationReader( + args.calibration_dir, args.onnx, max_batches=args.max_batches + ), + calibration_method=calibration_method, + calibration_eps=["trt", "cuda:0", "cpu"], + op_types_to_exclude=["MatMul"], + disable_mha_qdq=True, + trt_plugins=[str(plugin) for plugin in args.trt_plugins], + high_precision_dtype="fp16", + output_path=str(args.output), + ) + + +if __name__ == "__main__": + main() diff --git a/examples/onnx_ptq/bevformer/requirements.txt b/examples/onnx_ptq/bevformer/requirements.txt new file mode 100644 index 00000000000..66769088925 --- /dev/null +++ b/examples/onnx_ptq/bevformer/requirements.txt @@ -0,0 +1,16 @@ +matplotlib==3.10.8 +netron==9.2.1 +numba==0.63.1 +numpy==2.1.0 +nuscenes-devkit==1.1.9 +opencv-python==4.12.0.88 +pillow==12.1.0 +pycocotools==2.0.11 +pycuda==2026.1 +pyquaternion==0.9.9 +scipy==1.16.3 +shapely==2.1.2 +terminaltables==3.1.10 +thop==0.1.1.post2209072238 +tqdm==4.67.1 +trimesh==5.0.0 diff --git a/tests/unit/onnx/quantization/test_bevformer_example.py b/tests/unit/onnx/quantization/test_bevformer_example.py new file mode 100644 index 00000000000..fb555b83fa4 --- /dev/null +++ b/tests/unit/onnx/quantization/test_bevformer_example.py @@ -0,0 +1,75 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import copy + +import numpy as np + +from examples.onnx_ptq.bevformer.prepare_calibration import build_inputs + + +class DataContainer: + def __init__(self, data): + self.data = data + + +class NumpyTensor: + def __init__(self, array): + self.array = array + + def numpy(self): + return self.array + + +def make_data(scene_token, can_bus): + metadata = { + "scene_token": scene_token, + "can_bus": np.array(can_bus, dtype=np.float64), + "lidar2img": [np.eye(4, dtype=np.float64) for _ in range(6)], + } + image = np.ones((1, 6, 3, 2, 2), dtype=np.float32) + return { + "img": [DataContainer([NumpyTensor(image)])], + "img_metas": [DataContainer([[metadata]])], + } + + +def test_temporal_inputs_reset_and_update_between_scenes(): + prev_bev = np.zeros((4, 1, 2), dtype=np.float32) + previous_frame = {"scene_token": None, "position": 0, "angle": 0} + first_can_bus = [1, 2, 3, *range(4, 18), 18] + + first = build_inputs(make_data("scene-a", first_can_bus), prev_bev, previous_frame) + + assert first["use_prev_bev"].tolist() == [0.0] + np.testing.assert_array_equal(first["can_bus"][:3], np.zeros(3)) + assert first["can_bus"][-1] == 0 + np.testing.assert_array_equal(previous_frame["position"], first_can_bus[:3]) + assert previous_frame["angle"] == first_can_bus[-1] + + second_can_bus = copy.copy(first_can_bus) + second_can_bus[:3] = [3, 5, 7] + second_can_bus[-1] = 21 + second = build_inputs(make_data("scene-a", second_can_bus), prev_bev, previous_frame) + + assert second["use_prev_bev"].tolist() == [1.0] + np.testing.assert_array_equal(second["can_bus"][:3], [2, 3, 4]) + assert second["can_bus"][-1] == 3 + + third = build_inputs(make_data("scene-b", second_can_bus), prev_bev, previous_frame) + + assert third["use_prev_bev"].tolist() == [0.0] + np.testing.assert_array_equal(third["can_bus"][:3], np.zeros(3)) + assert third["can_bus"][-1] == 0