tensoracle 0.3.1__py3-none-any.whl
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.
- tensoracle/__init__.py +52 -0
- tensoracle/_build.py +137 -0
- tensoracle/_cache.py +359 -0
- tensoracle/_comparison.py +117 -0
- tensoracle/_execution.py +184 -0
- tensoracle/_memory.py +18 -0
- tensoracle/_plugins.py +223 -0
- tensoracle/_version.py +24 -0
- tensoracle/bundle.py +206 -0
- tensoracle/convert.py +71 -0
- tensoracle/cpp/cmake/TensoracleCubinPlugin.cmake +29 -0
- tensoracle/cpp/include/tensoracle/cubin_plugin.hpp +156 -0
- tensoracle/cpp/src/cubin_plugin.cpp +376 -0
- tensoracle/producer.py +267 -0
- tensoracle-0.3.1.dist-info/METADATA +329 -0
- tensoracle-0.3.1.dist-info/RECORD +18 -0
- tensoracle-0.3.1.dist-info/WHEEL +4 -0
- tensoracle-0.3.1.dist-info/licenses/LICENSE +21 -0
tensoracle/__init__.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Build, store, reuse, and execute TensorRT engines from safetensors weights."""
|
|
2
|
+
|
|
3
|
+
from importlib import import_module
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"BuilderSettings",
|
|
7
|
+
"CorruptEngineError",
|
|
8
|
+
"EngineBuild",
|
|
9
|
+
"EngineCacheError",
|
|
10
|
+
"EngineConditionError",
|
|
11
|
+
"EngineExecution",
|
|
12
|
+
"EngineHandle",
|
|
13
|
+
"EngineIO",
|
|
14
|
+
"EngineIOError",
|
|
15
|
+
"InputShape",
|
|
16
|
+
"OutputComparison",
|
|
17
|
+
"PluginLibraryError",
|
|
18
|
+
"TensorSpec",
|
|
19
|
+
"build_serialized_engine",
|
|
20
|
+
"compare_outputs",
|
|
21
|
+
"open_engine",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
_MODULES = {
|
|
25
|
+
**dict.fromkeys(
|
|
26
|
+
("BuilderSettings", "EngineBuild", "InputShape", "build_serialized_engine"), "._build"
|
|
27
|
+
),
|
|
28
|
+
**dict.fromkeys(
|
|
29
|
+
(
|
|
30
|
+
"CorruptEngineError",
|
|
31
|
+
"EngineCacheError",
|
|
32
|
+
"EngineConditionError",
|
|
33
|
+
"EngineHandle",
|
|
34
|
+
"EngineIO",
|
|
35
|
+
"EngineIOError",
|
|
36
|
+
"TensorSpec",
|
|
37
|
+
"open_engine",
|
|
38
|
+
),
|
|
39
|
+
"._cache",
|
|
40
|
+
),
|
|
41
|
+
**dict.fromkeys(("OutputComparison", "compare_outputs"), "._comparison"),
|
|
42
|
+
"PluginLibraryError": "._plugins",
|
|
43
|
+
"EngineExecution": "._execution",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def __getattr__(name: str):
|
|
48
|
+
if name not in _MODULES:
|
|
49
|
+
raise AttributeError(name)
|
|
50
|
+
value = getattr(import_module(_MODULES[name], __name__), name)
|
|
51
|
+
globals()[name] = value
|
|
52
|
+
return value
|
tensoracle/_build.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Build serialized TensorRT engines from caller-defined network graphs."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
import tensorrt as trt
|
|
9
|
+
|
|
10
|
+
from ._memory import release_host_memory
|
|
11
|
+
from ._plugins import register_libraries
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class InputShape:
|
|
16
|
+
"""The minimum, preferred, and maximum dimensions of one input."""
|
|
17
|
+
|
|
18
|
+
minimum: tuple[int, ...]
|
|
19
|
+
optimum: tuple[int, ...]
|
|
20
|
+
maximum: tuple[int, ...]
|
|
21
|
+
|
|
22
|
+
def __post_init__(self) -> None:
|
|
23
|
+
if (
|
|
24
|
+
not self.minimum
|
|
25
|
+
or len(self.minimum) != len(self.optimum)
|
|
26
|
+
or len(self.minimum) != len(self.maximum)
|
|
27
|
+
):
|
|
28
|
+
raise ValueError("input shapes must have the same nonzero rank")
|
|
29
|
+
if any(
|
|
30
|
+
not isinstance(low, int)
|
|
31
|
+
or not isinstance(mid, int)
|
|
32
|
+
or not isinstance(high, int)
|
|
33
|
+
or not 0 < low <= mid <= high
|
|
34
|
+
for low, mid, high in zip(self.minimum, self.optimum, self.maximum, strict=True)
|
|
35
|
+
):
|
|
36
|
+
raise ValueError(
|
|
37
|
+
"input dimensions must be positive and ordered minimum <= optimum <= maximum"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def fixed(cls, shape: tuple[int, ...]) -> "InputShape":
|
|
42
|
+
"""Declare one fixed input shape."""
|
|
43
|
+
return cls(shape, shape, shape)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def ranged(
|
|
47
|
+
cls, minimum: tuple[int, ...], optimum: tuple[int, ...], maximum: tuple[int, ...]
|
|
48
|
+
) -> "InputShape":
|
|
49
|
+
"""Declare the three shapes of one TensorRT optimization profile."""
|
|
50
|
+
return cls(minimum, optimum, maximum)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class BuilderSettings:
|
|
55
|
+
"""TensorRT builder options that affect the serialized engine."""
|
|
56
|
+
|
|
57
|
+
workspace_limit: int = 1 << 30
|
|
58
|
+
optimization_level: int = 3
|
|
59
|
+
tf32: bool = False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True)
|
|
63
|
+
class EngineBuild:
|
|
64
|
+
"""One engine's precision, named input shapes, and builder settings."""
|
|
65
|
+
|
|
66
|
+
precision: Literal["fp32", "fp16"]
|
|
67
|
+
inputs: Mapping[str, InputShape]
|
|
68
|
+
settings: BuilderSettings = field(default_factory=BuilderSettings)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def build_serialized_engine(
|
|
72
|
+
graph: Callable[[trt.INetworkDefinition], None],
|
|
73
|
+
declaration: EngineBuild,
|
|
74
|
+
*,
|
|
75
|
+
plugins: Sequence[Path] = (),
|
|
76
|
+
logger: trt.ILogger | None = None,
|
|
77
|
+
) -> bytes:
|
|
78
|
+
"""Register ``plugins`` before calling ``graph`` and serializing its engine."""
|
|
79
|
+
if declaration.precision not in ("fp32", "fp16"):
|
|
80
|
+
raise ValueError(f"unsupported precision: {declaration.precision}")
|
|
81
|
+
if declaration.settings.tf32:
|
|
82
|
+
raise ValueError("TF32 cannot be enabled")
|
|
83
|
+
if declaration.settings.workspace_limit <= 0:
|
|
84
|
+
raise ValueError("workspace_limit must be positive")
|
|
85
|
+
if not 0 <= declaration.settings.optimization_level <= 5:
|
|
86
|
+
raise ValueError("optimization_level must be between 0 and 5")
|
|
87
|
+
|
|
88
|
+
register_libraries(plugins)
|
|
89
|
+
|
|
90
|
+
builder = network = config = profile = serialized = tensor = None
|
|
91
|
+
inputs = {}
|
|
92
|
+
try:
|
|
93
|
+
builder = trt.Builder(logger or trt.Logger(trt.Logger.ERROR))
|
|
94
|
+
network = builder.create_network(0)
|
|
95
|
+
graph(network)
|
|
96
|
+
config = builder.create_builder_config()
|
|
97
|
+
config.clear_flag(trt.BuilderFlag.TF32)
|
|
98
|
+
config.set_memory_pool_limit(
|
|
99
|
+
trt.MemoryPoolType.WORKSPACE, declaration.settings.workspace_limit
|
|
100
|
+
)
|
|
101
|
+
config.builder_optimization_level = declaration.settings.optimization_level
|
|
102
|
+
|
|
103
|
+
inputs = {
|
|
104
|
+
network.get_input(index).name: network.get_input(index)
|
|
105
|
+
for index in range(network.num_inputs)
|
|
106
|
+
}
|
|
107
|
+
if inputs.keys() != declaration.inputs.keys():
|
|
108
|
+
raise ValueError("declared input names must match the graph inputs")
|
|
109
|
+
|
|
110
|
+
for name, tensor in inputs.items():
|
|
111
|
+
shape = declaration.inputs[name]
|
|
112
|
+
network_shape = tuple(tensor.shape)
|
|
113
|
+
if len(network_shape) != len(shape.minimum):
|
|
114
|
+
raise ValueError(f"declared rank does not match graph input {name}")
|
|
115
|
+
for dimension, low, mid, high in zip(
|
|
116
|
+
network_shape, shape.minimum, shape.optimum, shape.maximum, strict=True
|
|
117
|
+
):
|
|
118
|
+
if dimension != -1 and (dimension != low or dimension != mid or dimension != high):
|
|
119
|
+
raise ValueError(f"declared dimensions do not match graph input {name}")
|
|
120
|
+
if -1 in network_shape:
|
|
121
|
+
if profile is None:
|
|
122
|
+
profile = builder.create_optimization_profile()
|
|
123
|
+
profile.set_shape(name, shape.minimum, shape.optimum, shape.maximum)
|
|
124
|
+
if not profile:
|
|
125
|
+
raise ValueError(f"TensorRT rejected the shape range for graph input {name}")
|
|
126
|
+
if profile is not None:
|
|
127
|
+
config.add_optimization_profile(profile)
|
|
128
|
+
|
|
129
|
+
serialized = builder.build_serialized_network(network, config)
|
|
130
|
+
if serialized is None:
|
|
131
|
+
raise RuntimeError("TensorRT failed to build the serialized engine")
|
|
132
|
+
return bytes(serialized)
|
|
133
|
+
finally:
|
|
134
|
+
# Tensor references also keep the network alive, including in exception frames.
|
|
135
|
+
inputs.clear()
|
|
136
|
+
del tensor, profile, serialized, config, network, builder
|
|
137
|
+
release_host_memory()
|
tensoracle/_cache.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""Open TensorRT engines from the user cache."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import ctypes.util
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import platform
|
|
9
|
+
import tempfile
|
|
10
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
11
|
+
from dataclasses import asdict, dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
import tensorrt as trt
|
|
16
|
+
|
|
17
|
+
from ._build import EngineBuild, build_serialized_engine
|
|
18
|
+
from ._plugins import register_libraries
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from ._execution import EngineExecution
|
|
22
|
+
|
|
23
|
+
_MAGIC = b"TENSORACLE\x01"
|
|
24
|
+
_WEIGHT_RECORD_VERSION = 1
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class TensorSpec:
|
|
29
|
+
"""Expected dtype, build-time dimensions, and optional location and format."""
|
|
30
|
+
|
|
31
|
+
dtype: trt.DataType
|
|
32
|
+
shape: tuple[int, ...]
|
|
33
|
+
location: trt.TensorLocation | None = None
|
|
34
|
+
format: trt.TensorFormat | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class EngineIO:
|
|
39
|
+
"""Expected named inputs and outputs of a TensorRT engine."""
|
|
40
|
+
|
|
41
|
+
inputs: Mapping[str, TensorSpec]
|
|
42
|
+
outputs: Mapping[str, TensorSpec]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class EngineHandle:
|
|
47
|
+
"""Keep the deserialized engine and its owning runtime alive together."""
|
|
48
|
+
|
|
49
|
+
engine: trt.ICudaEngine
|
|
50
|
+
runtime: trt.Runtime
|
|
51
|
+
path: Path
|
|
52
|
+
|
|
53
|
+
def create_execution(self) -> "EngineExecution":
|
|
54
|
+
"""Create an independent execution context with explicit close semantics."""
|
|
55
|
+
from ._execution import EngineExecution
|
|
56
|
+
|
|
57
|
+
return EngineExecution(self)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class EngineCacheError(ValueError):
|
|
61
|
+
"""A stored engine cannot be used for this declaration."""
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CorruptEngineError(EngineCacheError):
|
|
65
|
+
"""A stored engine is incomplete or corrupt."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class EngineConditionError(EngineCacheError):
|
|
69
|
+
"""A stored engine belongs to different build conditions."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class EngineIOError(EngineCacheError):
|
|
73
|
+
"""The engine's input or output signature differs from the declaration."""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def open_engine(
|
|
77
|
+
graph: Callable[[trt.INetworkDefinition], None],
|
|
78
|
+
declaration: EngineBuild,
|
|
79
|
+
*,
|
|
80
|
+
name: str,
|
|
81
|
+
graph_version: str,
|
|
82
|
+
weights: Sequence[Path],
|
|
83
|
+
io: EngineIO,
|
|
84
|
+
plugins: Sequence[Path] = (),
|
|
85
|
+
force_rebuild: bool = False,
|
|
86
|
+
logger: trt.ILogger | None = None,
|
|
87
|
+
) -> EngineHandle:
|
|
88
|
+
"""Register ``plugins`` and build or load an engine for the supplied conditions."""
|
|
89
|
+
registered_digests = register_libraries(plugins)
|
|
90
|
+
conditions = _conditions(
|
|
91
|
+
name, graph_version, weights, plugins, declaration, force_rebuild=force_rebuild
|
|
92
|
+
)
|
|
93
|
+
if tuple(conditions["plugins"]) != registered_digests:
|
|
94
|
+
raise ValueError("plugin library changed while determining engine conditions")
|
|
95
|
+
digest = hashlib.sha256(_json_bytes(conditions)).hexdigest()
|
|
96
|
+
path = _cache_directory() / f"{digest}.engine"
|
|
97
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
98
|
+
actual_logger = logger or trt.Logger(trt.Logger.ERROR)
|
|
99
|
+
|
|
100
|
+
if force_rebuild:
|
|
101
|
+
path.write_bytes(b"")
|
|
102
|
+
else:
|
|
103
|
+
try:
|
|
104
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
105
|
+
except FileExistsError:
|
|
106
|
+
plan = _read_plan(path, conditions)
|
|
107
|
+
return _open_plan(plan, path, declaration, io, actual_logger)
|
|
108
|
+
else:
|
|
109
|
+
os.close(fd)
|
|
110
|
+
|
|
111
|
+
temporary: Path | None = None
|
|
112
|
+
try:
|
|
113
|
+
plan = build_serialized_engine(graph, declaration, plugins=plugins, logger=actual_logger)
|
|
114
|
+
handle = _open_plan(plan, path, declaration, io, actual_logger)
|
|
115
|
+
header = {"conditions": conditions, "plan_sha256": hashlib.sha256(plan).hexdigest()}
|
|
116
|
+
header_bytes = _json_bytes(header)
|
|
117
|
+
fd, filename = tempfile.mkstemp(prefix=f".{digest}.", dir=path.parent)
|
|
118
|
+
temporary = Path(filename)
|
|
119
|
+
with os.fdopen(fd, "wb") as stream:
|
|
120
|
+
stream.write(_MAGIC + len(header_bytes).to_bytes(8, "big") + header_bytes)
|
|
121
|
+
stream.write(plan)
|
|
122
|
+
stream.flush()
|
|
123
|
+
os.fsync(stream.fileno())
|
|
124
|
+
os.replace(temporary, path)
|
|
125
|
+
return handle
|
|
126
|
+
except BaseException:
|
|
127
|
+
if temporary is not None:
|
|
128
|
+
temporary.unlink(missing_ok=True)
|
|
129
|
+
path.unlink(missing_ok=True)
|
|
130
|
+
raise
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _json_bytes(value: object) -> bytes:
|
|
134
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _file_sha256(path: Path) -> str:
|
|
138
|
+
digest = hashlib.sha256()
|
|
139
|
+
with path.open("rb") as stream:
|
|
140
|
+
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
|
141
|
+
digest.update(chunk)
|
|
142
|
+
return digest.hexdigest()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _cache_directory() -> Path:
|
|
146
|
+
cache_root = Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache")
|
|
147
|
+
return cache_root / "tensoracle"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _weight_sha256(path: Path, *, force_rebuild: bool) -> str:
|
|
151
|
+
absolute_path = Path(os.path.abspath(path))
|
|
152
|
+
before = absolute_path.stat()
|
|
153
|
+
identity = {
|
|
154
|
+
"path": str(absolute_path),
|
|
155
|
+
"size": before.st_size,
|
|
156
|
+
"mtime_ns": before.st_mtime_ns,
|
|
157
|
+
"inode": before.st_ino,
|
|
158
|
+
}
|
|
159
|
+
path_digest = hashlib.sha256(os.fsencode(absolute_path)).hexdigest()
|
|
160
|
+
record_path = _cache_directory() / f"weight-{path_digest}.json"
|
|
161
|
+
|
|
162
|
+
if not force_rebuild:
|
|
163
|
+
try:
|
|
164
|
+
record = json.loads(record_path.read_bytes())
|
|
165
|
+
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
|
|
166
|
+
record = None
|
|
167
|
+
if (
|
|
168
|
+
isinstance(record, dict)
|
|
169
|
+
and record.keys() == identity.keys() | {"version", "sha256", "checksum"}
|
|
170
|
+
and record["version"] == _WEIGHT_RECORD_VERSION
|
|
171
|
+
and type(record["version"]) is int
|
|
172
|
+
and all(
|
|
173
|
+
type(record[key]) is type(value) and record[key] == value
|
|
174
|
+
for key, value in identity.items()
|
|
175
|
+
)
|
|
176
|
+
and isinstance(record["sha256"], str)
|
|
177
|
+
and len(record["sha256"]) == 64
|
|
178
|
+
and all(character in "0123456789abcdef" for character in record["sha256"])
|
|
179
|
+
and record["checksum"]
|
|
180
|
+
== hashlib.sha256(
|
|
181
|
+
_json_bytes({key: value for key, value in record.items() if key != "checksum"})
|
|
182
|
+
).hexdigest()
|
|
183
|
+
):
|
|
184
|
+
return record["sha256"]
|
|
185
|
+
|
|
186
|
+
digest = _file_sha256(absolute_path)
|
|
187
|
+
try:
|
|
188
|
+
after = absolute_path.stat()
|
|
189
|
+
except OSError:
|
|
190
|
+
return digest
|
|
191
|
+
if (before.st_size, before.st_mtime_ns, before.st_ino) == (
|
|
192
|
+
after.st_size,
|
|
193
|
+
after.st_mtime_ns,
|
|
194
|
+
after.st_ino,
|
|
195
|
+
):
|
|
196
|
+
payload = {"version": _WEIGHT_RECORD_VERSION, **identity, "sha256": digest}
|
|
197
|
+
_write_weight_record(
|
|
198
|
+
record_path, {**payload, "checksum": hashlib.sha256(_json_bytes(payload)).hexdigest()}
|
|
199
|
+
)
|
|
200
|
+
return digest
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _write_weight_record(path: Path, record: dict) -> None:
|
|
204
|
+
temporary: Path | None = None
|
|
205
|
+
try:
|
|
206
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
207
|
+
fd, filename = tempfile.mkstemp(prefix=f".{path.stem}.", dir=path.parent)
|
|
208
|
+
temporary = Path(filename)
|
|
209
|
+
with os.fdopen(fd, "wb") as stream:
|
|
210
|
+
stream.write(_json_bytes(record))
|
|
211
|
+
stream.flush()
|
|
212
|
+
os.fsync(stream.fileno())
|
|
213
|
+
os.replace(temporary, path)
|
|
214
|
+
except OSError:
|
|
215
|
+
pass
|
|
216
|
+
finally:
|
|
217
|
+
if temporary is not None:
|
|
218
|
+
try:
|
|
219
|
+
temporary.unlink(missing_ok=True)
|
|
220
|
+
except OSError:
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _gpu_name() -> str:
|
|
225
|
+
library_name = ctypes.util.find_library("cuda") or ctypes.util.find_library("nvcuda")
|
|
226
|
+
if library_name is None:
|
|
227
|
+
raise RuntimeError("CUDA driver library was not found")
|
|
228
|
+
cuda = ctypes.CDLL(library_name)
|
|
229
|
+
cuda.cuInit.argtypes = [ctypes.c_uint]
|
|
230
|
+
cuda.cuCtxGetDevice.argtypes = [ctypes.POINTER(ctypes.c_int)]
|
|
231
|
+
cuda.cuDeviceGet.argtypes = [ctypes.POINTER(ctypes.c_int), ctypes.c_int]
|
|
232
|
+
cuda.cuDeviceGetName.argtypes = [ctypes.c_char_p, ctypes.c_int, ctypes.c_int]
|
|
233
|
+
if cuda.cuInit(0) != 0:
|
|
234
|
+
raise RuntimeError("CUDA driver initialization failed")
|
|
235
|
+
device = ctypes.c_int()
|
|
236
|
+
if (
|
|
237
|
+
cuda.cuCtxGetDevice(ctypes.byref(device)) != 0
|
|
238
|
+
and cuda.cuDeviceGet(ctypes.byref(device), 0) != 0
|
|
239
|
+
):
|
|
240
|
+
raise RuntimeError("CUDA device selection failed")
|
|
241
|
+
name = ctypes.create_string_buffer(256)
|
|
242
|
+
if cuda.cuDeviceGetName(name, len(name), device.value) != 0:
|
|
243
|
+
raise RuntimeError("CUDA device name lookup failed")
|
|
244
|
+
return name.value.decode("utf-8")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _conditions(
|
|
248
|
+
name: str,
|
|
249
|
+
graph_version: str,
|
|
250
|
+
weights: Sequence[Path],
|
|
251
|
+
plugins: Sequence[Path],
|
|
252
|
+
declaration: EngineBuild,
|
|
253
|
+
*,
|
|
254
|
+
force_rebuild: bool = False,
|
|
255
|
+
) -> dict:
|
|
256
|
+
if not name or not graph_version or not weights:
|
|
257
|
+
raise ValueError("engine name, graph version, and at least one weight file are required")
|
|
258
|
+
if declaration.settings.tf32:
|
|
259
|
+
raise ValueError("TF32 cannot be enabled")
|
|
260
|
+
return {
|
|
261
|
+
"name": name,
|
|
262
|
+
"graph_version": graph_version,
|
|
263
|
+
"weights": [_weight_sha256(Path(path), force_rebuild=force_rebuild) for path in weights],
|
|
264
|
+
"plugins": [_file_sha256(Path(path)) for path in plugins],
|
|
265
|
+
"precision": declaration.precision,
|
|
266
|
+
"inputs": {
|
|
267
|
+
name: {
|
|
268
|
+
"minimum": list(shape.minimum),
|
|
269
|
+
"optimum": list(shape.optimum),
|
|
270
|
+
"maximum": list(shape.maximum),
|
|
271
|
+
}
|
|
272
|
+
for name, shape in declaration.inputs.items()
|
|
273
|
+
},
|
|
274
|
+
"builder_settings": asdict(declaration.settings),
|
|
275
|
+
"gpu_name": _gpu_name(),
|
|
276
|
+
"os": platform.system(),
|
|
277
|
+
"architecture": platform.machine(),
|
|
278
|
+
"tensorrt_version": trt.__version__,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _read_plan(path: Path, conditions: dict) -> memoryview:
|
|
283
|
+
data = path.read_bytes()
|
|
284
|
+
offset = len(_MAGIC)
|
|
285
|
+
if not data.startswith(_MAGIC) or len(data) < offset + 8:
|
|
286
|
+
raise CorruptEngineError(f"incomplete TensorRT engine file: {path}")
|
|
287
|
+
header_size = int.from_bytes(data[offset : offset + 8], "big")
|
|
288
|
+
header_end = offset + 8 + header_size
|
|
289
|
+
if header_size == 0 or header_end >= len(data):
|
|
290
|
+
raise CorruptEngineError(f"invalid TensorRT engine header: {path}")
|
|
291
|
+
try:
|
|
292
|
+
header = json.loads(data[offset + 8 : header_end])
|
|
293
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as error:
|
|
294
|
+
raise CorruptEngineError(f"invalid TensorRT engine header: {path}") from error
|
|
295
|
+
if not isinstance(header, dict) or not isinstance(header.get("conditions"), dict):
|
|
296
|
+
raise CorruptEngineError(f"invalid TensorRT engine header: {path}")
|
|
297
|
+
if header["conditions"] != conditions:
|
|
298
|
+
raise EngineConditionError(f"TensorRT engine conditions differ: {path}")
|
|
299
|
+
plan = memoryview(data)[header_end:]
|
|
300
|
+
if header.get("plan_sha256") != hashlib.sha256(plan).hexdigest():
|
|
301
|
+
raise CorruptEngineError(f"TensorRT engine checksum differs: {path}")
|
|
302
|
+
return plan
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _open_plan(
|
|
306
|
+
plan: bytes | memoryview,
|
|
307
|
+
path: Path,
|
|
308
|
+
declaration: EngineBuild,
|
|
309
|
+
io: EngineIO,
|
|
310
|
+
logger: trt.ILogger,
|
|
311
|
+
) -> EngineHandle:
|
|
312
|
+
runtime = trt.Runtime(logger)
|
|
313
|
+
engine = runtime.deserialize_cuda_engine(plan)
|
|
314
|
+
if engine is None:
|
|
315
|
+
raise CorruptEngineError(
|
|
316
|
+
f"TensorRT could not deserialize engine: {path}. Check that plugins= includes the "
|
|
317
|
+
"libraries providing the plugin creators (name, version, namespace) used by this engine."
|
|
318
|
+
)
|
|
319
|
+
names = {engine.get_tensor_name(index) for index in range(engine.num_io_tensors)}
|
|
320
|
+
expected_names = io.inputs.keys() | io.outputs.keys()
|
|
321
|
+
for name in sorted(names ^ expected_names):
|
|
322
|
+
_io_mismatch(path, name, "name", sorted(names), sorted(expected_names))
|
|
323
|
+
for specs, mode in ((io.inputs, trt.TensorIOMode.INPUT), (io.outputs, trt.TensorIOMode.OUTPUT)):
|
|
324
|
+
for name, expected in specs.items():
|
|
325
|
+
for field, observed, value in (
|
|
326
|
+
("mode", engine.get_tensor_mode(name), mode),
|
|
327
|
+
("dtype", engine.get_tensor_dtype(name), expected.dtype),
|
|
328
|
+
("shape", tuple(engine.get_tensor_shape(name)), expected.shape),
|
|
329
|
+
):
|
|
330
|
+
if observed != value:
|
|
331
|
+
_io_mismatch(path, name, field, observed, value)
|
|
332
|
+
if expected.location is not None:
|
|
333
|
+
observed = engine.get_tensor_location(name)
|
|
334
|
+
if observed != expected.location:
|
|
335
|
+
_io_mismatch(path, name, "location", observed, expected.location)
|
|
336
|
+
if expected.format is not None:
|
|
337
|
+
observed = engine.get_tensor_format(name)
|
|
338
|
+
if observed != expected.format:
|
|
339
|
+
_io_mismatch(path, name, "format", observed, expected.format)
|
|
340
|
+
for name in sorted(declaration.inputs.keys() ^ io.inputs.keys()):
|
|
341
|
+
_io_mismatch(
|
|
342
|
+
path, name, "name (build input)", sorted(io.inputs), sorted(declaration.inputs)
|
|
343
|
+
)
|
|
344
|
+
for name, shape in declaration.inputs.items():
|
|
345
|
+
expected_range = (shape.minimum, shape.optimum, shape.maximum)
|
|
346
|
+
if -1 in io.inputs[name].shape:
|
|
347
|
+
actual_range = tuple(tuple(part) for part in engine.get_tensor_profile_shape(name, 0))
|
|
348
|
+
else:
|
|
349
|
+
actual_range = (io.inputs[name].shape,) * 3
|
|
350
|
+
if actual_range != expected_range:
|
|
351
|
+
_io_mismatch(path, name, "profile range", actual_range, expected_range)
|
|
352
|
+
return EngineHandle(engine, runtime, path)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _io_mismatch(path: Path, name: str, field: str, observed: object, expected: object) -> None:
|
|
356
|
+
raise EngineIOError(
|
|
357
|
+
f"TensorRT engine tensor {name!r} {field} differs: "
|
|
358
|
+
f"observed={observed!r}, expected={expected!r}; path={path}"
|
|
359
|
+
)
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Model-independent measurements for one pair of CPU output arrays."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class OutputComparison:
|
|
10
|
+
"""Measurements for a candidate output against its reference output.
|
|
11
|
+
|
|
12
|
+
Float measurements are ``None`` for integer or boolean arrays and when
|
|
13
|
+
either float array contains a non-finite value. Tolerance measurements are
|
|
14
|
+
also ``None`` when the caller has not supplied both tolerances. Mismatch
|
|
15
|
+
measurements apply only to integer and boolean arrays.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
candidate_nonfinite_count: int
|
|
19
|
+
reference_nonfinite_count: int
|
|
20
|
+
max_absolute_error: float | None = None
|
|
21
|
+
relative_l2_error: float | None = None
|
|
22
|
+
cosine_similarity: float | None = None
|
|
23
|
+
outside_tolerance_count: int | None = None
|
|
24
|
+
outside_tolerance_fraction: float | None = None
|
|
25
|
+
mismatch_count: int | None = None
|
|
26
|
+
mismatch_fraction: float | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def compare_outputs(
|
|
30
|
+
candidate: np.ndarray,
|
|
31
|
+
reference: np.ndarray,
|
|
32
|
+
*,
|
|
33
|
+
atol: float | None = None,
|
|
34
|
+
rtol: float | None = None,
|
|
35
|
+
) -> OutputComparison:
|
|
36
|
+
"""Measure one candidate output against the corresponding reference output.
|
|
37
|
+
|
|
38
|
+
Both inputs must be nonempty NumPy arrays of equal shape and the same kind:
|
|
39
|
+
floating point, integer, or boolean. Floating point inputs are converted
|
|
40
|
+
to float64 before arithmetic. Tolerances must be finite, nonnegative, and
|
|
41
|
+
supplied together. This function reports measurements; the caller chooses
|
|
42
|
+
the limits that constitute an acceptable output.
|
|
43
|
+
"""
|
|
44
|
+
if not isinstance(candidate, np.ndarray) or not isinstance(reference, np.ndarray):
|
|
45
|
+
raise TypeError("candidate and reference must be NumPy arrays")
|
|
46
|
+
if candidate.shape != reference.shape or candidate.size == 0:
|
|
47
|
+
raise ValueError("candidate and reference must have the same nonempty shape")
|
|
48
|
+
|
|
49
|
+
candidate_kind = candidate.dtype.kind
|
|
50
|
+
reference_kind = reference.dtype.kind
|
|
51
|
+
floats = candidate_kind == reference_kind == "f"
|
|
52
|
+
integers = candidate_kind in "iu" and reference_kind in "iu"
|
|
53
|
+
booleans = candidate_kind == reference_kind == "b"
|
|
54
|
+
if not (floats or integers or booleans):
|
|
55
|
+
raise ValueError("candidate and reference must have the same numeric kind")
|
|
56
|
+
|
|
57
|
+
if (atol is None) != (rtol is None):
|
|
58
|
+
raise ValueError("atol and rtol must be supplied together")
|
|
59
|
+
if atol is not None and rtol is not None:
|
|
60
|
+
try:
|
|
61
|
+
atol = float(atol)
|
|
62
|
+
rtol = float(rtol)
|
|
63
|
+
except (TypeError, ValueError) as error:
|
|
64
|
+
raise ValueError("atol and rtol must be finite nonnegative numbers") from error
|
|
65
|
+
if not (np.isfinite(atol) and np.isfinite(rtol) and atol >= 0 and rtol >= 0):
|
|
66
|
+
raise ValueError("atol and rtol must be finite nonnegative numbers")
|
|
67
|
+
|
|
68
|
+
if not floats:
|
|
69
|
+
# Mixed signed and unsigned widths can otherwise promote to float64,
|
|
70
|
+
# which would erase differences between neighboring large integers.
|
|
71
|
+
if candidate_kind != reference_kind:
|
|
72
|
+
unequal = candidate.astype(object) != reference.astype(object)
|
|
73
|
+
else:
|
|
74
|
+
unequal = candidate != reference
|
|
75
|
+
mismatch_count = int(np.count_nonzero(unequal))
|
|
76
|
+
return OutputComparison(
|
|
77
|
+
candidate_nonfinite_count=0,
|
|
78
|
+
reference_nonfinite_count=0,
|
|
79
|
+
mismatch_count=mismatch_count,
|
|
80
|
+
mismatch_fraction=mismatch_count / candidate.size,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
candidate_nonfinite_count = int(np.count_nonzero(~np.isfinite(candidate)))
|
|
84
|
+
reference_nonfinite_count = int(np.count_nonzero(~np.isfinite(reference)))
|
|
85
|
+
if candidate_nonfinite_count or reference_nonfinite_count:
|
|
86
|
+
return OutputComparison(candidate_nonfinite_count, reference_nonfinite_count)
|
|
87
|
+
|
|
88
|
+
candidate64 = candidate.astype(np.float64)
|
|
89
|
+
reference64 = reference.astype(np.float64)
|
|
90
|
+
difference = candidate64 - reference64
|
|
91
|
+
reference_norm = float(np.hypot.reduce(reference64.ravel()))
|
|
92
|
+
candidate_norm = float(np.hypot.reduce(candidate64.ravel()))
|
|
93
|
+
difference_norm = float(np.hypot.reduce(difference.ravel()))
|
|
94
|
+
|
|
95
|
+
outside_count = None
|
|
96
|
+
outside_fraction = None
|
|
97
|
+
if atol is not None and rtol is not None:
|
|
98
|
+
outside_count = int(
|
|
99
|
+
np.count_nonzero(~np.isclose(candidate64, reference64, atol=atol, rtol=rtol))
|
|
100
|
+
)
|
|
101
|
+
outside_fraction = outside_count / candidate.size
|
|
102
|
+
|
|
103
|
+
cosine_similarity = None
|
|
104
|
+
if candidate_norm and reference_norm:
|
|
105
|
+
cosine_similarity = float(
|
|
106
|
+
np.dot(candidate64.ravel() / candidate_norm, reference64.ravel() / reference_norm)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return OutputComparison(
|
|
110
|
+
candidate_nonfinite_count=0,
|
|
111
|
+
reference_nonfinite_count=0,
|
|
112
|
+
max_absolute_error=float(np.max(np.abs(difference))),
|
|
113
|
+
relative_l2_error=difference_norm / reference_norm if reference_norm else None,
|
|
114
|
+
cosine_similarity=cosine_similarity,
|
|
115
|
+
outside_tolerance_count=outside_count,
|
|
116
|
+
outside_tolerance_fraction=outside_fraction,
|
|
117
|
+
)
|