inference-artifact-lab 0.1.0.dev0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. inference_artifact_lab-0.1.0.dev0/LICENSE +21 -0
  2. inference_artifact_lab-0.1.0.dev0/PKG-INFO +100 -0
  3. inference_artifact_lab-0.1.0.dev0/README.md +84 -0
  4. inference_artifact_lab-0.1.0.dev0/pyproject.toml +26 -0
  5. inference_artifact_lab-0.1.0.dev0/setup.cfg +4 -0
  6. inference_artifact_lab-0.1.0.dev0/src/inference_artifact_lab/__init__.py +14 -0
  7. inference_artifact_lab-0.1.0.dev0/src/inference_artifact_lab/__main__.py +85 -0
  8. inference_artifact_lab-0.1.0.dev0/src/inference_artifact_lab/adapters.py +296 -0
  9. inference_artifact_lab-0.1.0.dev0/src/inference_artifact_lab/gate.py +163 -0
  10. inference_artifact_lab-0.1.0.dev0/src/inference_artifact_lab/models.py +264 -0
  11. inference_artifact_lab-0.1.0.dev0/src/inference_artifact_lab/release.py +70 -0
  12. inference_artifact_lab-0.1.0.dev0/src/inference_artifact_lab/runtime.py +177 -0
  13. inference_artifact_lab-0.1.0.dev0/src/inference_artifact_lab.egg-info/PKG-INFO +100 -0
  14. inference_artifact_lab-0.1.0.dev0/src/inference_artifact_lab.egg-info/SOURCES.txt +22 -0
  15. inference_artifact_lab-0.1.0.dev0/src/inference_artifact_lab.egg-info/dependency_links.txt +1 -0
  16. inference_artifact_lab-0.1.0.dev0/src/inference_artifact_lab.egg-info/entry_points.txt +2 -0
  17. inference_artifact_lab-0.1.0.dev0/src/inference_artifact_lab.egg-info/requires.txt +7 -0
  18. inference_artifact_lab-0.1.0.dev0/src/inference_artifact_lab.egg-info/top_level.txt +1 -0
  19. inference_artifact_lab-0.1.0.dev0/tests/test_adapters.py +171 -0
  20. inference_artifact_lab-0.1.0.dev0/tests/test_gate.py +151 -0
  21. inference_artifact_lab-0.1.0.dev0/tests/test_models.py +52 -0
  22. inference_artifact_lab-0.1.0.dev0/tests/test_package.py +5 -0
  23. inference_artifact_lab-0.1.0.dev0/tests/test_release.py +78 -0
  24. inference_artifact_lab-0.1.0.dev0/tests/test_runtime.py +44 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ray Carter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: inference-artifact-lab
