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/_execution.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Execute TensorRT engines with caller-owned addresses and CUDA streams."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import TYPE_CHECKING, Self
|
|
7
|
+
|
|
8
|
+
import tensorrt as trt
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from ._cache import EngineHandle
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EngineExecution:
|
|
15
|
+
"""One independent TensorRT context retained with its checked engine handle.
|
|
16
|
+
|
|
17
|
+
Use ``handle.create_execution()`` or construct this type with an ``EngineHandle``.
|
|
18
|
+
The caller owns buffers, streams, their ordering, and completion. This object
|
|
19
|
+
neither copies data nor synchronizes. Finish outstanding work before changing
|
|
20
|
+
shapes or streams, closing, or reusing the object from another thread.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, handle: EngineHandle) -> None:
|
|
24
|
+
self._handle: EngineHandle | None = handle
|
|
25
|
+
self._context: trt.IExecutionContext | None = None
|
|
26
|
+
self._ready = False
|
|
27
|
+
engine = handle.engine
|
|
28
|
+
self._names = tuple(engine.get_tensor_name(i) for i in range(engine.num_io_tensors))
|
|
29
|
+
self._ranges: dict[str, tuple[tuple[int, ...], tuple[int, ...]]] = {}
|
|
30
|
+
initial_shapes = {}
|
|
31
|
+
try:
|
|
32
|
+
for name in self._names:
|
|
33
|
+
if engine.get_tensor_location(name) != trt.TensorLocation.DEVICE:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"tensor {name!r} has HOST location; execution requires DEVICE"
|
|
36
|
+
)
|
|
37
|
+
if engine.get_tensor_mode(name) != trt.TensorIOMode.INPUT:
|
|
38
|
+
continue
|
|
39
|
+
if engine.is_shape_inference_io(name):
|
|
40
|
+
raise ValueError(f"input {name!r} is an external shape tensor; not supported")
|
|
41
|
+
shape = tuple(engine.get_tensor_shape(name))
|
|
42
|
+
if -1 in shape:
|
|
43
|
+
low, optimum, high = (
|
|
44
|
+
tuple(part) for part in engine.get_tensor_profile_shape(name, 0)
|
|
45
|
+
)
|
|
46
|
+
initial_shapes[name] = optimum
|
|
47
|
+
if low != high:
|
|
48
|
+
self._ranges[name] = (low, high)
|
|
49
|
+
else:
|
|
50
|
+
initial_shapes[name] = shape
|
|
51
|
+
self._context = engine.create_execution_context()
|
|
52
|
+
if self._context is None:
|
|
53
|
+
raise RuntimeError("TensorRT failed to create an execution context")
|
|
54
|
+
# Probe output dimensions before accepting the engine. A ranged engine
|
|
55
|
+
# still requires an explicit caller setting after this capability check.
|
|
56
|
+
self._set_shapes(initial_shapes, creating=True)
|
|
57
|
+
self._ready = not self._ranges
|
|
58
|
+
except BaseException:
|
|
59
|
+
self.close()
|
|
60
|
+
raise
|
|
61
|
+
|
|
62
|
+
def set_input_shapes(self, shapes: Mapping[str, tuple[int, ...]]) -> None:
|
|
63
|
+
"""Set every ranged input together, validating all shapes before TensorRT.
|
|
64
|
+
|
|
65
|
+
Fixed inputs, including single-point profiles, are initialized at creation
|
|
66
|
+
and omitted here. A TensorRT failure invalidates the setting until a complete
|
|
67
|
+
subsequent call succeeds; partial settings are not rolled back.
|
|
68
|
+
"""
|
|
69
|
+
self._require_open()
|
|
70
|
+
self._check_names(shapes, self._ranges.keys(), "input shape")
|
|
71
|
+
for name, shape in shapes.items():
|
|
72
|
+
low, high = self._ranges[name]
|
|
73
|
+
if (
|
|
74
|
+
not isinstance(shape, tuple)
|
|
75
|
+
or len(shape) != len(low)
|
|
76
|
+
or any(
|
|
77
|
+
type(value) is not int or not minimum <= value <= maximum
|
|
78
|
+
for value, minimum, maximum in zip(shape, low, high, strict=True)
|
|
79
|
+
)
|
|
80
|
+
):
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"input {name!r} shape {shape!r} is outside range {low!r}..{high!r}"
|
|
83
|
+
)
|
|
84
|
+
self._set_shapes(shapes)
|
|
85
|
+
|
|
86
|
+
def get_tensor_shape(self, name: str) -> tuple[int, ...]:
|
|
87
|
+
"""Read a concrete input or output shape after all input shapes are ready."""
|
|
88
|
+
context = self._require_ready()
|
|
89
|
+
self._check_tensor(name)
|
|
90
|
+
shape = tuple(context.get_tensor_shape(name))
|
|
91
|
+
if any(dimension < 0 for dimension in shape):
|
|
92
|
+
self._ready = False
|
|
93
|
+
raise RuntimeError(f"tensor {name!r} shape {shape!r} is unresolved")
|
|
94
|
+
return shape
|
|
95
|
+
|
|
96
|
+
def get_tensor_dtype(self, name: str) -> trt.DataType:
|
|
97
|
+
"""Read a tensor's TensorRT dtype for allocating a caller-owned buffer."""
|
|
98
|
+
self._require_open()
|
|
99
|
+
self._check_tensor(name)
|
|
100
|
+
return self._handle.engine.get_tensor_dtype(name)
|
|
101
|
+
|
|
102
|
+
def enqueue(self, addresses: Mapping[str, int], *, stream: int) -> None:
|
|
103
|
+
"""Enqueue all tensor addresses on the caller's stream without waiting.
|
|
104
|
+
|
|
105
|
+
Addresses must be positive integers, and the stream must be a nonnegative
|
|
106
|
+
integer (0 is the default stream). Bool is rejected. This method cannot
|
|
107
|
+
check buffer dtype, dimensions, layout, size, lifetime, or device selection.
|
|
108
|
+
"""
|
|
109
|
+
self._require_open()
|
|
110
|
+
self._check_names(addresses, self._names, "address")
|
|
111
|
+
for name, address in addresses.items():
|
|
112
|
+
if type(address) is not int or address <= 0:
|
|
113
|
+
raise ValueError(f"tensor {name!r} address {address!r} must be a positive integer")
|
|
114
|
+
if type(stream) is not int or stream < 0:
|
|
115
|
+
raise ValueError(f"stream {stream!r} must be a nonnegative integer")
|
|
116
|
+
context = self._require_ready()
|
|
117
|
+
for name in self._names:
|
|
118
|
+
if not context.set_tensor_address(name, addresses[name]):
|
|
119
|
+
raise RuntimeError(f"TensorRT rejected tensor {name!r} address {addresses[name]!r}")
|
|
120
|
+
if not context.execute_async_v3(stream):
|
|
121
|
+
raise RuntimeError(f"TensorRT enqueue failed on stream {stream!r}")
|
|
122
|
+
|
|
123
|
+
def close(self) -> None:
|
|
124
|
+
"""Release context before handle without synchronization; repeated calls do nothing.
|
|
125
|
+
|
|
126
|
+
The caller must confirm that all enqueued work has completed before closing.
|
|
127
|
+
"""
|
|
128
|
+
self._ready = False
|
|
129
|
+
self._context = None
|
|
130
|
+
self._handle = None
|
|
131
|
+
|
|
132
|
+
def __enter__(self) -> Self:
|
|
133
|
+
self._require_open()
|
|
134
|
+
return self
|
|
135
|
+
|
|
136
|
+
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
|
137
|
+
self.close()
|
|
138
|
+
|
|
139
|
+
def _require_open(self) -> trt.IExecutionContext:
|
|
140
|
+
if self._context is None:
|
|
141
|
+
raise RuntimeError("engine execution is closed")
|
|
142
|
+
return self._context
|
|
143
|
+
|
|
144
|
+
def _require_ready(self) -> trt.IExecutionContext:
|
|
145
|
+
context = self._require_open()
|
|
146
|
+
if not self._ready:
|
|
147
|
+
raise RuntimeError("input shapes need a complete successful setting before execution")
|
|
148
|
+
return context
|
|
149
|
+
|
|
150
|
+
def _check_tensor(self, name: str) -> None:
|
|
151
|
+
if name not in self._names:
|
|
152
|
+
raise ValueError(f"unknown engine tensor {name!r}")
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _check_names(values: Mapping, names, field: str) -> None:
|
|
156
|
+
expected = set(names)
|
|
157
|
+
for name in sorted(expected - values.keys(), key=repr):
|
|
158
|
+
raise ValueError(f"tensor {name!r} {field} is missing")
|
|
159
|
+
for name in sorted(values.keys() - expected, key=repr):
|
|
160
|
+
raise ValueError(f"unexpected tensor {name!r} {field} {values[name]!r}")
|
|
161
|
+
|
|
162
|
+
def _set_shapes(self, shapes: Mapping[str, tuple[int, ...]], *, creating: bool = False) -> None:
|
|
163
|
+
self._ready = False
|
|
164
|
+
for name, shape in shapes.items():
|
|
165
|
+
try:
|
|
166
|
+
accepted = self._context.set_input_shape(name, shape)
|
|
167
|
+
except Exception as error:
|
|
168
|
+
raise RuntimeError(
|
|
169
|
+
f"TensorRT failed to set input {name!r} shape {shape!r}"
|
|
170
|
+
) from error
|
|
171
|
+
if not accepted:
|
|
172
|
+
raise RuntimeError(f"TensorRT rejected input {name!r} shape {shape!r}")
|
|
173
|
+
missing = self._context.infer_shapes()
|
|
174
|
+
if missing:
|
|
175
|
+
raise RuntimeError(f"TensorRT shape inference requires tensors {missing!r}")
|
|
176
|
+
for name in self._names:
|
|
177
|
+
shape = tuple(self._context.get_tensor_shape(name))
|
|
178
|
+
if any(dimension < 0 for dimension in shape):
|
|
179
|
+
if creating:
|
|
180
|
+
raise ValueError(
|
|
181
|
+
f"tensor {name!r} has data-dependent shape {shape!r}; not supported"
|
|
182
|
+
)
|
|
183
|
+
raise RuntimeError(f"tensor {name!r} shape {shape!r} is unresolved")
|
|
184
|
+
self._ready = True
|
tensoracle/_memory.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Release unused host memory when the process allocator supports trimming."""
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import ctypes.util
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def release_host_memory() -> None:
|
|
8
|
+
"""Return free glibc heap pages without requiring libc on other platforms."""
|
|
9
|
+
try:
|
|
10
|
+
library = ctypes.util.find_library("c")
|
|
11
|
+
if library is None:
|
|
12
|
+
return
|
|
13
|
+
trim = ctypes.CDLL(library).malloc_trim
|
|
14
|
+
trim.argtypes = [ctypes.c_size_t]
|
|
15
|
+
trim.restype = ctypes.c_int
|
|
16
|
+
trim(0)
|
|
17
|
+
except (OSError, AttributeError):
|
|
18
|
+
pass
|
tensoracle/_plugins.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Validate native plugin exports and retain TensorRT library handles."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import struct
|
|
5
|
+
import sys
|
|
6
|
+
import threading
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PluginLibraryError(ValueError):
|
|
12
|
+
"""A native plugin library cannot be registered for this process."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_lock = threading.RLock()
|
|
16
|
+
_loaded_paths: dict[Path, str] = {}
|
|
17
|
+
_loaded_digests: dict[str, object] = {}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _read_struct(data: bytes, offset: int, format_: str) -> tuple[int, ...]:
|
|
21
|
+
size = struct.calcsize(format_)
|
|
22
|
+
if offset < 0 or offset + size > len(data):
|
|
23
|
+
raise PluginLibraryError("truncated native library")
|
|
24
|
+
return struct.unpack_from(format_, data, offset)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _elf_exports(data: bytes) -> set[str]:
|
|
28
|
+
if len(data) < 64 or data[:4] != b"\x7fELF":
|
|
29
|
+
raise PluginLibraryError("invalid ELF plugin library")
|
|
30
|
+
bits, encoding = data[4:6]
|
|
31
|
+
if bits not in (1, 2) or encoding not in (1, 2):
|
|
32
|
+
raise PluginLibraryError("unsupported ELF plugin library")
|
|
33
|
+
endian = "<" if encoding == 1 else ">"
|
|
34
|
+
if bits == 2:
|
|
35
|
+
(section_offset,) = _read_struct(data, 40, endian + "Q")
|
|
36
|
+
section_size, section_count = _read_struct(data, 58, endian + "HH")
|
|
37
|
+
section_format = endian + "IIQQQQIIQQ"
|
|
38
|
+
symbol_format = endian + "IBBHQQ"
|
|
39
|
+
else:
|
|
40
|
+
(section_offset,) = _read_struct(data, 32, endian + "I")
|
|
41
|
+
section_size, section_count = _read_struct(data, 46, endian + "HH")
|
|
42
|
+
section_format = endian + "IIIIIIIIII"
|
|
43
|
+
symbol_format = endian + "IIIBBH"
|
|
44
|
+
if section_count == 0 or section_size < struct.calcsize(section_format):
|
|
45
|
+
raise PluginLibraryError("missing ELF section table")
|
|
46
|
+
sections = [
|
|
47
|
+
_read_struct(data, section_offset + index * section_size, section_format)
|
|
48
|
+
for index in range(section_count)
|
|
49
|
+
]
|
|
50
|
+
exports = set()
|
|
51
|
+
for section in sections:
|
|
52
|
+
if section[1] != 11: # SHT_DYNSYM
|
|
53
|
+
continue
|
|
54
|
+
string_index = section[6]
|
|
55
|
+
if string_index >= len(sections):
|
|
56
|
+
raise PluginLibraryError("invalid ELF symbol string table")
|
|
57
|
+
strings = sections[string_index]
|
|
58
|
+
string_data = data[strings[4] : strings[4] + strings[5]]
|
|
59
|
+
if len(string_data) != strings[5]:
|
|
60
|
+
raise PluginLibraryError("truncated ELF symbol string table")
|
|
61
|
+
symbols_offset, symbols_size, symbol_stride = section[4], section[5], section[9]
|
|
62
|
+
if symbol_stride < struct.calcsize(symbol_format) or symbols_size % symbol_stride:
|
|
63
|
+
raise PluginLibraryError("invalid ELF symbol table")
|
|
64
|
+
for offset in range(symbols_offset, symbols_offset + symbols_size, symbol_stride):
|
|
65
|
+
symbol = _read_struct(data, offset, symbol_format)
|
|
66
|
+
name_offset = symbol[0]
|
|
67
|
+
if bits == 2:
|
|
68
|
+
info, visibility, section_index = symbol[1:4]
|
|
69
|
+
else:
|
|
70
|
+
info, visibility, section_index = symbol[3:6]
|
|
71
|
+
if section_index == 0 or info >> 4 not in (1, 2) or visibility & 3 not in (0, 3):
|
|
72
|
+
continue
|
|
73
|
+
if name_offset >= len(string_data):
|
|
74
|
+
raise PluginLibraryError("invalid ELF symbol name")
|
|
75
|
+
end = string_data.find(b"\0", name_offset)
|
|
76
|
+
if end < 0:
|
|
77
|
+
raise PluginLibraryError("unterminated ELF symbol name")
|
|
78
|
+
exports.add(string_data[name_offset:end].decode("ascii", errors="replace"))
|
|
79
|
+
return exports
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _pe_export_offsets(data: bytes) -> dict[str, int]:
|
|
83
|
+
"""Locate PE export values without executing any library initializers."""
|
|
84
|
+
if data[:2] != b"MZ":
|
|
85
|
+
raise PluginLibraryError("invalid PE plugin library")
|
|
86
|
+
(header,) = _read_struct(data, 0x3C, "<I")
|
|
87
|
+
if data[header : header + 4] != b"PE\0\0":
|
|
88
|
+
raise PluginLibraryError("invalid PE plugin library")
|
|
89
|
+
(section_count,) = _read_struct(data, header + 6, "<H")
|
|
90
|
+
(optional_size,) = _read_struct(data, header + 20, "<H")
|
|
91
|
+
optional = header + 24
|
|
92
|
+
(magic,) = _read_struct(data, optional, "<H")
|
|
93
|
+
if magic not in (0x10B, 0x20B):
|
|
94
|
+
raise PluginLibraryError("unsupported PE plugin library")
|
|
95
|
+
(export_rva,) = _read_struct(data, optional + (112 if magic == 0x20B else 96), "<I")
|
|
96
|
+
sections = [
|
|
97
|
+
_read_struct(data, optional + optional_size + index * 40 + 8, "<IIII")
|
|
98
|
+
for index in range(section_count)
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
def file_offset(rva: int) -> int:
|
|
102
|
+
for virtual_size, virtual_address, raw_size, raw_offset in sections:
|
|
103
|
+
if virtual_address <= rva < virtual_address + max(virtual_size, raw_size):
|
|
104
|
+
return raw_offset + rva - virtual_address
|
|
105
|
+
raise PluginLibraryError("invalid PE export address")
|
|
106
|
+
|
|
107
|
+
if export_rva == 0:
|
|
108
|
+
return {}
|
|
109
|
+
export = file_offset(export_rva)
|
|
110
|
+
(count,) = _read_struct(data, export + 24, "<I")
|
|
111
|
+
functions_rva, names_rva, ordinals_rva = _read_struct(data, export + 28, "<III")
|
|
112
|
+
if count > 100_000:
|
|
113
|
+
raise PluginLibraryError("invalid PE export count")
|
|
114
|
+
names = {}
|
|
115
|
+
for index in range(count):
|
|
116
|
+
(name_rva,) = _read_struct(data, file_offset(names_rva) + index * 4, "<I")
|
|
117
|
+
offset = file_offset(name_rva)
|
|
118
|
+
end = data.find(b"\0", offset)
|
|
119
|
+
if end < 0:
|
|
120
|
+
raise PluginLibraryError("unterminated PE export name")
|
|
121
|
+
(ordinal,) = _read_struct(data, file_offset(ordinals_rva) + index * 2, "<H")
|
|
122
|
+
(value_rva,) = _read_struct(data, file_offset(functions_rva) + ordinal * 4, "<I")
|
|
123
|
+
names[data[offset:end].decode("ascii", errors="replace")] = file_offset(value_rva)
|
|
124
|
+
return names
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _pe_exports(data: bytes) -> set[str]:
|
|
128
|
+
return set(_pe_export_offsets(data))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _exports(data: bytes) -> set[str]:
|
|
132
|
+
if sys.platform.startswith("linux"):
|
|
133
|
+
return _elf_exports(data)
|
|
134
|
+
if sys.platform == "win32":
|
|
135
|
+
return _pe_exports(data)
|
|
136
|
+
raise PluginLibraryError(f"native plugin inspection is unsupported on {sys.platform}")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def exported_data(data: bytes, name: str, expected_size: int | None = None) -> bytes:
|
|
140
|
+
"""Read an exported constant from a native library without loading the library."""
|
|
141
|
+
if sys.platform == "win32":
|
|
142
|
+
if expected_size is None or expected_size <= 0:
|
|
143
|
+
raise PluginLibraryError("PE data inspection requires a positive size")
|
|
144
|
+
offset = _pe_export_offsets(data).get(name)
|
|
145
|
+
if offset is None or offset + expected_size > len(data):
|
|
146
|
+
raise PluginLibraryError(f"native library does not export {name}")
|
|
147
|
+
return data[offset : offset + expected_size]
|
|
148
|
+
if sys.platform != "linux":
|
|
149
|
+
raise PluginLibraryError(f"native data inspection is unsupported on {sys.platform}")
|
|
150
|
+
if len(data) < 64 or data[:6] != b"\x7fELF\x02\x01":
|
|
151
|
+
raise PluginLibraryError("expected a 64-bit little-endian ELF plugin library")
|
|
152
|
+
(section_offset,) = _read_struct(data, 40, "<Q")
|
|
153
|
+
section_size, section_count = _read_struct(data, 58, "<HH")
|
|
154
|
+
if section_size < 64:
|
|
155
|
+
raise PluginLibraryError("invalid ELF section table")
|
|
156
|
+
sections = [
|
|
157
|
+
_read_struct(data, section_offset + index * section_size, "<IIQQQQIIQQ")
|
|
158
|
+
for index in range(section_count)
|
|
159
|
+
]
|
|
160
|
+
for section in sections:
|
|
161
|
+
if section[1] != 11:
|
|
162
|
+
continue
|
|
163
|
+
if section[6] >= len(sections):
|
|
164
|
+
raise PluginLibraryError("invalid ELF symbol string table")
|
|
165
|
+
strings = sections[section[6]]
|
|
166
|
+
names = data[strings[4] : strings[4] + strings[5]]
|
|
167
|
+
stride = section[9]
|
|
168
|
+
if stride < 24:
|
|
169
|
+
raise PluginLibraryError("invalid ELF symbol stride")
|
|
170
|
+
for offset in range(section[4], section[4] + section[5], stride):
|
|
171
|
+
name_offset, _, _, section_index, address, size = _read_struct(data, offset, "<IBBHQQ")
|
|
172
|
+
if name_offset >= len(names) or section_index >= len(sections):
|
|
173
|
+
continue
|
|
174
|
+
end = names.find(b"\0", name_offset)
|
|
175
|
+
if end < 0 or names[name_offset:end] != name.encode("ascii"):
|
|
176
|
+
continue
|
|
177
|
+
owner = sections[section_index]
|
|
178
|
+
file_offset = owner[4] + address - owner[3]
|
|
179
|
+
if size <= 0 or file_offset < 0 or file_offset + size > len(data):
|
|
180
|
+
raise PluginLibraryError(f"invalid native data symbol {name}")
|
|
181
|
+
if expected_size is not None and expected_size != size:
|
|
182
|
+
raise PluginLibraryError(f"native data symbol {name} has the wrong size")
|
|
183
|
+
return data[file_offset : file_offset + size]
|
|
184
|
+
raise PluginLibraryError(f"native library does not export {name}")
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def register_libraries(paths: Sequence[Path]) -> tuple[str, ...]:
|
|
188
|
+
"""Register each distinct library once and return the digests of its loaded bytes."""
|
|
189
|
+
import tensorrt as trt
|
|
190
|
+
|
|
191
|
+
digests = []
|
|
192
|
+
with _lock:
|
|
193
|
+
for candidate in paths:
|
|
194
|
+
try:
|
|
195
|
+
path = Path(candidate).resolve(strict=True)
|
|
196
|
+
data = path.read_bytes()
|
|
197
|
+
except OSError as error:
|
|
198
|
+
raise PluginLibraryError(
|
|
199
|
+
f"plugin library is missing or unreadable: {candidate}"
|
|
200
|
+
) from error
|
|
201
|
+
digest = hashlib.sha256(data).hexdigest()
|
|
202
|
+
if path in _loaded_paths:
|
|
203
|
+
if _loaded_paths[path] != digest:
|
|
204
|
+
raise PluginLibraryError(f"loaded plugin library changed: {path}")
|
|
205
|
+
digests.append(digest)
|
|
206
|
+
continue
|
|
207
|
+
if "getCreators" not in _exports(data):
|
|
208
|
+
raise PluginLibraryError(f"plugin library does not export getCreators: {path}")
|
|
209
|
+
if digest not in _loaded_digests:
|
|
210
|
+
try:
|
|
211
|
+
handle = trt.get_plugin_registry().load_library(str(path))
|
|
212
|
+
except Exception as error:
|
|
213
|
+
raise PluginLibraryError(
|
|
214
|
+
f"TensorRT could not register plugin library: {path}"
|
|
215
|
+
) from error
|
|
216
|
+
if handle is None:
|
|
217
|
+
raise PluginLibraryError(f"TensorRT could not register plugin library: {path}")
|
|
218
|
+
if hashlib.sha256(path.read_bytes()).hexdigest() != digest:
|
|
219
|
+
raise PluginLibraryError(f"plugin library changed while loading: {path}")
|
|
220
|
+
_loaded_digests[digest] = handle
|
|
221
|
+
_loaded_paths[path] = digest
|
|
222
|
+
digests.append(digest)
|
|
223
|
+
return tuple(digests)
|
tensoracle/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.3.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 3, 1)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
tensoracle/bundle.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Inspect a cubin bundle and its native plugin without a GPU or producer."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import struct
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from zipfile import ZipFile
|
|
9
|
+
|
|
10
|
+
from ._plugins import PluginLibraryError, exported_data
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BundleError(ValueError):
|
|
14
|
+
"""A cubin bundle does not match its manifest or native plugin."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MissingCubinError(BundleError):
|
|
18
|
+
"""A declared cubin is missing."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CorruptCubinError(BundleError):
|
|
22
|
+
"""A cubin's bytes differ from its declared digest or length."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CubinArchitectureError(BundleError):
|
|
26
|
+
"""A cubin's ELF architecture differs from its declared SM."""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PluginBundleMismatchError(BundleError):
|
|
30
|
+
"""A native plugin was built for a different cubin bundle."""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _canonical(value: object) -> bytes:
|
|
34
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def bundle_sha256(manifest: dict) -> str:
|
|
38
|
+
"""Return the identity of a manifest, excluding its own digest field."""
|
|
39
|
+
return hashlib.sha256(
|
|
40
|
+
_canonical({k: v for k, v in manifest.items() if k != "bundle_sha256"})
|
|
41
|
+
).hexdigest()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def cubin_sm(data: bytes) -> str:
|
|
45
|
+
"""Read the NVIDIA architecture from a cubin ELF header."""
|
|
46
|
+
if len(data) < 64 or data[:6] != b"\x7fELF\x02\x01":
|
|
47
|
+
raise CubinArchitectureError("cubin is not a 64-bit little-endian ELF")
|
|
48
|
+
if struct.unpack_from("<H", data, 18)[0] != 190:
|
|
49
|
+
raise CubinArchitectureError("cubin has no NVIDIA CUDA ELF machine")
|
|
50
|
+
flags = struct.unpack_from("<I", data, 48)[0]
|
|
51
|
+
old_sm = (flags >> 16) & 0xFF
|
|
52
|
+
new_sm = (flags >> 8) & 0xFF
|
|
53
|
+
if old_sm in (86, 89):
|
|
54
|
+
return f"sm{old_sm}"
|
|
55
|
+
if new_sm == 120:
|
|
56
|
+
return "sm120"
|
|
57
|
+
raise CubinArchitectureError(f"unsupported cubin architecture in ELF flags: {flags:#x}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _validate_entry(entry: dict) -> None:
|
|
61
|
+
if entry.get("target_sm") not in ("sm86", "sm89", "sm120"):
|
|
62
|
+
raise BundleError("unsupported target SM")
|
|
63
|
+
hidden = entry.get("hidden_trailing_parameters")
|
|
64
|
+
if hidden != [
|
|
65
|
+
{"name": "global_scratch", "parameter_bytes": 8},
|
|
66
|
+
{"name": "profile_scratch", "parameter_bytes": 8},
|
|
67
|
+
]:
|
|
68
|
+
raise BundleError("unsupported hidden trailing parameters")
|
|
69
|
+
resources = entry.get("resources")
|
|
70
|
+
if not isinstance(resources, dict):
|
|
71
|
+
raise BundleError("missing cubin resources")
|
|
72
|
+
if resources.get("global_scratch_bytes") != 0 or resources.get("profile_scratch_bytes") != 0:
|
|
73
|
+
raise BundleError("nonzero scratch is unsupported")
|
|
74
|
+
for key in (
|
|
75
|
+
"compiled_shared_memory_bytes",
|
|
76
|
+
"launch_dynamic_shared_memory_bytes",
|
|
77
|
+
"registers_per_thread",
|
|
78
|
+
):
|
|
79
|
+
if type(resources.get(key)) is not int or resources[key] < 0:
|
|
80
|
+
raise BundleError(f"invalid cubin resource {key}")
|
|
81
|
+
if resources["registers_per_thread"] == 0 or (
|
|
82
|
+
resources["launch_dynamic_shared_memory_bytes"] < resources["compiled_shared_memory_bytes"]
|
|
83
|
+
):
|
|
84
|
+
raise BundleError("unsupported cubin resource range")
|
|
85
|
+
signature = entry.get("signature")
|
|
86
|
+
if (
|
|
87
|
+
not isinstance(entry.get("entry_symbol"), str)
|
|
88
|
+
or not entry["entry_symbol"]
|
|
89
|
+
or not isinstance(signature, list)
|
|
90
|
+
):
|
|
91
|
+
raise BundleError("missing cubin symbol or signature")
|
|
92
|
+
if not 1 <= len(signature) <= 14:
|
|
93
|
+
raise BundleError("unsupported public argument count")
|
|
94
|
+
names = set()
|
|
95
|
+
for argument in signature:
|
|
96
|
+
if not isinstance(argument, dict) or not isinstance(argument.get("name"), str):
|
|
97
|
+
raise BundleError("invalid public argument")
|
|
98
|
+
if argument["name"] in names or argument.get("type") not in (
|
|
99
|
+
"*fp16",
|
|
100
|
+
"*fp32",
|
|
101
|
+
"*i8",
|
|
102
|
+
"*i32",
|
|
103
|
+
"i32",
|
|
104
|
+
"fp32",
|
|
105
|
+
):
|
|
106
|
+
raise BundleError("unsupported public argument")
|
|
107
|
+
names.add(argument["name"])
|
|
108
|
+
specialization = entry.get("specialization")
|
|
109
|
+
if not isinstance(specialization, dict) or set(specialization) != {"divisible_by"}:
|
|
110
|
+
raise BundleError("unsupported specialization rule")
|
|
111
|
+
divisors = specialization["divisible_by"]
|
|
112
|
+
if not isinstance(divisors, dict) or any(
|
|
113
|
+
name not in names
|
|
114
|
+
or type(divisor) is not int
|
|
115
|
+
or divisor < 1
|
|
116
|
+
or divisor > (1 << 30)
|
|
117
|
+
or divisor & (divisor - 1)
|
|
118
|
+
for name, divisor in divisors.items()
|
|
119
|
+
):
|
|
120
|
+
raise BundleError("unsupported specialization rule")
|
|
121
|
+
if any(
|
|
122
|
+
argument["type"] == "fp32" and divisors.get(argument["name"], 1) != 1
|
|
123
|
+
for argument in signature
|
|
124
|
+
):
|
|
125
|
+
raise BundleError("unsupported floating point specialization rule")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def inspect_bundle(root: Path, *, plugin: Path | None = None) -> str:
|
|
129
|
+
"""Verify cubin bytes, ELF SM, supported launch metadata, and plugin identity."""
|
|
130
|
+
root = Path(root)
|
|
131
|
+
manifest = json.loads((root / "bundle.json").read_text(encoding="utf-8"))
|
|
132
|
+
if not isinstance(manifest, dict) or manifest.get("schema_version") != 1:
|
|
133
|
+
raise BundleError("unsupported bundle manifest")
|
|
134
|
+
entries = manifest.get("entries")
|
|
135
|
+
if not isinstance(entries, list) or not entries:
|
|
136
|
+
raise BundleError("bundle has no cubin entries")
|
|
137
|
+
digest = bundle_sha256(manifest)
|
|
138
|
+
if "bundle_sha256" in manifest and manifest["bundle_sha256"] != digest:
|
|
139
|
+
raise BundleError("bundle manifest digest differs")
|
|
140
|
+
visited = set()
|
|
141
|
+
cubins = {}
|
|
142
|
+
for entry in entries:
|
|
143
|
+
if not isinstance(entry, dict):
|
|
144
|
+
raise BundleError("invalid cubin entry")
|
|
145
|
+
_validate_entry(entry)
|
|
146
|
+
name = entry.get("file")
|
|
147
|
+
if not isinstance(name, str) or Path(name).is_absolute() or ".." in Path(name).parts:
|
|
148
|
+
raise BundleError("invalid cubin path")
|
|
149
|
+
if name in visited:
|
|
150
|
+
raise BundleError("duplicate cubin path")
|
|
151
|
+
visited.add(name)
|
|
152
|
+
path = root / name
|
|
153
|
+
if not path.is_file():
|
|
154
|
+
raise MissingCubinError(f"missing cubin: {path}")
|
|
155
|
+
data = path.read_bytes()
|
|
156
|
+
if len(data) != entry.get("bytes") or hashlib.sha256(data).hexdigest() != entry.get(
|
|
157
|
+
"sha256"
|
|
158
|
+
):
|
|
159
|
+
raise CorruptCubinError(f"cubin digest or size differs: {path}")
|
|
160
|
+
actual_sm = cubin_sm(data)
|
|
161
|
+
if actual_sm != entry["target_sm"]:
|
|
162
|
+
raise CubinArchitectureError(
|
|
163
|
+
f"cubin {path} declares {entry['target_sm']} but ELF contains {actual_sm}"
|
|
164
|
+
)
|
|
165
|
+
if "embedded_symbol" in entry:
|
|
166
|
+
cubins[entry["embedded_symbol"]] = data
|
|
167
|
+
if plugin is not None:
|
|
168
|
+
try:
|
|
169
|
+
library = Path(plugin).read_bytes()
|
|
170
|
+
embedded = exported_data(library, "tensoracle_bundle_sha256", 65)
|
|
171
|
+
except (OSError, PluginLibraryError) as error:
|
|
172
|
+
raise PluginBundleMismatchError(
|
|
173
|
+
f"plugin bundle identity is unavailable: {plugin}"
|
|
174
|
+
) from error
|
|
175
|
+
if embedded.rstrip(b"\0") != digest.encode("ascii"):
|
|
176
|
+
raise PluginBundleMismatchError(f"plugin and cubin bundle differ: {plugin}")
|
|
177
|
+
for symbol, data in cubins.items():
|
|
178
|
+
try:
|
|
179
|
+
native = exported_data(library, symbol, len(data))
|
|
180
|
+
except PluginLibraryError as error:
|
|
181
|
+
raise PluginBundleMismatchError(f"plugin lacks embedded cubin {symbol}") from error
|
|
182
|
+
if native != data:
|
|
183
|
+
raise PluginBundleMismatchError(f"plugin embedded cubin differs: {symbol}")
|
|
184
|
+
return digest
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def inspect_wheel(path: Path, *, plugin: str | None = None) -> str:
|
|
188
|
+
"""Inspect the one cubin bundle inside a wheel without installing it."""
|
|
189
|
+
with tempfile.TemporaryDirectory() as directory, ZipFile(path) as archive:
|
|
190
|
+
root = Path(directory)
|
|
191
|
+
for info in archive.infolist():
|
|
192
|
+
member = Path(info.filename)
|
|
193
|
+
if member.is_absolute() or ".." in member.parts:
|
|
194
|
+
raise BundleError("wheel contains an invalid path")
|
|
195
|
+
if info.is_dir():
|
|
196
|
+
continue
|
|
197
|
+
destination = root / member
|
|
198
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
199
|
+
destination.write_bytes(archive.read(info))
|
|
200
|
+
manifests = list(root.rglob("bundle.json"))
|
|
201
|
+
if len(manifests) != 1:
|
|
202
|
+
raise BundleError("wheel must contain exactly one cubin bundle")
|
|
203
|
+
library = root / plugin if plugin is not None else None
|
|
204
|
+
if library is not None and (Path(plugin).is_absolute() or ".." in Path(plugin).parts):
|
|
205
|
+
raise BundleError("invalid plugin path in wheel")
|
|
206
|
+
return inspect_bundle(manifests[0].parent, plugin=library)
|