| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| """CPU-only regression tests for build_tensorrt_engine.build_full_pipeline. |
| |
| The full TRT build path is exercised by tests/scripts/deployment/test_trt_pipeline.py |
| under @pytest.mark.gpu. These tests cover the orchestration layer only: shape |
| inference, engine compilation, and the tensorrt / onnx imports themselves are |
| stubbed so the assertions run on any CPU host. |
| |
| Keep the stubs and the build_tensorrt_engine import inside the |
| build_full_pipeline fixture. Installing them at module top-level replaces |
| sys.modules["onnx"] for every pytest-xdist worker that collects this file, |
| including GPU workers, where the empty stub then crashes torch.onnx.export |
| inside the unrelated test_trt_full_pipeline. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
| import sys |
| import types |
| from unittest.mock import patch |
|
|
| import pytest |
|
|
|
|
| _PIPELINE_ONNX_FILES = [ |
| |
| |
| |
| "vit_fp32.onnx", |
| "llm_bf16.onnx", |
| "vl_self_attention.onnx", |
| "state_encoder.onnx", |
| "action_encoder.onnx", |
| "dit_bf16.onnx", |
| "action_decoder.onnx", |
| ] |
|
|
|
|
| @pytest.fixture |
| def build_full_pipeline(monkeypatch): |
| """Yield build_full_pipeline with tensorrt/onnx stubbed in sys.modules. |
| |
| Every side effect goes through monkeypatch so it is reverted at teardown |
| and never leaks across tests collected by the same pytest-xdist worker. |
| """ |
| if "tensorrt" not in sys.modules: |
| trt_stub = types.ModuleType("tensorrt") |
| trt_stub.Logger = types.SimpleNamespace(WARNING=0, ERROR=1, INFO=2, VERBOSE=3) |
| monkeypatch.setitem(sys.modules, "tensorrt", trt_stub) |
| if "onnx" not in sys.modules: |
| monkeypatch.setitem(sys.modules, "onnx", types.ModuleType("onnx")) |
|
|
| |
| |
| deploy_dir = os.path.abspath( |
| os.path.join(os.path.dirname(__file__), "../../../scripts/deployment") |
| ) |
| monkeypatch.syspath_prepend(deploy_dir) |
|
|
| from build_tensorrt_engine import build_full_pipeline as fn |
|
|
| yield fn |
|
|
|
|
| def _seed_dummy_onnx_dir(onnx_dir): |
| """Touch every ONNX file build_full_pipeline iterates over.""" |
| onnx_dir.mkdir(parents=True, exist_ok=True) |
| for fname in _PIPELINE_ONNX_FILES: |
| (onnx_dir / fname).touch() |
|
|
|
|
| def _fake_build_engine_success(onnx_path, engine_path, **kwargs): |
| with open(engine_path, "wb"): |
| pass |
|
|
|
|
| def test_build_full_pipeline_raises_when_any_engine_fails(tmp_path, build_full_pipeline): |
| """Regression: a single sub-engine failure must not silently exit 0. |
| |
| Before the fix, build_full_pipeline caught all build_engine exceptions, logged |
| them into a results list, and returned without raising. main() therefore |
| exited 0 even though the engine directory was incomplete, and downstream |
| verify/benchmark steps were the first to notice. |
| """ |
| onnx_dir = tmp_path / "onnx" |
| engine_dir = tmp_path / "engines" |
| _seed_dummy_onnx_dir(onnx_dir) |
|
|
| def fake_build_engine(onnx_path, engine_path, **kwargs): |
| if "llm" in os.path.basename(onnx_path): |
| raise RuntimeError("simulated TRT failure") |
| _fake_build_engine_success(onnx_path, engine_path) |
|
|
| with ( |
| patch("build_tensorrt_engine.derive_shapes_with_hint", return_value=({}, {}, {})), |
| patch("build_tensorrt_engine.build_engine", side_effect=fake_build_engine), |
| pytest.raises(RuntimeError, match=r"1/\d+ engine\(s\) failed"), |
| ): |
| build_full_pipeline( |
| onnx_dir=str(onnx_dir), |
| engine_dir=str(engine_dir), |
| precision="bf16", |
| allow_default_hints=True, |
| ) |
|
|
|
|
| def test_build_full_pipeline_returns_normally_when_all_engines_build(tmp_path, build_full_pipeline): |
| """Happy path: every engine builds → no exception.""" |
| onnx_dir = tmp_path / "onnx" |
| engine_dir = tmp_path / "engines" |
| _seed_dummy_onnx_dir(onnx_dir) |
|
|
| with ( |
| patch("build_tensorrt_engine.derive_shapes_with_hint", return_value=({}, {}, {})), |
| patch("build_tensorrt_engine.build_engine", side_effect=_fake_build_engine_success), |
| ): |
| build_full_pipeline( |
| onnx_dir=str(onnx_dir), |
| engine_dir=str(engine_dir), |
| precision="bf16", |
| allow_default_hints=True, |
| ) |
|
|
|
|
| def test_build_full_pipeline_raises_when_all_onnx_inputs_missing(tmp_path, build_full_pipeline): |
| """Empty ONNX dir must raise instead of producing zero engines and exiting 0.""" |
| onnx_dir = tmp_path / "onnx_missing" |
| onnx_dir.mkdir() |
| engine_dir = tmp_path / "engines" |
|
|
| with ( |
| patch("build_tensorrt_engine.derive_shapes_with_hint", return_value=({}, {}, {})), |
| patch("build_tensorrt_engine.build_engine", side_effect=_fake_build_engine_success), |
| pytest.raises( |
| RuntimeError, |
| match=rf"{len(_PIPELINE_ONNX_FILES)}/{len(_PIPELINE_ONNX_FILES)} component\(s\) had no ONNX input", |
| ), |
| ): |
| build_full_pipeline( |
| onnx_dir=str(onnx_dir), |
| engine_dir=str(engine_dir), |
| precision="bf16", |
| allow_default_hints=True, |
| ) |
|
|
| assert not engine_dir.exists() or not list(engine_dir.iterdir()) |
|
|
|
|
| def test_build_full_pipeline_raises_when_some_onnx_inputs_missing(tmp_path, build_full_pipeline): |
| """Partially-populated ONNX dir must raise; "full pipeline" means full.""" |
| onnx_dir = tmp_path / "onnx_partial" |
| onnx_dir.mkdir() |
| seeded = _PIPELINE_ONNX_FILES[:5] |
| missing = _PIPELINE_ONNX_FILES[5:] |
| for fname in seeded: |
| (onnx_dir / fname).touch() |
| engine_dir = tmp_path / "engines" |
|
|
| with ( |
| patch("build_tensorrt_engine.derive_shapes_with_hint", return_value=({}, {}, {})), |
| patch("build_tensorrt_engine.build_engine", side_effect=_fake_build_engine_success), |
| pytest.raises( |
| RuntimeError, |
| match=rf"{len(missing)}/{len(_PIPELINE_ONNX_FILES)} component\(s\) had no ONNX input", |
| ), |
| ): |
| build_full_pipeline( |
| onnx_dir=str(onnx_dir), |
| engine_dir=str(engine_dir), |
| precision="bf16", |
| allow_default_hints=True, |
| ) |
|
|
|
|
| def test_build_full_pipeline_error_mentions_both_skips_and_failures(tmp_path, build_full_pipeline): |
| """When components both skip AND fail, the exception message lists both reasons.""" |
| onnx_dir = tmp_path / "onnx" |
| onnx_dir.mkdir() |
| skipped_file = "llm_bf16.onnx" |
| failing_file = "dit_bf16.onnx" |
| for fname in _PIPELINE_ONNX_FILES: |
| if fname == skipped_file: |
| continue |
| (onnx_dir / fname).touch() |
| engine_dir = tmp_path / "engines" |
|
|
| def fake_build(onnx_path, engine_path, **kwargs): |
| if os.path.basename(onnx_path) == failing_file: |
| raise RuntimeError("simulated TRT failure") |
| _fake_build_engine_success(onnx_path, engine_path) |
|
|
| with ( |
| patch("build_tensorrt_engine.derive_shapes_with_hint", return_value=({}, {}, {})), |
| patch("build_tensorrt_engine.build_engine", side_effect=fake_build), |
| ): |
| with pytest.raises(RuntimeError) as exc_info: |
| build_full_pipeline( |
| onnx_dir=str(onnx_dir), |
| engine_dir=str(engine_dir), |
| precision="bf16", |
| allow_default_hints=True, |
| ) |
|
|
| message = str(exc_info.value) |
| assert "engine(s) failed" in message, message |
| assert "had no ONNX input" in message, message |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| @pytest.fixture |
| def check_precision_match(build_full_pipeline): |
| """Return the _check_strongly_typed_precision_match helper from the module.""" |
| from build_tensorrt_engine import _check_strongly_typed_precision_match |
|
|
| return _check_strongly_typed_precision_match |
|
|
|
|
| def test_strongly_typed_precision_match_passes_for_pure_match(check_precision_match): |
| """bf16 request against a pure-BF16 network is fine.""" |
| check_precision_match({"BF16"}, "bf16") |
|
|
|
|
| def test_strongly_typed_precision_match_passes_for_mixed_network(check_precision_match): |
| """The real exporter produces a mixed graph (ViT FP32, rest BF16); bf16 still matches.""" |
| check_precision_match({"BF16", "FLOAT"}, "bf16") |
|
|
|
|
| @pytest.mark.parametrize( |
| ("network_dtypes", "requested"), |
| [ |
| ({"BF16"}, "fp16"), |
| ({"BF16", "FLOAT"}, "fp16"), |
| ({"FLOAT"}, "bf16"), |
| ({"BF16"}, "fp8"), |
| |
| |
| |
| |
| |
| ({"BF16", "FLOAT"}, "fp32"), |
| ({"HALF", "FLOAT"}, "fp32"), |
| ({"FP8", "FLOAT"}, "fp32"), |
| ], |
| ) |
| def test_strongly_typed_precision_match_raises_on_mismatch( |
| check_precision_match, network_dtypes, requested |
| ): |
| """A --precision the STRONGLY_TYPED network cannot honor must raise, not pass silently.""" |
| with pytest.raises(ValueError, match="cannot be honored"): |
| check_precision_match(network_dtypes, requested) |
|
|
|
|
| def test_strongly_typed_precision_match_rejects_unknown_token(check_precision_match): |
| """A typo like --precision=int8 should fail loudly, not be treated as 'no match'.""" |
| with pytest.raises(ValueError, match="Unknown precision"): |
| check_precision_match({"BF16"}, "int8") |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
|
|
| @pytest.fixture |
| def precision_from_onnx_path(build_full_pipeline): |
| """Return the _precision_from_onnx_path helper from the module.""" |
| from build_tensorrt_engine import _precision_from_onnx_path |
|
|
| return _precision_from_onnx_path |
|
|
|
|
| @pytest.mark.parametrize( |
| ("path", "expected"), |
| [ |
| ("/some/dir/vit_fp32.onnx", "fp32"), |
| ("vit_bf16.onnx", "bf16"), |
| ("dit_bf16.onnx", "bf16"), |
| ("llm_bf16.onnx", "bf16"), |
| ("model_fp16.onnx", "fp16"), |
| ("model_fp8.onnx", "fp8"), |
| ], |
| ) |
| def test_precision_from_onnx_path_reads_filename_suffix(precision_from_onnx_path, path, expected): |
| """Recognized suffix overrides the pipeline default.""" |
| assert precision_from_onnx_path(path, default="bf16") == expected |
|
|
|
|
| @pytest.mark.parametrize( |
| "path", |
| [ |
| "state_encoder.onnx", |
| "action_encoder.onnx", |
| "vl_self_attention.onnx", |
| "action_decoder.onnx", |
| "/abs/path/with_no_precision_suffix.onnx", |
| ], |
| ) |
| def test_precision_from_onnx_path_falls_back_to_default(precision_from_onnx_path, path): |
| """No recognized suffix → return the pipeline default unchanged.""" |
| assert precision_from_onnx_path(path, default="bf16") == "bf16" |
| assert precision_from_onnx_path(path, default="fp32") == "fp32" |
|
|
|
|
| def test_build_full_pipeline_passes_per_component_precision(tmp_path, build_full_pipeline): |
| """ViT is built FP32, every other component inherits the pipeline default. |
| |
| The real exporter writes vit_fp32.onnx (FLOAT IO), so the ViT engine must |
| be built with precision=fp32 to satisfy the STRONGLY_TYPED sanity check, |
| while every other component uses the pipeline-wide default (bf16). |
| """ |
| onnx_dir = tmp_path / "onnx" |
| engine_dir = tmp_path / "engines" |
| _seed_dummy_onnx_dir(onnx_dir) |
|
|
| seen_precisions: dict[str, str] = {} |
|
|
| def fake_build_engine(onnx_path, engine_path, precision, **kwargs): |
| seen_precisions[os.path.basename(onnx_path)] = precision |
| _fake_build_engine_success(onnx_path, engine_path) |
|
|
| with ( |
| patch("build_tensorrt_engine.derive_shapes_with_hint", return_value=({}, {}, {})), |
| patch("build_tensorrt_engine.build_engine", side_effect=fake_build_engine), |
| ): |
| build_full_pipeline( |
| onnx_dir=str(onnx_dir), |
| engine_dir=str(engine_dir), |
| precision="bf16", |
| allow_default_hints=True, |
| ) |
|
|
| assert seen_precisions["vit_fp32.onnx"] == "fp32", ( |
| f"ViT must be built as fp32 to match its STRONGLY_TYPED FLOAT IO " |
| f"(saw precision={seen_precisions['vit_fp32.onnx']!r})" |
| ) |
| for fname in ( |
| "llm_bf16.onnx", |
| "dit_bf16.onnx", |
| "vl_self_attention.onnx", |
| "state_encoder.onnx", |
| "action_encoder.onnx", |
| "action_decoder.onnx", |
| ): |
| assert seen_precisions[fname] == "bf16", ( |
| f"{fname} should inherit the pipeline default precision=bf16 " |
| f"(saw {seen_precisions[fname]!r})" |
| ) |
|
|
|
|
| def test_build_full_pipeline_falls_back_to_vit_bf16_when_fp32_missing( |
| tmp_path, build_full_pipeline |
| ): |
| """If only vit_bf16.onnx is on disk, ViT picks up the pipeline default (bf16).""" |
| onnx_dir = tmp_path / "onnx" |
| engine_dir = tmp_path / "engines" |
| onnx_dir.mkdir(parents=True) |
| |
| fallback_files = [ |
| "vit_bf16.onnx", |
| "llm_bf16.onnx", |
| "vl_self_attention.onnx", |
| "state_encoder.onnx", |
| "action_encoder.onnx", |
| "dit_bf16.onnx", |
| "action_decoder.onnx", |
| ] |
| for fname in fallback_files: |
| (onnx_dir / fname).touch() |
|
|
| seen_precisions: dict[str, str] = {} |
|
|
| def fake_build_engine(onnx_path, engine_path, precision, **kwargs): |
| seen_precisions[os.path.basename(onnx_path)] = precision |
| _fake_build_engine_success(onnx_path, engine_path) |
|
|
| with ( |
| patch("build_tensorrt_engine.derive_shapes_with_hint", return_value=({}, {}, {})), |
| patch("build_tensorrt_engine.build_engine", side_effect=fake_build_engine), |
| ): |
| build_full_pipeline( |
| onnx_dir=str(onnx_dir), |
| engine_dir=str(engine_dir), |
| precision="bf16", |
| allow_default_hints=True, |
| ) |
|
|
| assert seen_precisions["vit_bf16.onnx"] == "bf16" |
|
|