3
+ Version: 0.1.0.dev0
4
+ Summary: Reproducible validation gates for machine-learning inference artifacts
5
+ Author: Ray Carter
6
+ License: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Provides-Extra: test
11
+ Requires-Dist: pytest>=8.0; extra == "test"
12
+ Provides-Extra: runtime-cpu
13
+ Requires-Dist: numpy>=1.26; extra == "runtime-cpu"
14
+ Requires-Dist: onnxruntime==1.30.0; extra == "runtime-cpu"
15
+ Dynamic: license-file
16
+
17
+ # Inference Artifact Lab
18
+
19
+ Inference Artifact Lab is a clean-room, public-by-design project for validating
20
+ machine-learning deployment artifacts before release. Its first product increment
21
+ is the **Model Release Gate**: a reproducible gate for artifact integrity,
22
+ input/output contracts, runtime correctness, and environment compatibility.
23
+
24
+ The project uses public models, public datasets, and generated fixtures. It does
25
+ not train models, provide a general model-serving gateway, or claim model quality
26
+ beyond the declared validation evidence.
27
+
28
+ ## Current status
29
+
30
+ Development preview: public SqueezeNet CPU and TensorRT smoke runs are recorded.
31
+ Phase 1 acceptance remains incomplete; report delivery and clean reproduction
32
+ need further work. See [publication review](docs/publication-review.md) for
33
+ known limitations. The commands below are development examples, not a verified
34
+ from-scratch reproduction procedure.
35
+
36
+ ## Planned flow
37
+
38
+ ```text
39
+ public model
40
+ -> source and artifact manifest
41
+ -> export/build
42
+ -> integrity and contract checks
43
+ -> reference/runtime equivalence checks
44
+ -> environment compatibility checks
45
+ -> resource benchmark
46
+ -> machine-readable release report
47
+ ```
48
+
49
+ Run the public-reference smoke gate with:
50
+
51
+ ```text
52
+ uv run --with torch --with torchvision --with onnx --with onnxruntime python scripts/run_torchvision_gate.py
53
+ ```
54
+
55
+ It writes `reports/phase-1/squeezenet11-torchvision-onnx-cpu.json`.
56
+
57
+ Build and verify the TensorRT profile after pulling the pinned public image:
58
+
59
+ ```text
60
+ pwsh scripts/build_tensorrt_engine.ps1
61
+ pwsh scripts/benchmark_tensorrt_engine.ps1
62
+ docker run --rm --gpus all -v "${PWD}:/workspace" -w /workspace `
63
+ -e MODEL_RELEASE_GATE_CONTAINER_DIGEST=sha256:814325e2b8a653f354c30bbcf5ecc8d4c780cf878a88a320ae648fbfdd9dd82d `
64
+ nvcr.io/nvidia/tensorrt:25.02-py3 bash -lc `
65
+ "python -m pip install --quiet --index-url https://pypi.org/simple cuda-python==12.8.0; `
66
+ PYTHONPATH=/workspace/src python scripts/run_tensorrt_in_container.py `
67
+ --engine artifacts/squeezenet1.1-fp32.engine `
68
+ --fixture artifacts/squeezenet11-fixture.npy `
69
+ --output artifacts/squeezenet11-tensorrt-output.json"
70
+ uv run --with numpy==2.4.6 python scripts/compose_tensorrt_report.py `
71
+ --trtexec-log reports/phase-1/tensorrt-trtexec-benchmark.log
72
+ ```
73
+
74
+ The generated TensorRT report includes the contract, engine digest, fixture
75
+ equivalence, declared GPU/container fingerprint, and `trtexec` benchmark scope.
76
+
77
+ Render any JSON report for review with:
78
+
79
+ ```text
80
+ uv run python scripts/render_report.py reports/phase-1/squeezenet11-tensorrt.json
81
+ ```
82
+
83
+ The report contract is defined by
84
+ `schemas/release-report.schema.json`. A clean CPU reproduction starts with
85
+ `pwsh scripts/clean_reproduction.ps1` in a fresh checkout.
86
+
87
+ After generating the public fixture and reference output, the package CLI can
88
+ execute the ONNX CPU adapter directly:
89
+
90
+ ```text
91
+ python -m inference_artifact_lab examples/squeezenet11-torchvision.manifest.json `
92
+ --runtime onnx-cpu --inputs-npy artifacts/squeezenet11-fixture.npy `
93
+ --reference-output reference-output.json --environment environment.json `
94
+ --report reports/phase-1/cli-onnx-cpu.json
95
+ ```
96
+
97
+ The authoritative development documentation follows the same phase/stage model
98
+ used by the other portfolio repositories. Start at the [Codex document index](docs/codex/README.md), then read the [product contract](docs/codex/product-contract.md) and [Phase 1 plan](docs/codex/phases/phase-1-model-release-gate/README.md).
99
+
100
+ For the human-readable brief and clean-room record, see [Product Requirements](docs/product-requirements.md), [Acceptance Contract](docs/acceptance-contract.md), and [Clean-room Record](docs/clean-room-record.md).
@@ -0,0 +1,84 @@
1
+ # Inference Artifact Lab
2
+
3
+ Inference Artifact Lab is a clean-room, public-by-design project for validating
4
+ machine-learning deployment artifacts before release. Its first product increment
5
+ is the **Model Release Gate**: a reproducible gate for artifact integrity,
6
+ input/output contracts, runtime correctness, and environment compatibility.
7
+
8
+ The project uses public models, public datasets, and generated fixtures. It does
9
+ not train models, provide a general model-serving gateway, or claim model quality
10
+ beyond the declared validation evidence.
11
+
12
+ ## Current status
13
+
14
+ Development preview: public SqueezeNet CPU and TensorRT smoke runs are recorded.
15
+ Phase 1 acceptance remains incomplete; report delivery and clean reproduction
16
+ need further work. See [publication review](docs/publication-review.md) for
17
+ known limitations. The commands below are development examples, not a verified
18
+ from-scratch reproduction procedure.
19
+
20
+ ## Planned flow
21
+
22
+ ```text
23
+ public model
24
+ -> source and artifact manifest
25
+ -> export/build
26
+ -> integrity and contract checks
27
+ -> reference/runtime equivalence checks
28
+ -> environment compatibility checks
29
+ -> resource benchmark
30
+ -> machine-readable release report
31
+ ```
32
+
33
+ Run the public-reference smoke gate with:
34
+
35
+ ```text
36
+ uv run --with torch --with torchvision --with onnx --with onnxruntime python scripts/run_torchvision_gate.py
37
+ ```
38
+
39
+ It writes `reports/phase-1/squeezenet11-torchvision-onnx-cpu.json`.
40
+
41
+ Build and verify the TensorRT profile after pulling the pinned public image:
42
+
43
+ ```text
44
+ pwsh scripts/build_tensorrt_engine.ps1
45
+ pwsh scripts/benchmark_tensorrt_engine.ps1
46
+ docker run --rm --gpus all -v "${PWD}:/workspace" -w /workspace `
47
+ -e MODEL_RELEASE_GATE_CONTAINER_DIGEST=sha256:814325e2b8a653f354c30bbcf5ecc8d4c780cf878a88a320ae648fbfdd9dd82d `
48
+ nvcr.io/nvidia/tensorrt:25.02-py3 bash -lc `
49
+ "python -m pip install --quiet --index-url https://pypi.org/simple cuda-python==12.8.0; `
50
+ PYTHONPATH=/workspace/src python scripts/run_tensorrt_in_container.py `
51
+ --engine artifacts/squeezenet1.1-fp32.engine `
52
+ --fixture artifacts/squeezenet11-fixture.npy `
53
+ --output artifacts/squeezenet11-tensorrt-output.json"
54
+ uv run --with numpy==2.4.6 python scripts/compose_tensorrt_report.py `
55
+ --trtexec-log reports/phase-1/tensorrt-trtexec-benchmark.log
56
+ ```
57
+
58
+ The generated TensorRT report includes the contract, engine digest, fixture
59
+ equivalence, declared GPU/container fingerprint, and `trtexec` benchmark scope.
60
+
61
+ Render any JSON report for review with:
62
+
63
+ ```text
64
+ uv run python scripts/render_report.py reports/phase-1/squeezenet11-tensorrt.json
65
+ ```
66
+
67
+ The report contract is defined by
68
+ `schemas/release-report.schema.json`. A clean CPU reproduction starts with
69
+ `pwsh scripts/clean_reproduction.ps1` in a fresh checkout.
70
+
71
+ After generating the public fixture and reference output, the package CLI can
72
+ execute the ONNX CPU adapter directly:
73
+
74
+ ```text
75
+ python -m inference_artifact_lab examples/squeezenet11-torchvision.manifest.json `
76
+ --runtime onnx-cpu --inputs-npy artifacts/squeezenet11-fixture.npy `
77
+ --reference-output reference-output.json --environment environment.json `
78
+ --report reports/phase-1/cli-onnx-cpu.json
79
+ ```
80
+
81
+ The authoritative development documentation follows the same phase/stage model
82
+ used by the other portfolio repositories. Start at the [Codex document index](docs/codex/README.md), then read the [product contract](docs/codex/product-contract.md) and [Phase 1 plan](docs/codex/phases/phase-1-model-release-gate/README.md).
83
+
84
+ For the human-readable brief and clean-room record, see [Product Requirements](docs/product-requirements.md), [Acceptance Contract](docs/acceptance-contract.md), and [Clean-room Record](docs/clean-room-record.md).
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "inference-artifact-lab"
3
+ version = "0.1.0.dev0"
4
+ description = "Reproducible validation gates for machine-learning inference artifacts"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = {text = "MIT"}
8
+ authors = [{name = "Ray Carter"}]
9
+ dependencies = []
10
+
11
+ [project.scripts]
12
+ model-release-gate = "inference_artifact_lab.__main__:main"
13
+
14
+ [project.optional-dependencies]
15
+ test = ["pytest>=8.0"]
16
+ runtime-cpu = ["numpy>=1.26", "onnxruntime==1.30.0"]
17
+
18
+ [build-system]
19
+ requires = ["setuptools>=75"]
20
+ build-backend = "setuptools.build_meta"
21
+
22
+ [tool.pytest.ini_options]
23
+ testpaths = ["tests"]
24
+
25
+ [tool.setuptools.packages.find]
26
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,14 @@
1
+ """Inference Artifact Lab package."""
2
+
3
+ from .gate import canonical_manifest_digest, current_environment, load_manifest, run_gate, run_gate_from_file, sha256_file
4
+ from .adapters import OnnxRuntimeAdapter, RuntimeUnavailable, TensorRTAdapter
5
+ from .models import ArtifactSpec, CheckResult, GateReport, GateStatus, Manifest, ManifestError, TensorSpec
6
+ from .release import run_release_gate
7
+ from .runtime import BenchmarkResult, benchmark, compare_outputs
8
+
9
+ __all__ = [
10
+ "__version__", "ArtifactSpec", "CheckResult", "GateReport", "GateStatus", "Manifest", "ManifestError", "TensorSpec",
11
+ "canonical_manifest_digest", "current_environment", "load_manifest", "run_gate", "run_gate_from_file", "run_release_gate", "sha256_file",
12
+ "BenchmarkResult", "benchmark", "compare_outputs", "OnnxRuntimeAdapter", "TensorRTAdapter", "RuntimeUnavailable",
13
+ ]
14
+ __version__ = "0.1.0.dev0"
@@ -0,0 +1,85 @@
1
+ """Command line entry point: ``python -m inference_artifact_lab``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ from .gate import run_gate_from_file
12
+ from .release import run_release_gate
13
+ from .runtime import benchmark
14
+
15
+
16
+ def main(argv: list[str] | None = None) -> int:
17
+ parser = argparse.ArgumentParser(description="Run core Model Release Gate checks")
18
+ parser.add_argument("manifest", help="path to a JSON manifest")
19
+ parser.add_argument("--artifact", help="artifact path (defaults to manifest artifact.path)")
20
+ parser.add_argument("--observed-contract", help="JSON file containing observed inputs and outputs")
21
+ parser.add_argument("--environment", help="JSON file containing the observed environment fingerprint")
22
+ parser.add_argument("--reference-output", help="JSON file containing reference runtime output")
23
+ parser.add_argument("--target-output", help="JSON file containing target runtime output")
24
+ parser.add_argument("--benchmark-evidence", help="JSON file containing benchmark evidence")
25
+ parser.add_argument("--report", help="write the release report JSON to this path")
26
+ parser.add_argument("--runtime", choices=["onnx-cpu"], help="run a real package adapter before composing the release report")
27
+ parser.add_argument("--inputs-npy", help="NumPy .npy input for the selected runtime adapter")
28
+ args = parser.parse_args(argv)
29
+ observed = None
30
+ if args.observed_contract:
31
+ with open(args.observed_contract, encoding="utf-8") as handle:
32
+ observed = json.load(handle)
33
+ environment = None
34
+ if args.environment:
35
+ with open(args.environment, encoding="utf-8") as handle:
36
+ environment = json.load(handle)
37
+ def load_json(path: str | None):
38
+ if not path:
39
+ return None
40
+ with open(path, encoding="utf-8") as handle:
41
+ return json.load(handle)
42
+ try:
43
+ reference = load_json(args.reference_output)
44
+ target = load_json(args.target_output)
45
+ benchmark_evidence = load_json(args.benchmark_evidence)
46
+ if args.runtime:
47
+ if not args.inputs_npy or reference is None or environment is None:
48
+ raise ValueError("--runtime requires --inputs-npy, --reference-output, and --environment")
49
+ if args.runtime != "onnx-cpu": # argparse currently prevents this; retain an explicit guard.
50
+ raise ValueError(f"unsupported runtime {args.runtime}")
51
+ import numpy as np
52
+ from .adapters import OnnxRuntimeAdapter
53
+ from .gate import load_manifest
54
+ manifest = load_manifest(args.manifest)
55
+ manifest_file = Path(args.manifest)
56
+ artifact = Path(args.artifact) if args.artifact else Path(manifest.artifact.path)
57
+ if not artifact.is_absolute():
58
+ artifact = Path(os.path.normpath(str(manifest_file.parent / artifact)))
59
+ values = np.load(args.inputs_npy, allow_pickle=False)
60
+ adapter = OnnxRuntimeAdapter(artifact, providers=["CPUExecutionProvider"])
61
+ output_name = adapter.contract()["outputs"][0]["name"]
62
+ input_name = adapter.contract()["inputs"][0]["name"]
63
+ target = adapter.run({input_name: values})[output_name]
64
+ reference_value = reference.get(output_name) if isinstance(reference, dict) else reference
65
+ measured = benchmark(lambda: adapter.run({input_name: values}), warmup_runs=1, measured_runs=5)
66
+ report = run_release_gate(manifest, artifact, observed_contract=adapter.contract(), observed_environment=environment, reference_outputs=reference_value, target_outputs=target, benchmark_evidence=measured.to_dict())
67
+ elif reference is not None or target is not None or benchmark_evidence is not None:
68
+ from .gate import load_manifest
69
+ manifest = load_manifest(args.manifest)
70
+ report = run_release_gate(manifest, args.artifact, observed_contract=observed, observed_environment=environment, reference_outputs=reference, target_outputs=target, benchmark_evidence=benchmark_evidence)
71
+ else:
72
+ report = run_gate_from_file(args.manifest, args.artifact, observed_contract=observed, observed_environment=environment)
73
+ except (OSError, ValueError) as exc:
74
+ print(json.dumps({"status": "fail", "error": str(exc)}), file=sys.stdout)
75
+ return 2
76
+ rendered = json.dumps(report.to_dict(), indent=2, sort_keys=True)
77
+ if args.report:
78
+ with open(args.report, "w", encoding="utf-8") as handle:
79
+ handle.write(rendered + "\n")
80
+ print(rendered)
81
+ return 0 if report.status.value == "pass" else 1
82
+
83
+
84
+ if __name__ == "__main__":
85
+ raise SystemExit(main())
@@ -0,0 +1,296 @@
1
+ """Optional runtime adapters used by the Phase 1 evidence harness."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import ctypes
6
+ import platform
7
+ from pathlib import Path
8
+ from typing import Any, Mapping
9
+
10
+
11
+ class RuntimeUnavailable(RuntimeError):
12
+ """Raised when a declared runtime dependency is not installed."""
13
+
14
+
15
+ class OnnxRuntimeAdapter:
16
+ """Small ONNX Runtime adapter with no dependency on the gate domain."""
17
+
18
+ def __init__(self, artifact: str | Path, *, providers: list[str] | None = None) -> None:
19
+ try:
20
+ import onnxruntime as ort
21
+ except ImportError as exc: # pragma: no cover - optional dependency
22
+ raise RuntimeUnavailable("install the runtime-cpu extra for ONNX Runtime") from exc
23
+ self._session = ort.InferenceSession(str(artifact), providers=providers)
24
+ self._ort_version = ort.__version__
25
+ self._requested_providers = tuple(providers) if providers is not None else None
26
+
27
+ def contract(self) -> dict[str, list[dict[str, Any]]]:
28
+ def describe(value: Any) -> dict[str, Any]:
29
+ return {"name": value.name, "dtype": value.type, "shape": list(value.shape)}
30
+
31
+ return {
32
+ "artifact_format": "onnx",
33
+ "inputs": [describe(value) for value in self._session.get_inputs()],
34
+ "outputs": [describe(value) for value in self._session.get_outputs()],
35
+ }
36
+
37
+ def runtime_info(self) -> dict[str, Any]:
38
+ """Return selected providers, including an explicit fallback provider."""
39
+
40
+ return {
41
+ "runtime": "onnxruntime",
42
+ "version": self._ort_version,
43
+ "requested_providers": list(self._requested_providers) if self._requested_providers is not None else None,
44
+ "providers": list(self._session.get_providers()),
45
+ "python": platform.python_version(),
46
+ "system": platform.system(),
47
+ "machine": platform.machine(),
48
+ }
49
+
50
+ def run(self, inputs: Mapping[str, Any]) -> dict[str, Any]:
51
+ values = self._session.run(None, dict(inputs))
52
+ return {
53
+ spec["name"]: value.tolist() if hasattr(value, "tolist") else value
54
+ for spec, value in zip(self.contract()["outputs"], values)
55
+ }
56
+
57
+
58
+ class _CtypesCudaRuntime:
59
+ """Minimal libcudart binding used when the optional cuda-python is absent."""
60
+
61
+ binding_name = "ctypes-libcudart"
62
+
63
+ class _MemcpyKind:
64
+ cudaMemcpyHostToDevice = 1
65
+ cudaMemcpyDeviceToHost = 2
66
+
67
+ cudaMemcpyKind = _MemcpyKind
68
+
69
+ def __init__(self) -> None:
70
+ last_error: OSError | None = None
71
+ for name in ("libcudart.so", "libcudart.so.12", "cudart64_*.dll"):
72
+ try:
73
+ self._lib = ctypes.CDLL(name)
74
+ break
75
+ except OSError as exc:
76
+ last_error = exc
77
+ else:
78
+ raise RuntimeUnavailable("TensorRTAdapter requires libcudart or cuda-python") from last_error
79
+ self._configure()
80
+
81
+ def _configure(self) -> None:
82
+ ptr, size = ctypes.c_void_p, ctypes.c_size_t
83
+ self._lib.cudaStreamCreate.argtypes = [ctypes.POINTER(ptr)]
84
+ self._lib.cudaStreamCreate.restype = ctypes.c_int
85
+ self._lib.cudaStreamDestroy.argtypes = [ptr]
86
+ self._lib.cudaStreamDestroy.restype = ctypes.c_int
87
+ self._lib.cudaStreamSynchronize.argtypes = [ptr]
88
+ self._lib.cudaStreamSynchronize.restype = ctypes.c_int
89
+ self._lib.cudaMalloc.argtypes = [ctypes.POINTER(ptr), size]
90
+ self._lib.cudaMalloc.restype = ctypes.c_int
91
+ self._lib.cudaFree.argtypes = [ptr]
92
+ self._lib.cudaFree.restype = ctypes.c_int
93
+ self._lib.cudaMemcpyAsync.argtypes = [ptr, ptr, size, ctypes.c_int, ptr]
94
+ self._lib.cudaMemcpyAsync.restype = ctypes.c_int
95
+
96
+ def cudaStreamCreate(self) -> tuple[int, int | None]:
97
+ value = ctypes.c_void_p()
98
+ return self._lib.cudaStreamCreate(ctypes.byref(value)), value.value
99
+
100
+ def cudaStreamDestroy(self, stream: int | None) -> int:
101
+ return self._lib.cudaStreamDestroy(ctypes.c_void_p(stream))
102
+
103
+ def cudaStreamSynchronize(self, stream: int | None) -> int:
104
+ return self._lib.cudaStreamSynchronize(ctypes.c_void_p(stream))
105
+
106
+ def cudaMalloc(self, size: int) -> tuple[int, int | None]:
107
+ value = ctypes.c_void_p()
108
+ return self._lib.cudaMalloc(ctypes.byref(value), ctypes.c_size_t(size)), value.value
109
+
110
+ def cudaFree(self, device: int | None) -> int:
111
+ return self._lib.cudaFree(ctypes.c_void_p(device))
112
+
113
+ def cudaMemcpyAsync(self, destination: int, source: int, size: int, kind: int, stream: int | None) -> int:
114
+ return self._lib.cudaMemcpyAsync(ctypes.c_void_p(destination), ctypes.c_void_p(source), ctypes.c_size_t(size), int(kind), ctypes.c_void_p(stream))
115
+
116
+
117
+ def _load_cuda_runtime() -> Any:
118
+ try:
119
+ from cuda import cudart # type: ignore[import-not-found]
120
+ except ImportError:
121
+ return _CtypesCudaRuntime()
122
+ # Kept as evidence in runtime_info; provider selection is never implicit.
123
+ try:
124
+ cudart.binding_name = "cuda-python" # type: ignore[attr-defined]
125
+ except AttributeError:
126
+ pass
127
+ return cudart
128
+
129
+
130
+ def _status_is_success(status: Any) -> bool:
131
+ value = getattr(status, "value", status)
132
+ if isinstance(value, bool):
133
+ return value
134
+ if isinstance(value, int):
135
+ return value == 0
136
+ name = str(getattr(status, "name", status)).lower()
137
+ return name in {"success", "cudasuccess", "cuda_success"}
138
+
139
+
140
+ def _check_cuda(result: Any, operation: str) -> Any:
141
+ """Check cuda-python/libcudart status and return an optional payload."""
142
+
143
+ if isinstance(result, tuple):
144
+ status, payload = result[0], result[1] if len(result) > 1 else None
145
+ else:
146
+ status, payload = result, None
147
+ if not _status_is_success(status):
148
+ raise RuntimeError(f"{operation} failed with CUDA status {status!r}")
149
+ return payload
150
+
151
+
152
+ class TensorRTAdapter:
153
+ """TensorRT engine adapter with explicit CUDA ownership and validation."""
154
+
155
+ def __init__(self, artifact: str | Path, *, trt_module: Any | None = None, cuda_runtime: Any | None = None, numpy_module: Any | None = None) -> None:
156
+ try:
157
+ trt = trt_module or __import__("tensorrt")
158
+ np = numpy_module or __import__("numpy")
159
+ except ImportError as exc: # pragma: no cover - optional container dependency
160
+ raise RuntimeUnavailable("TensorRTAdapter requires the pinned NVIDIA TensorRT container") from exc
161
+ self._trt, self._np = trt, np
162
+ self._cuda = cuda_runtime or _load_cuda_runtime()
163
+ self._logger = trt.Logger(trt.Logger.ERROR)
164
+ with Path(artifact).open("rb") as handle:
165
+ self._runtime = trt.Runtime(self._logger)
166
+ self._engine = self._runtime.deserialize_cuda_engine(handle.read())
167
+ if self._engine is None:
168
+ raise RuntimeError("TensorRT engine deserialization failed")
169
+ self._context = self._engine.create_execution_context()
170
+ if self._context is None:
171
+ raise RuntimeError("TensorRT execution context creation failed")
172
+
173
+ @staticmethod
174
+ def _dtype_name(dtype: Any, *, np: Any, trt: Any) -> str:
175
+ try:
176
+ value = np.dtype(trt.nptype(dtype))
177
+ except (TypeError, ValueError, AttributeError) as exc:
178
+ raise RuntimeError(f"unsupported TensorRT tensor dtype {dtype!r}") from exc
179
+ names = {"float32": "float", "float16": "float16", "float64": "double", "int8": "int8", "int16": "int16", "int32": "int32", "int64": "int64", "uint8": "uint8", "uint16": "uint16", "uint32": "uint32", "uint64": "uint64", "bool": "bool"}
180
+ try:
181
+ return f"tensor({names[value.name]})"
182
+ except KeyError as exc:
183
+ raise RuntimeError(f"unsupported TensorRT tensor dtype {value}") from exc
184
+
185
+ def _tensor_shape(self, name: str) -> list[int | None]:
186
+ return [None if int(dimension) < 0 else int(dimension) for dimension in self._engine.get_tensor_shape(name)]
187
+
188
+ def contract(self) -> dict[str, list[dict[str, Any]]]:
189
+ inputs: list[dict[str, Any]] = []
190
+ outputs: list[dict[str, Any]] = []
191
+ for index in range(self._engine.num_io_tensors):
192
+ name = self._engine.get_tensor_name(index)
193
+ value = {"name": name, "dtype": self._dtype_name(self._engine.get_tensor_dtype(name), np=self._np, trt=self._trt), "shape": self._tensor_shape(name)}
194
+ target = inputs if self._engine.get_tensor_mode(name) == self._trt.TensorIOMode.INPUT else outputs
195
+ target.append(value)
196
+ return {"artifact_format": "tensorrt-engine", "inputs": inputs, "outputs": outputs}
197
+
198
+ def optimization_profiles(self) -> list[dict[str, Any]]:
199
+ """Return engine profile bounds without inventing a manifest schema."""
200
+ count = int(getattr(self._engine, "num_optimization_profiles", 0) or 0)
201
+ profiles: list[dict[str, Any]] = []
202
+ for profile_index in range(count):
203
+ tensors: dict[str, dict[str, list[int]]] = {}
204
+ for index in range(self._engine.num_io_tensors):
205
+ name = self._engine.get_tensor_name(index)
206
+ if self._engine.get_tensor_mode(name) != self._trt.TensorIOMode.INPUT:
207
+ continue
208
+ try:
209
+ minimum, optimum, maximum = self._engine.get_tensor_profile_shape(name, profile_index)
210
+ except (AttributeError, RuntimeError):
211
+ continue
212
+ tensors[name] = {"min": list(minimum), "opt": list(optimum), "max": list(maximum)}
213
+ profiles.append({"index": profile_index, "inputs": tensors})
214
+ return profiles
215
+
216
+ def runtime_info(self) -> dict[str, Any]:
217
+ return {"runtime": "tensorrt", "tensorrt_version": getattr(self._trt, "__version__", None), "cuda_binding": getattr(self._cuda, "binding_name", type(self._cuda).__name__), "optimization_profiles": self.optimization_profiles(), "python": platform.python_version(), "system": platform.system(), "machine": platform.machine()}
218
+
219
+ def _names(self, mode: Any) -> list[str]:
220
+ return [self._engine.get_tensor_name(index) for index in range(self._engine.num_io_tensors) if self._engine.get_tensor_mode(self._engine.get_tensor_name(index)) == mode]
221
+
222
+ def run(self, inputs: Mapping[str, Any]) -> dict[str, Any]:
223
+ expected_inputs, expected_outputs = self._names(self._trt.TensorIOMode.INPUT), self._names(self._trt.TensorIOMode.OUTPUT)
224
+ missing, extra = [name for name in expected_inputs if name not in inputs], [name for name in inputs if name not in expected_inputs]
225
+ if missing or extra:
226
+ raise ValueError(f"TensorRT inputs do not match engine (missing={missing}, extra={extra})")
227
+ prepared: dict[str, Any] = {}
228
+ for name in expected_inputs:
229
+ value = self._np.asarray(inputs[name])
230
+ expected_dtype = self._np.dtype(self._trt.nptype(self._engine.get_tensor_dtype(name)))
231
+ if value.dtype != expected_dtype:
232
+ raise TypeError(f"input {name!r} has dtype {value.dtype}, expected {expected_dtype}")
233
+ if not value.flags.c_contiguous:
234
+ raise ValueError(f"input {name!r} must be C-contiguous")
235
+ engine_shape = self._engine.get_tensor_shape(name)
236
+ if len(value.shape) != len(engine_shape) or any(int(want) >= 0 and int(want) != got for want, got in zip(engine_shape, value.shape)):
237
+ raise ValueError(f"input {name!r} shape {tuple(value.shape)} does not satisfy engine shape {tuple(engine_shape)}")
238
+ prepared[name] = value
239
+ if self._context.set_input_shape(name, tuple(value.shape)) is False:
240
+ raise ValueError(f"TensorRT rejected input shape for {name!r}: {tuple(value.shape)}")
241
+
242
+ allocations: list[tuple[str, int, Any]] = []
243
+ stream: Any = None
244
+ synchronized, active_error = False, None
245
+ try:
246
+ stream = _check_cuda(self._cuda.cudaStreamCreate(), "cudaStreamCreate")
247
+ if stream is None:
248
+ raise RuntimeError("cudaStreamCreate returned a null stream")
249
+ for name in expected_inputs + expected_outputs:
250
+ if name in prepared:
251
+ value = prepared[name]
252
+ else:
253
+ shape = tuple(int(dimension) for dimension in self._context.get_tensor_shape(name))
254
+ if any(dimension < 0 for dimension in shape):
255
+ raise ValueError(f"TensorRT output {name!r} has unresolved dynamic shape {shape}")
256
+ value = self._np.empty(shape, dtype=self._np.dtype(self._trt.nptype(self._engine.get_tensor_dtype(name))))
257
+ if value.nbytes <= 0:
258
+ raise ValueError(f"TensorRT tensor {name!r} has an empty buffer")
259
+ device = _check_cuda(self._cuda.cudaMalloc(value.nbytes), f"cudaMalloc({name})")
260
+ if not device:
261
+ raise RuntimeError(f"cudaMalloc({name}) returned a null pointer")
262
+ allocations.append((name, int(device), value))
263
+ self._context.set_tensor_address(name, int(device))
264
+ if name in prepared:
265
+ _check_cuda(self._cuda.cudaMemcpyAsync(int(device), int(value.ctypes.data), value.nbytes, self._cuda.cudaMemcpyKind.cudaMemcpyHostToDevice, stream), f"cudaMemcpyAsync(HtoD:{name})")
266
+ if self._context.execute_async_v3(stream_handle=stream) is False:
267
+ raise RuntimeError("TensorRT execution failed")
268
+ for name, device, value in allocations:
269
+ if name not in expected_outputs:
270
+ continue
271
+ _check_cuda(self._cuda.cudaMemcpyAsync(int(value.ctypes.data), device, value.nbytes, self._cuda.cudaMemcpyKind.cudaMemcpyDeviceToHost, stream), f"cudaMemcpyAsync(DtoH:{name})")
272
+ _check_cuda(self._cuda.cudaStreamSynchronize(stream), "cudaStreamSynchronize")
273
+ synchronized = True
274
+ return {name: value.tolist() for name, _, value in allocations if name in expected_outputs}
275
+ except BaseException as exc:
276
+ active_error = exc
277
+ raise
278
+ finally:
279
+ cleanup_errors: list[str] = []
280
+ if stream is not None and not synchronized:
281
+ try:
282
+ _check_cuda(self._cuda.cudaStreamSynchronize(stream), "cudaStreamSynchronize(cleanup)")
283
+ except Exception as exc:
284
+ cleanup_errors.append(str(exc))
285
+ for name, device, _ in reversed(allocations):
286
+ try:
287
+ _check_cuda(self._cuda.cudaFree(device), f"cudaFree({name})")
288
+ except Exception as exc:
289
+ cleanup_errors.append(str(exc))
290
+ if stream is not None:
291
+ try:
292
+ _check_cuda(self._cuda.cudaStreamDestroy(stream), "cudaStreamDestroy")
293
+ except Exception as exc:
294
+ cleanup_errors.append(str(exc))
295
+ if active_error is None and cleanup_errors:
296
+ raise RuntimeError("TensorRT CUDA cleanup failed: " + "; ".join(cleanup_errors))