| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| """CPU-only tests for the export/TRT single-source contract helpers. |
| |
| ``_trt_contract`` has no heavy deps (json / os / logging only), so we import |
| it directly after putting ``scripts/deployment`` on ``sys.path``. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import os |
| import sys |
| import types |
|
|
| import pytest |
|
|
|
|
| DEPLOY_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../scripts/deployment")) |
| if DEPLOY_DIR not in sys.path: |
| sys.path.insert(0, DEPLOY_DIR) |
|
|
| import _trt_contract as tc |
|
|
|
|
| def _write_metadata(d, **kwargs): |
| meta = {"action_horizon": 16, "sa_seq_len": 17, "batch_size": 1} |
| meta.update(kwargs) |
| with open(os.path.join(d, "export_metadata.json"), "w") as f: |
| json.dump(meta, f) |
| return meta |
|
|
|
|
| def _fake_policy(action_horizon): |
| cfg = types.SimpleNamespace(action_horizon=action_horizon) |
| action_head = types.SimpleNamespace(config=cfg, action_horizon=action_horizon) |
| model = types.SimpleNamespace(action_head=action_head) |
| return types.SimpleNamespace(model=model) |
|
|
|
|
| |
|
|
|
|
| def test_load_metadata_from_engine_dir(tmp_path): |
| _write_metadata(tmp_path) |
| meta = tc.load_export_metadata(str(tmp_path)) |
| assert meta["action_horizon"] == 16 |
|
|
|
|
| def test_load_metadata_from_engine_file(tmp_path): |
| _write_metadata(tmp_path) |
| meta = tc.load_export_metadata(str(tmp_path / "dit_bf16.engine")) |
| assert meta["batch_size"] == 1 |
|
|
|
|
| def test_load_metadata_from_sibling_onnx_dir(tmp_path): |
| onnx = tmp_path / "onnx" |
| engines = tmp_path / "engines" |
| onnx.mkdir() |
| engines.mkdir() |
| _write_metadata(onnx) |
| meta = tc.load_export_metadata(str(engines)) |
| assert meta["action_horizon"] == 16 |
|
|
|
|
| def test_load_metadata_absent_returns_none(tmp_path): |
| assert tc.load_export_metadata(str(tmp_path)) is None |
|
|
|
|
| |
|
|
|
|
| def _valid_metadata(): |
| return { |
| "schema_version": tc.EXPORT_METADATA_SCHEMA_VERSION, |
| "sa_seq_len": 17, |
| "vl_seq_len": 280, |
| "llm_seq_len": 280, |
| "num_patches": 256, |
| "num_merged_patches": 64, |
| "num_vis_tokens": 64, |
| "action_horizon": 16, |
| "batch_size": 1, |
| "precision": "bf16", |
| } |
|
|
|
|
| def test_validate_export_metadata_ok(): |
| tc.validate_export_metadata(_valid_metadata()) |
|
|
|
|
| def test_validate_export_metadata_wrong_version_raises(): |
| meta = _valid_metadata() |
| meta["schema_version"] = tc.EXPORT_METADATA_SCHEMA_VERSION + 1 |
| with pytest.raises(ValueError, match="schema_version"): |
| tc.validate_export_metadata(meta) |
|
|
|
|
| def test_validate_export_metadata_missing_version_raises(): |
| meta = _valid_metadata() |
| del meta["schema_version"] |
| with pytest.raises(ValueError, match="schema_version"): |
| tc.validate_export_metadata(meta) |
|
|
|
|
| @pytest.mark.parametrize( |
| "key", [k for k in tc.REQUIRED_EXPORT_METADATA_KEYS if k != "schema_version"] |
| ) |
| def test_validate_export_metadata_missing_key_raises(key): |
| meta = _valid_metadata() |
| del meta[key] |
| with pytest.raises(ValueError, match="missing required key"): |
| tc.validate_export_metadata(meta) |
|
|
|
|
| |
|
|
|
|
| def test_engine_matches_policy_ok(tmp_path): |
| _write_metadata(tmp_path, action_horizon=16, sa_seq_len=17) |
| out = tc.assert_engine_matches_policy(_fake_policy(16), str(tmp_path)) |
| assert out["action_horizon"] == 16 |
|
|
|
|
| def test_engine_action_horizon_mismatch_raises(tmp_path): |
| _write_metadata(tmp_path, action_horizon=16, sa_seq_len=17) |
| with pytest.raises(ValueError, match="disagree on chunk size"): |
| tc.assert_engine_matches_policy(_fake_policy(40), str(tmp_path)) |
|
|
|
|
| def test_engine_corrupt_sa_seq_len_raises(tmp_path): |
| _write_metadata(tmp_path, action_horizon=16, sa_seq_len=99) |
| with pytest.raises(ValueError, match="corrupt"): |
| tc.assert_engine_matches_policy(_fake_policy(16), str(tmp_path)) |
|
|
|
|
| def test_engine_missing_metadata_warns_returns_none(tmp_path, caplog): |
| with caplog.at_level("WARNING"): |
| out = tc.assert_engine_matches_policy(_fake_policy(16), str(tmp_path)) |
| assert out is None |
| assert any("no export_metadata.json" in r.getMessage() for r in caplog.records) |
|
|
|
|
| def test_corrupt_metadata_treated_as_absent(tmp_path): |
| (tmp_path / "export_metadata.json").write_text("{ not valid json ") |
| |
| assert tc.load_export_metadata(str(tmp_path)) is None |
| assert tc.assert_engine_matches_policy(_fake_policy(16), str(tmp_path)) is None |
|
|
|
|
| def test_action_horizon_mismatch_message_without_sa_seq_len(tmp_path): |
| |
| |
| with open(tmp_path / "export_metadata.json", "w") as f: |
| json.dump({"action_horizon": 16, "batch_size": 1}, f) |
| with pytest.raises(ValueError) as exc: |
| tc.assert_engine_matches_policy(_fake_policy(40), str(tmp_path)) |
| assert "sa_seq_len=None" not in str(exc.value) |
|
|
|
|
| |
|
|
|
|
| _FULL_PIPELINE_REQUIRED = ( |
| "state_encoder.engine", |
| "action_encoder.engine", |
| "dit_bf16.engine", |
| "action_decoder.engine", |
| ) |
|
|
|
|
| def test_bundle_present_ok(tmp_path): |
| for name in _FULL_PIPELINE_REQUIRED: |
| (tmp_path / name).write_bytes(b"stub") |
| |
| tc.assert_engine_bundle_present( |
| str(tmp_path), _FULL_PIPELINE_REQUIRED, mode="n17_full_pipeline" |
| ) |
|
|
|
|
| def test_bundle_missing_dir_raises_with_build_hint(tmp_path): |
| missing_dir = tmp_path / "gr00t_trt_deployment" / "engines" |
| with pytest.raises(FileNotFoundError) as exc: |
| tc.assert_engine_bundle_present( |
| str(missing_dir), _FULL_PIPELINE_REQUIRED, mode="n17_full_pipeline" |
| ) |
| msg = str(exc.value) |
| assert "n17_full_pipeline" in msg |
| assert "build_trt_pipeline.py" in msg |
|
|
|
|
| def test_bundle_missing_one_file_names_it(tmp_path): |
| for name in _FULL_PIPELINE_REQUIRED: |
| if name != "dit_bf16.engine": |
| (tmp_path / name).write_bytes(b"stub") |
| with pytest.raises(FileNotFoundError) as exc: |
| tc.assert_engine_bundle_present( |
| str(tmp_path), _FULL_PIPELINE_REQUIRED, mode="n17_full_pipeline" |
| ) |
| msg = str(exc.value) |
| assert "dit_bf16.engine" in msg |
| assert "state_encoder.engine" not in msg |
|
|
|
|
| |
|
|
|
|
| def test_resolve_batch_size_default_from_metadata(tmp_path): |
| _write_metadata(tmp_path, batch_size=4) |
| assert tc.resolve_batch_size(str(tmp_path)) == 4 |
|
|
|
|
| def test_resolve_batch_size_matching_request(tmp_path): |
| _write_metadata(tmp_path, batch_size=2) |
| assert tc.resolve_batch_size(str(tmp_path), 2) == 2 |
|
|
|
|
| def test_resolve_batch_size_mismatch_raises(tmp_path): |
| _write_metadata(tmp_path, batch_size=1) |
| with pytest.raises(ValueError, match="built .*for batch_size=1"): |
| tc.resolve_batch_size(str(tmp_path), 4) |
|
|
|
|
| def test_resolve_batch_size_no_metadata_defaults_to_one(tmp_path): |
| assert tc.resolve_batch_size(str(tmp_path)) == 1 |
| |
| assert tc.resolve_batch_size(str(tmp_path), 8) == 8 |
|
|
|
|
| |
|
|
|
|
| def test_grid_thw_matches_ok(): |
| tc.assert_grid_thw_matches([[1, 16, 16]], [[1, 16, 16]]) |
|
|
|
|
| def test_grid_thw_none_baked_skips(): |
| |
| tc.assert_grid_thw_matches(None, [[1, 8, 8]]) |
|
|
|
|
| def test_grid_thw_different_layout_same_count_raises(): |
| |
| |
| with pytest.raises(ValueError, match="image_grid_thw"): |
| tc.assert_grid_thw_matches([[1, 16, 16]], [[1, 8, 32]]) |
|
|
|
|
| def test_grid_thw_more_views_same_layout_ok(): |
| |
| |
| |
| tc.assert_grid_thw_matches([[1, 16, 16], [1, 16, 16]], [[1, 16, 16]] * 4) |
|
|
|
|
| def test_grid_thw_extra_view_unbaked_layout_raises(): |
| |
| with pytest.raises(ValueError, match="ViT TRT"): |
| tc.assert_grid_thw_matches([[1, 16, 16]], [[1, 16, 16], [1, 8, 32]]) |
|
|
|
|
| def test_grid_thw_accepts_tensor_like(): |
| class _FakeTensor: |
| def __init__(self, data): |
| self._data = data |
|
|
| def detach(self): |
| return self |
|
|
| def cpu(self): |
| return self |
|
|
| def tolist(self): |
| return self._data |
|
|
| tc.assert_grid_thw_matches([[1, 16, 16]], _FakeTensor([[1, 16, 16]])) |
| with pytest.raises(ValueError): |
| tc.assert_grid_thw_matches([[1, 16, 16]], _FakeTensor([[2, 16, 16]])) |
|
|
|
|
| |
|
|
|
|
| @pytest.mark.parametrize( |
| "exec_h,model_h,ok", |
| [(16, 16, True), (8, 16, True), (1, 40, True), (17, 16, False), (0, 16, False)], |
| ) |
| def test_assert_exec_horizon_within_model(exec_h, model_h, ok): |
| if ok: |
| tc.assert_exec_horizon_within_model(exec_horizon=exec_h, model_action_horizon=model_h) |
| else: |
| with pytest.raises(ValueError, match="execution-horizon"): |
| tc.assert_exec_horizon_within_model(exec_horizon=exec_h, model_action_horizon=model_h) |
|
|