Skip to content

Commit 332078f

Browse files
Test/tensorrt cpu validation (#162)
## πŸ”— Related Issue Fixes # ## πŸ“ Description * Added CPU validation for the TensorRT export path. * Fixed missing export CLI defaults. * Validated PyTorch β†’ ONNX β†’ ONNX Runtime on Linux CPU. * TensorRT correctly requires CUDA/NVIDIA GPU. ## πŸ”„ Type of Change * [x] πŸ› Bug fix (non-breaking change which fixes an issue) * [ ] πŸš€ New feature (non-breaking change which adds functionality) * [ ] πŸ’₯ Breaking change (fix or feature that would cause existing functionality to not work as expected) * [x] πŸ“– Documentation update * [ ] πŸ—οΈ Infrastructure / CI/CD update ## πŸ§ͺ Hardware & Matrix Testing **I have successfully built and tested this code using `uv` on:** * [x] `anomavision[cpu]` (Standard/Edge) * [ ] `anomavision[cu121]` (CUDA 12.1) * [ ] `anomavision[cu124]` (CUDA 12.4) * [ ] `anomavision[cu118]` (CUDA 11.8) **Host OS used for testing:** * [x] Linux / Ubuntu * [ ] Windows (Native or WSL2) * [ ] macOS ## βœ… Developer Checklist * [x] My code follows the core style guidelines of this project (Ruff/Black formatting). * [ ] I have run `uv run pytest` and all unit tests pass locally. * [ ] **Lockfile Guard:** If I added or modified a dependency in `pyproject.toml`, I have run `uv lock --python 3.10` and committed the updated `uv.lock` file. * [x] I have added tests that prove my fix is effective or that my feature works. * [x] I have updated the documentation accordingly. ## πŸ“Έ Screenshots / Visual Proof (Optional) * CPU TensorRT validation: **4 tests passed** * ONNX validation: **OK** * ONNX Runtime inference: **OK** * TensorRT CPU guard: **correctly rejects export without CUDA**
2 parents 8768aea + a2b0267 commit 332078f

3 files changed

Lines changed: 223 additions & 0 deletions

File tree

β€Žanomavision/export.pyβ€Ž

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,7 @@ def _apply_export_defaults(config):
916916
"quantize_static": False,
917917
"static_batch": False,
918918
"optimize": False,
919+
"log_level": "INFO",
919920
}
920921
for key, value in defaults.items():
921922
if not hasattr(config, key):

β€Ždocs/tensorrt.mdβ€Ž

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# TensorRT
2+
3+
AnomaVision TensorRT export builds a native NVIDIA TensorRT engine from the model's FP32 ONNX graph.
4+
5+
## What requires an NVIDIA GPU
6+
7+
A real TensorRT engine cannot be built or executed on a CPU-only machine. The following steps require an NVIDIA Linux environment with CUDA, TensorRT, and PyCUDA:
8+
9+
- TensorRT network parsing and engine building
10+
- FP16/FP32 engine generation
11+
- INT8 calibration
12+
- TensorRT engine deserialization and inference
13+
14+
Do **not** install `nvidia-utils` on a CPU-only machine just to test this path. The NVIDIA GPU and driver must actually be available.
15+
16+
## What can be tested on CPU
17+
18+
The repository now includes CPU-only tests for the TensorRT implementation:
19+
20+
```bash
21+
pytest -q tests/test_tensorrt_cpu_validation.py
22+
```
23+
24+
These tests verify:
25+
26+
- the main `anomavision export` parser accepts all TensorRT arguments;
27+
- the standalone `scripts/convert_to_tensorrt.py` parser accepts TensorRT options;
28+
- `ModelExporter.export_tensorrt()` stops cleanly on CPU before importing TensorRT or PyCUDA;
29+
- no temporary ONNX file is left behind when the CPU guard is triggered.
30+
31+
You can also verify the CLI help without a GPU:
32+
33+
```bash
34+
python -m anomavision.cli export --help
35+
python scripts/convert_to_tensorrt.py --help
36+
```
37+
38+
## CPU guard test
39+
40+
The expected behavior on a CPU-only machine is a clean TensorRT failure with the message:
41+
42+
```text
43+
TensorRT export requires a CUDA device.
44+
```
45+
46+
The export method returns `None`, allowing the CLI to report the failed export instead of crashing during a TensorRT/PyCUDA import.
47+
48+
## Real TensorRT test on NVIDIA Linux
49+
50+
After moving the repository to an NVIDIA Linux machine, verify the environment first:
51+
52+
```bash
53+
nvidia-smi
54+
```
55+
56+
Then test the normal export path. For example:
57+
58+
```bash
59+
anomavision export \
60+
--config config.yml \
61+
--format tensorrt \
62+
--device cuda \
63+
--tensorrt-precision fp16
64+
```
65+
66+
For INT8:
67+
68+
```bash
69+
anomavision export \
70+
--config config.yml \
71+
--format tensorrt \
72+
--device cuda \
73+
--tensorrt-precision int8 \
74+
--calib-dir ./dataset/<class>/train/good \
75+
--calib-samples 100
76+
```
77+
78+
The standalone converter is also available:
79+
80+
```bash
81+
python scripts/convert_to_tensorrt.py \
82+
--model ./path/to/model.pth \
83+
--output-dir ./engines \
84+
--precision fp16 \
85+
--device cuda
86+
```
87+
88+
For INT8:
89+
90+
```bash
91+
python scripts/convert_to_tensorrt.py \
92+
--model ./path/to/model.pth \
93+
--output-dir ./engines \
94+
--precision int8 \
95+
--device cuda \
96+
--calib-dir ./dataset/<class>/train/good \
97+
--calib-samples 100
98+
```
99+
100+
## Dynamic batch profiles
101+
102+
TensorRT dynamic export uses:
103+
104+
```text
105+
min_batch <= opt_batch <= max_batch
106+
```
107+
108+
Defaults are:
109+
110+
```text
111+
min_batch = 1
112+
opt_batch = 1
113+
max_batch = 4
114+
```
115+
116+
Use `--static-batch` to disable the optimization profile and build a fixed-batch engine.
117+
118+
## INT8 calibration
119+
120+
TensorRT INT8 calibration intentionally requires real calibration images. Random calibration data is disabled for TensorRT export.
121+
122+
Calibration images are loaded recursively and preprocessed to NCHW FP32 tensors using the same image preprocessing used by AnomaVision.
123+
124+
A reusable TensorRT calibration cache is written next to the engine output.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from unittest.mock import MagicMock
2+
3+
import torch
4+
5+
from anomavision.export import ModelExporter, create_parser
6+
from scripts.convert_to_tensorrt import build_parser
7+
8+
9+
def test_export_parser_accepts_tensorrt_options():
10+
args = create_parser().parse_args(
11+
[
12+
"--model_data_path",
13+
"./distributions",
14+
"--algorithm",
15+
"padim",
16+
"--model",
17+
"model.pt",
18+
"--format",
19+
"tensorrt",
20+
"--device",
21+
"cpu",
22+
"--tensorrt-precision",
23+
"int8",
24+
"--calib-dir",
25+
"./calibration",
26+
"--calib-samples",
27+
"25",
28+
"--workspace-gb",
29+
"1.5",
30+
"--min-batch",
31+
"1",
32+
"--opt-batch",
33+
"2",
34+
"--max-batch",
35+
"4",
36+
]
37+
)
38+
39+
assert args.format == "tensorrt"
40+
assert args.device == "cpu"
41+
assert args.tensorrt_precision == "int8"
42+
assert args.calib_dir == "./calibration"
43+
assert args.calib_samples == 25
44+
assert args.workspace_gb == 1.5
45+
assert (args.min_batch, args.opt_batch, args.max_batch) == (1, 2, 4)
46+
47+
48+
def test_standalone_tensorrt_parser_accepts_cpu_validation_arguments():
49+
args = build_parser().parse_args(
50+
[
51+
"--model",
52+
"model.pth",
53+
"--output-dir",
54+
"engines",
55+
"--precision",
56+
"fp16",
57+
"--device",
58+
"cpu",
59+
]
60+
)
61+
62+
assert args.precision == "fp16"
63+
assert args.device == "cpu"
64+
assert args.input_shape == (1, 3, 224, 224)
65+
66+
67+
def test_tensorrt_export_stops_cleanly_on_cpu_without_importing_tensorrt(tmp_path):
68+
logger = MagicMock()
69+
exporter = ModelExporter(
70+
tmp_path / "missing.pt",
71+
tmp_path,
72+
logger,
73+
device="cpu",
74+
)
75+
76+
result = exporter.export_tensorrt(
77+
input_shape=(1, 3, 16, 16),
78+
output_name="model_fp16.engine",
79+
precision="fp16",
80+
)
81+
82+
assert result is None
83+
logger.exception.assert_called_once()
84+
message = logger.exception.call_args.args[0]
85+
assert message == "tensorrt: failed after %.2fs"
86+
assert not (tmp_path / "model_fp16_fp32.onnx").exists()
87+
88+
89+
def test_cpu_tensor_device_is_used_by_exporter(tmp_path):
90+
logger = MagicMock()
91+
exporter = ModelExporter(
92+
tmp_path / "missing.pt",
93+
tmp_path,
94+
logger,
95+
device="cpu",
96+
)
97+
98+
assert exporter.device == torch.device("cpu")

0 commit comments

Comments
Β (0)