kernel-lens 1.2.1__tar.gz → 2.0.0__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 (62) hide show
  1. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/PKG-INFO +58 -6
  2. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/README.md +53 -1
  3. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/__init__.py +1 -1
  4. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/backends/builder.py +15 -0
  5. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/backends/ort_gen.py +55 -20
  6. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/backends/trt_gen.py +99 -54
  7. kernel_lens-2.0.0/kernel_lens/backends/trt_v3_gen.py +571 -0
  8. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/compiler/ast_analyzer.py +54 -7
  9. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/compiler/core.py +99 -9
  10. kernel_lens-2.0.0/kernel_lens/compiler/manifest.py +171 -0
  11. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/compiler/tracer.py +47 -0
  12. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/runtime/engine.py +164 -11
  13. kernel_lens-2.0.0/kernel_lens/utils/env_check.py +265 -0
  14. kernel_lens-2.0.0/kernel_lens/utils/trt_headers.py +199 -0
  15. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens.egg-info/PKG-INFO +58 -6
  16. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens.egg-info/SOURCES.txt +2 -0
  17. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens.egg-info/requires.txt +4 -4
  18. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/pyproject.toml +10 -3
  19. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_conformance.py +58 -0
  20. kernel_lens-1.2.1/kernel_lens/compiler/manifest.py +0 -80
  21. kernel_lens-1.2.1/kernel_lens/utils/env_check.py +0 -107
  22. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/LICENSE +0 -0
  23. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/backends/__init__.py +0 -0
  24. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/compiler/__init__.py +0 -0
  25. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/compiler/interaction.py +0 -0
  26. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/compiler/onnx_exporter.py +0 -0
  27. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/config.py +0 -0
  28. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/include/tensorrt/NvInfer.h +0 -0
  29. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/include/tensorrt/NvInferImpl.h +0 -0
  30. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/include/tensorrt/NvInferLegacyDims.h +0 -0
  31. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/include/tensorrt/NvInferPlugin.h +0 -0
  32. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/include/tensorrt/NvInferPluginBase.h +0 -0
  33. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/include/tensorrt/NvInferPluginUtils.h +0 -0
  34. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/include/tensorrt/NvInferRuntime.h +0 -0
  35. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/include/tensorrt/NvInferRuntimeBase.h +0 -0
  36. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/include/tensorrt/NvInferRuntimeCommon.h +0 -0
  37. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/include/tensorrt/NvInferRuntimePlugin.h +0 -0
  38. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/include/tensorrt/NvInferVersion.h +0 -0
  39. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/runtime/__init__.py +0 -0
  40. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/utils/__init__.py +0 -0
  41. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens/utils/deployment.py +0 -0
  42. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens.egg-info/dependency_links.txt +0 -0
  43. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/kernel_lens.egg-info/top_level.txt +0 -0
  44. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/setup.cfg +0 -0
  45. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_alignment_guard.py +0 -0
  46. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_architectural_fixes.py +0 -0
  47. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_edge_cases.py +0 -0
  48. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_fp8_gemm.py +0 -0
  49. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_fused_cross_entropy.py +0 -0
  50. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_fused_residual_rms.py +0 -0
  51. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_inplace.py +0 -0
  52. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_inplace_dtypes.py +0 -0
  53. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_int4_dequant_gemm.py +0 -0
  54. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_mamba_selective_scan.py +0 -0
  55. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_research_kernels.py +0 -0
  56. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_standalone.py +0 -0
  57. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_standalone_launch.py +0 -0
  58. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_standalone_ptx.py +0 -0
  59. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_tricky_triton_kernels.py +0 -0
  60. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_triton_vs_torch_compile.py +0 -0
  61. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_trt.py +0 -0
  62. {kernel_lens-1.2.1 → kernel_lens-2.0.0}/tests/test_unit_cpu.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kernel-lens
3
- Version: 1.2.1
3
+ Version: 2.0.0
4
4
  Summary: Compile PyTorch models with custom @triton.jit kernels into ONNX Runtime and TensorRT C++ plugins
5
5
  Author-email: Justin Duc <justin.duc@galiad-research.com>
6
6
  License: MIT License
@@ -52,12 +52,12 @@ Requires-Dist: onnx>=1.15.0
52
52
  Provides-Extra: ort
53
53
  Requires-Dist: onnxruntime-gpu>=1.17.0; extra == "ort"
54
54
  Provides-Extra: trt
55
- Requires-Dist: tensorrt<11.0.0,>=8.6.0; extra == "trt"
56
- Requires-Dist: tensorrt-cu12-libs<11.0.0; extra == "trt"
55
+ Requires-Dist: tensorrt<12.0.0,>=8.6.0; extra == "trt"
56
+ Requires-Dist: tensorrt-cu12-libs; extra == "trt"
57
57
  Provides-Extra: all
58
58
  Requires-Dist: onnxruntime-gpu>=1.17.0; extra == "all"
59
- Requires-Dist: tensorrt<11.0.0,>=8.6.0; extra == "all"
60
- Requires-Dist: tensorrt-cu12-libs<11.0.0; extra == "all"
59
+ Requires-Dist: tensorrt<12.0.0,>=8.6.0; extra == "all"
60
+ Requires-Dist: tensorrt-cu12-libs; extra == "all"
61
61
  Provides-Extra: bench
62
62
  Requires-Dist: transformers>=4.40.0; extra == "bench"
63
63
  Dynamic: license-file
@@ -114,6 +114,18 @@ compiled_model.run(inputs, backend="onnx"|"tensorrt")
114
114
  - **Dynamic grids in C++** — launch-grid expressions are transpiled to C++
115
115
  integer math over runtime shapes, so one plugin serves dynamic batch and
116
116
  sequence lengths.
117
+ - **One package, several TensorRT versions** — the plugin interface is chosen
118
+ from the environment. TensorRT 10 deprecated the whole `IPluginV2*` hierarchy
119
+ and TensorRT 11 removed it, so `plugin_api="auto"` (the default) emits
120
+ `IPluginV3` plugins where the runtime supports them and `IPluginV2DynamicExt`
121
+ plugins otherwise. This spans TensorRT 8.6 through 10.x today; TensorRT 11+
122
+ works when you point `TENSORRT_INCLUDE_DIR` at matching headers.
123
+ - **Shape-generic engines** — `kl.compile(..., dynamic_shapes=True)` exports the
124
+ leading dimension as a symbolic axis, so a single engine serves any size in a
125
+ declared range via a TensorRT optimization profile. Verified on Blackwell
126
+ (`sm_120a`) and Ampere (`sm_80`): one compile at batch 8 serving batches
127
+ 1-16, bit-exact on both engines. Off by default, because a shape-specialised
128
+ engine is what the published benchmarks measure.
117
129
  - **Zero-copy VRAM binding** — PyTorch `data_ptr()` values are registered
118
130
  directly with each engine; no host staging copies.
119
131
  - **Blackwell PTX ISA clamping** — a Triton-generated `.version 9.3` header is
@@ -151,11 +163,34 @@ pip install kernel-lens[all] # both
151
163
  Triton ≥ 2.1, and `nvcc` + `g++` on `PATH`. TensorRT must be `< 11` (the
152
164
  generator emits `IPluginV2DynamicExt`).
153
165
 
166
+ **TensorRT versions.** Handled automatically. The pip TensorRT wheels ship **no
167
+ headers**, so KernelLens fetches the set matching the installed runtime from the
168
+ TensorRT OSS repository and caches it under `~/.kernel_lens_headers/<major>.<minor>`;
169
+ when the headers it already has match the runtime major, nothing is fetched. If
170
+ the download is unavailable the fallback is the bundled set, and a
171
+ header/runtime major mismatch is then reported instead of crashing inside
172
+ TensorRT. `plugin_api="auto"` picks the interface from the runtime:
173
+
174
+ | TensorRT | `IPluginV2*` | `IPluginV3` | emitted |
175
+ | :--- | :--- | :--- | :--- |
176
+ | < 10 (8.6, 9.x) | present | absent | `v2` |
177
+ | 10.x | deprecated | present | `v3` |
178
+ | 11+ | **removed** | present | `v3` |
179
+
180
+ Verified on TensorRT 10.16 and 11.3, both 16/16 on the conformance suite. Force
181
+ a choice with `plugin_api="v2"` / `"v3"`, or the `KERNEL_LENS_PLUGIN_API`
182
+ environment variable. To supply headers yourself — offline installs, or a TensorRT
183
+ build the OSS repository does not track — set `TENSORRT_INCLUDE_DIR`, or
184
+ `KERNEL_LENS_FETCH_HEADERS=0` to disable fetching entirely.
185
+
154
186
  **Environment:** `KERNEL_LENS_CACHE_DIR` (put it on a persistent volume so the
155
187
  `.so` / `.engine` artifacts survive `kl.load`), `KERNEL_LENS_CCBIN` (host
156
188
  compiler for `nvcc` — CUDA toolkits before 12.9 reject GCC 13's exception
157
189
  specifiers, so set it to `g++-12` there), `KERNEL_LENS_DEBUG` (or `KL_VERBOSE`),
158
- `TENSORRT_INCLUDE_DIR`, and `KERNEL_LENS_STREAM_READER=1` to deserialize engines
190
+ `TENSORRT_INCLUDE_DIR` (override the TensorRT headers),
191
+ `KERNEL_LENS_HEADER_CACHE` (where fetched TensorRT headers are kept),
192
+ `KERNEL_LENS_FETCH_HEADERS=0` (disable fetching),
193
+ `KERNEL_LENS_PLUGIN_API` (`auto`, `v2` or `v3`), and `KERNEL_LENS_STREAM_READER=1` to deserialize engines
159
194
  above 2 GB from disk rather than from a memory buffer (opt-in; see the
160
195
  [changelog](CHANGELOG.md)).
161
196
 
@@ -196,6 +231,13 @@ compiled = kl.compile(RMSNorm(4096).cuda(), (x,), name="llama3_rmsnorm",
196
231
  backends=["onnx", "tensorrt"])
197
232
  out_ort = compiled.run((x,), backend="onnx")
198
233
  out_trt = compiled.run((x,), backend="tensorrt")
234
+
235
+ # One engine for any batch: mark the leading dim dynamic. dynamic_max bounds
236
+ # the optimization profile (default 4x the traced size).
237
+ serving = kl.compile(RMSNorm(4096).cuda(), (x,), name="llama3_rmsnorm_dyn",
238
+ backends=["onnx", "tensorrt"], dynamic_shapes=True)
239
+ for b in (1, 2, 4, 8, 16): # all through the engine built at b=4
240
+ serving.run((torch.randn(b, 512, 4096, device="cuda"),), backend="tensorrt")
199
241
  ```
200
242
 
201
243
  ## Results at a glance
@@ -212,6 +254,16 @@ and every raw JSON are on the
212
254
  it replaces is **1,137 lines across six files** for one fused RMSNorm kernel on
213
255
  both backends, measured with `tests/measure_codegen.py`.
214
256
 
257
+ **One engine, many shapes.** With `dynamic_shapes=True` a single compiled engine
258
+ serves any leading-dimension size in range. Measured with
259
+ `tests/test_conformance.py`, which compiles once and then runs every shape through
260
+ that engine — `0.00e+00` on both backends, on Blackwell and on Ampere:
261
+
262
+ | Case | Compiled at | Served |
263
+ | :--- | :--- | :--- |
264
+ | `dynamic_batch_sweep` | batch 8 | batches 1, 2, 4, 8, 16 |
265
+ | `dynamic_grid_sweep` | 524,288 elements | 65,536 / 131,072 / 524,288 / 1,048,576 |
266
+
215
267
  **Real pretrained models.** Every decoder `LayerNorm` of a real OPT checkpoint
216
268
  (125M → 6.7B) replaced by the Triton RMSNorm kernel, graded against
217
269
  `torch.compile` — the baseline that *captures the same Triton kernels* rather
@@ -50,6 +50,18 @@ compiled_model.run(inputs, backend="onnx"|"tensorrt")
50
50
  - **Dynamic grids in C++** — launch-grid expressions are transpiled to C++
51
51
  integer math over runtime shapes, so one plugin serves dynamic batch and
52
52
  sequence lengths.
53
+ - **One package, several TensorRT versions** — the plugin interface is chosen
54
+ from the environment. TensorRT 10 deprecated the whole `IPluginV2*` hierarchy
55
+ and TensorRT 11 removed it, so `plugin_api="auto"` (the default) emits
56
+ `IPluginV3` plugins where the runtime supports them and `IPluginV2DynamicExt`
57
+ plugins otherwise. This spans TensorRT 8.6 through 10.x today; TensorRT 11+
58
+ works when you point `TENSORRT_INCLUDE_DIR` at matching headers.
59
+ - **Shape-generic engines** — `kl.compile(..., dynamic_shapes=True)` exports the
60
+ leading dimension as a symbolic axis, so a single engine serves any size in a
61
+ declared range via a TensorRT optimization profile. Verified on Blackwell
62
+ (`sm_120a`) and Ampere (`sm_80`): one compile at batch 8 serving batches
63
+ 1-16, bit-exact on both engines. Off by default, because a shape-specialised
64
+ engine is what the published benchmarks measure.
53
65
  - **Zero-copy VRAM binding** — PyTorch `data_ptr()` values are registered
54
66
  directly with each engine; no host staging copies.
55
67
  - **Blackwell PTX ISA clamping** — a Triton-generated `.version 9.3` header is
@@ -87,11 +99,34 @@ pip install kernel-lens[all] # both
87
99
  Triton ≥ 2.1, and `nvcc` + `g++` on `PATH`. TensorRT must be `< 11` (the
88
100
  generator emits `IPluginV2DynamicExt`).
89
101
 
102
+ **TensorRT versions.** Handled automatically. The pip TensorRT wheels ship **no
103
+ headers**, so KernelLens fetches the set matching the installed runtime from the
104
+ TensorRT OSS repository and caches it under `~/.kernel_lens_headers/<major>.<minor>`;
105
+ when the headers it already has match the runtime major, nothing is fetched. If
106
+ the download is unavailable the fallback is the bundled set, and a
107
+ header/runtime major mismatch is then reported instead of crashing inside
108
+ TensorRT. `plugin_api="auto"` picks the interface from the runtime:
109
+
110
+ | TensorRT | `IPluginV2*` | `IPluginV3` | emitted |
111
+ | :--- | :--- | :--- | :--- |
112
+ | < 10 (8.6, 9.x) | present | absent | `v2` |
113
+ | 10.x | deprecated | present | `v3` |
114
+ | 11+ | **removed** | present | `v3` |
115
+
116
+ Verified on TensorRT 10.16 and 11.3, both 16/16 on the conformance suite. Force
117
+ a choice with `plugin_api="v2"` / `"v3"`, or the `KERNEL_LENS_PLUGIN_API`
118
+ environment variable. To supply headers yourself — offline installs, or a TensorRT
119
+ build the OSS repository does not track — set `TENSORRT_INCLUDE_DIR`, or
120
+ `KERNEL_LENS_FETCH_HEADERS=0` to disable fetching entirely.
121
+
90
122
  **Environment:** `KERNEL_LENS_CACHE_DIR` (put it on a persistent volume so the
91
123
  `.so` / `.engine` artifacts survive `kl.load`), `KERNEL_LENS_CCBIN` (host
92
124
  compiler for `nvcc` — CUDA toolkits before 12.9 reject GCC 13's exception
93
125
  specifiers, so set it to `g++-12` there), `KERNEL_LENS_DEBUG` (or `KL_VERBOSE`),
94
- `TENSORRT_INCLUDE_DIR`, and `KERNEL_LENS_STREAM_READER=1` to deserialize engines
126
+ `TENSORRT_INCLUDE_DIR` (override the TensorRT headers),
127
+ `KERNEL_LENS_HEADER_CACHE` (where fetched TensorRT headers are kept),
128
+ `KERNEL_LENS_FETCH_HEADERS=0` (disable fetching),
129
+ `KERNEL_LENS_PLUGIN_API` (`auto`, `v2` or `v3`), and `KERNEL_LENS_STREAM_READER=1` to deserialize engines
95
130
  above 2 GB from disk rather than from a memory buffer (opt-in; see the
96
131
  [changelog](CHANGELOG.md)).
97
132
 
@@ -132,6 +167,13 @@ compiled = kl.compile(RMSNorm(4096).cuda(), (x,), name="llama3_rmsnorm",
132
167
  backends=["onnx", "tensorrt"])
133
168
  out_ort = compiled.run((x,), backend="onnx")
134
169
  out_trt = compiled.run((x,), backend="tensorrt")
170
+
171
+ # One engine for any batch: mark the leading dim dynamic. dynamic_max bounds
172
+ # the optimization profile (default 4x the traced size).
173
+ serving = kl.compile(RMSNorm(4096).cuda(), (x,), name="llama3_rmsnorm_dyn",
174
+ backends=["onnx", "tensorrt"], dynamic_shapes=True)
175
+ for b in (1, 2, 4, 8, 16): # all through the engine built at b=4
176
+ serving.run((torch.randn(b, 512, 4096, device="cuda"),), backend="tensorrt")
135
177
  ```
136
178
 
137
179
  ## Results at a glance
@@ -148,6 +190,16 @@ and every raw JSON are on the
148
190
  it replaces is **1,137 lines across six files** for one fused RMSNorm kernel on
149
191
  both backends, measured with `tests/measure_codegen.py`.
150
192
 
193
+ **One engine, many shapes.** With `dynamic_shapes=True` a single compiled engine
194
+ serves any leading-dimension size in range. Measured with
195
+ `tests/test_conformance.py`, which compiles once and then runs every shape through
196
+ that engine — `0.00e+00` on both backends, on Blackwell and on Ampere:
197
+
198
+ | Case | Compiled at | Served |
199
+ | :--- | :--- | :--- |
200
+ | `dynamic_batch_sweep` | batch 8 | batches 1, 2, 4, 8, 16 |
201
+ | `dynamic_grid_sweep` | 524,288 elements | 65,536 / 131,072 / 524,288 / 1,048,576 |
202
+
151
203
  **Real pretrained models.** Every decoder `LayerNorm` of a real OPT checkpoint
152
204
  (125M → 6.7B) replaced by the Triton RMSNorm kernel, graded against
153
205
  `torch.compile` — the baseline that *captures the same Triton kernels* rather
@@ -7,7 +7,7 @@ from .config import set_verbose, is_verbose
7
7
 
8
8
 
9
9
 
10
- __version__ = "1.2.1"
10
+ __version__ = "2.0.0"
11
11
 
12
12
  __all__ = [
13
13
  "compile",
@@ -249,6 +249,21 @@ def build_trt_plugin(trt_plugins_dir: str, cache_dir: str):
249
249
  arch_flag = "-gencode=arch=compute_75,code=sm_75"
250
250
 
251
251
  trt_inc_dirs = []
252
+
253
+ # Headers matching the installed TensorRT, fetched and cached on first
254
+ # use when the bundled set is a different major version. See
255
+ # utils/trt_headers.py for why this is necessary: the pip TensorRT
256
+ # wheels ship no headers at all.
257
+ try:
258
+ from ..utils.trt_headers import resolve as _resolve_headers
259
+ _hdr_dir, _hdr_reason = _resolve_headers()
260
+ from ..config import debug_print as _dp
261
+ _dp(f"[KernelLens] TensorRT headers: {_hdr_dir or 'bundled'} ({_hdr_reason})")
262
+ if _hdr_dir:
263
+ trt_inc_dirs.append(_hdr_dir)
264
+ except Exception:
265
+ pass
266
+
252
267
  if os.environ.get("TENSORRT_INCLUDE_DIR"):
253
268
  trt_inc_dirs.append(os.environ["TENSORRT_INCLUDE_DIR"])
254
269
  user_trt_inc = os.path.expanduser("~/tensorrt_headers")
@@ -150,31 +150,26 @@ struct {op_name} : Ort::CustomOpBase<{op_name}, {kernel_name}> {{
150
150
  arg_setup_lines.append("auto ref_in = ctx.GetInput(0);")
151
151
  arg_setup_lines.append("auto info = ref_in.GetTensorTypeAndShapeInfo();")
152
152
  arg_setup_lines.append("std::vector<int64_t> dim_values = info.GetShape();")
153
+
154
+ # Dims the caller asked to keep dynamic: read them from the tensor that
155
+ # is actually being run, not from the traced shape.
156
+ _dyn_axes = tuple(getattr(manifest, "dynamic_axes", ()) or ())
153
157
 
154
158
  import re
155
159
  fn_match = re.search(r'\.entry\s+([a-zA-Z0-9_]+)', manifest.ptx)
156
160
  ptx_entry_name = fn_match.group(1) if fn_match else manifest.kernel_name
157
161
 
158
- # --- ROBUST GRID EVALUATION WITH GENERIC REGEX SUBSTITUTION ---
159
- grid_strs = []
160
- if hasattr(manifest, '_sym_grid_asts') and manifest._sym_grid_asts:
161
- for g in manifest._sym_grid_asts:
162
- expr = str(g.node.expr) if hasattr(g, 'node') else str(g)
163
- expr = re.sub(r'([a-zA-Z0-9_]+)\*\*([a-zA-Z0-9_]+)', r'std::pow(\1, \2)', expr)
164
- expr = expr.replace("//", "/")
165
- expr = re.sub(r'floor\((.*?)\)', r'(\1)', expr)
166
-
167
- # Replace any s(\d+) symbol dynamically using regex
168
- expr = re.sub(r'\bs(\d+)\b', r"(int64_t)(dim_values.size() > \1 ? dim_values[\1] : 1)", expr)
169
- grid_strs.append(expr)
170
-
162
+ # --- GRID EVALUATION, from the manifest's own symbol translation ---
163
+ grid_strs = manifest.grid_exprs(
164
+ lambda i: f"(int64_t)(dim_values.size() > {i} ? dim_values[{i}] : 1)"
165
+ )
171
166
  while len(grid_strs) < 3:
172
167
  grid_strs.append("1")
173
168
 
174
169
  grid_eval_lines = [
175
170
  f"unsigned int grid_x = std::max(1u, (unsigned int)({grid_strs[0]}));",
176
171
  f"unsigned int grid_y = std::max(1u, (unsigned int)({grid_strs[1]}));",
177
- f"unsigned int grid_z = std::max(1u, (unsigned int)({grid_strs[2]}));"
172
+ f"unsigned int grid_z = std::max(1u, (unsigned int)({grid_strs[2]}));",
178
173
  ]
179
174
 
180
175
  entry_match = re.search(r'\.entry\s+[a-zA-Z0-9_]+\s*\((.*?)\)\s*(?:\.reqntid|\{)', manifest.ptx, re.DOTALL)
@@ -215,6 +210,23 @@ struct {op_name} : Ort::CustomOpBase<{op_name}, {kernel_name}> {{
215
210
 
216
211
  ptx_ordered_slots.append({"type": c_type, "ident": str(param_num), "is_ptr": is_ptr, "decl": p_decl, "param_num": param_num})
217
212
 
213
+ # The node's inputs, in the order ONNX Runtime presents them. This is
214
+ # the same list GetInputTypeCount() reports, so indexing into it is
215
+ # authoritative. A running counter over manifest.arguments is not: any
216
+ # drift between the two silently points a scalar read at the wrong
217
+ # slot, and the kernel then masks against a garbage element count.
218
+ inputs_to_node = [
219
+ a for a in manifest.arguments
220
+ if (a.kind in ('input', 'inplace')
221
+ or (a.kind == 'scalar' and not getattr(a, 'is_constexpr', False)))
222
+ ]
223
+
224
+ def _node_input_index(a, fallback):
225
+ for k, cand in enumerate(inputs_to_node):
226
+ if cand is a:
227
+ return k
228
+ return fallback
229
+
218
230
  arg_setup_lines.append("// Extract inputs/outputs/scalars from ORT context")
219
231
  onnx_input_counter = 0
220
232
  ort_output_counter = 0
@@ -227,7 +239,7 @@ struct {op_name} : Ort::CustomOpBase<{op_name}, {kernel_name}> {{
227
239
  if _esz == 0.5
228
240
  else f"(in_tensor_{slot_idx}.GetTensorTypeAndShapeInfo().GetElementCount() * {int(_esz)})"
229
241
  )
230
- arg_setup_lines.append(f"auto in_tensor_{slot_idx} = ctx.GetInput({onnx_input_counter});")
242
+ arg_setup_lines.append(f"auto in_tensor_{slot_idx} = ctx.GetInput({_node_input_index(arg, onnx_input_counter)});")
231
243
  arg_setup_lines.append(f"static thread_local const void* arg_ptr_{slot_idx};")
232
244
  arg_setup_lines.append(f"const void* raw_ptr_{slot_idx} = (const void*)in_tensor_{slot_idx}.GetTensorData<uint8_t>();")
233
245
  arg_setup_lines.append(f"static thread_local void* align_buf_{slot_idx} = nullptr;")
@@ -242,6 +254,9 @@ struct {op_name} : Ort::CustomOpBase<{op_name}, {kernel_name}> {{
242
254
  elif arg.kind == 'output':
243
255
  out_dim_exprs = []
244
256
  for dim_idx, d in enumerate(arg.shape):
257
+ if dim_idx in _dyn_axes:
258
+ out_dim_exprs.append(f"(int64_t)(dim_values.size() > {dim_idx} ? dim_values[{dim_idx}] : 1)")
259
+ continue
245
260
  try:
246
261
  d_int = int(d)
247
262
  out_dim_exprs.append(str(d_int))
@@ -257,6 +272,9 @@ struct {op_name} : Ort::CustomOpBase<{op_name}, {kernel_name}> {{
257
272
  elif arg.kind == 'inplace':
258
273
  out_dim_exprs = []
259
274
  for dim_idx, d in enumerate(arg.shape):
275
+ if dim_idx in _dyn_axes:
276
+ out_dim_exprs.append(f"(int64_t)(dim_values.size() > {dim_idx} ? dim_values[{dim_idx}] : 1)")
277
+ continue
260
278
  try:
261
279
  d_int = int(d)
262
280
  out_dim_exprs.append(str(d_int))
@@ -264,7 +282,7 @@ struct {op_name} : Ort::CustomOpBase<{op_name}, {kernel_name}> {{
264
282
  out_dim_exprs.append(f"(int64_t)(dim_values.size() > {dim_idx} ? dim_values[{dim_idx}] : 1)")
265
283
  out_shape_str = "{" + ", ".join(out_dim_exprs) + "}" if out_dim_exprs else "dim_values"
266
284
 
267
- arg_setup_lines.append(f"auto in_tensor_{slot_idx} = ctx.GetInput({onnx_input_counter});")
285
+ arg_setup_lines.append(f"auto in_tensor_{slot_idx} = ctx.GetInput({_node_input_index(arg, onnx_input_counter)});")
268
286
  arg_setup_lines.append(f"std::vector<int64_t> out_dims_{ort_output_counter} = {out_shape_str};")
269
287
  arg_setup_lines.append(f"auto out_tensor_{slot_idx} = ctx.GetOutput({ort_output_counter}, out_dims_{ort_output_counter}.data(), out_dims_{ort_output_counter}.size());")
270
288
  _esz = element_size_bytes(arg.dtype)
@@ -282,16 +300,33 @@ struct {op_name} : Ort::CustomOpBase<{op_name}, {kernel_name}> {{
282
300
  onnx_input_counter += 1
283
301
  ort_output_counter += 1
284
302
  elif arg.kind == 'scalar':
285
- if not getattr(arg, 'is_constexpr', False):
286
- arg_setup_lines.append(f"auto scalar_tensor_{slot_idx} = ctx.GetInput({onnx_input_counter});")
303
+ expr = getattr(arg, 'cxx_expr', '') or ''
304
+ if 'dim_values' in expr:
305
+ # Shape-derived (n = x.numel()): compute it from the tensor
306
+ # being run. The exported graph folds this scalar into a
307
+ # constant -- it is not a graph input -- so reading the slot
308
+ # would hand the kernel the element count captured while
309
+ # tracing, and it would then process a fraction of the
310
+ # tensor and leave the rest untouched.
311
+ arg_setup_lines.append(f"double scalar_val_{slot_idx} = (double)({expr});")
312
+ onnx_input_counter += 1
313
+ elif not getattr(arg, 'is_constexpr', False):
314
+ arg_setup_lines.append(f"auto scalar_tensor_{slot_idx} = ctx.GetInput({_node_input_index(arg, onnx_input_counter)});")
287
315
  arg_setup_lines.append(f"double scalar_val_{slot_idx} = 0;")
288
316
  arg_setup_lines.append(f"if (scalar_tensor_{slot_idx}.GetTensorTypeAndShapeInfo().GetElementType() == ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64) {{ scalar_val_{slot_idx} = (double)(scalar_tensor_{slot_idx}.GetTensorData<int64_t>()[0]); }}")
289
317
  arg_setup_lines.append(f"else if (scalar_tensor_{slot_idx}.GetTensorTypeAndShapeInfo().GetElementType() == ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32) {{ scalar_val_{slot_idx} = (double)(scalar_tensor_{slot_idx}.GetTensorData<int32_t>()[0]); }}")
290
318
  arg_setup_lines.append(f"else {{ scalar_val_{slot_idx} = (double)(scalar_tensor_{slot_idx}.GetTensorData<float>()[0]); }}")
291
319
  onnx_input_counter += 1
292
320
  else:
293
- s_val = arg.value if getattr(arg, 'value', None) is not None else 0.0
294
- arg_setup_lines.append(f"double scalar_val_{slot_idx} = {s_val};")
321
+ # Prefer the manifest's translated expression: for a scalar
322
+ # derived from shapes (n = x.numel()) that is a runtime read
323
+ # of the input dims; otherwise it is the baked constant.
324
+ expr = getattr(arg, 'cxx_expr', '') or None
325
+ if expr:
326
+ arg_setup_lines.append(f"double scalar_val_{slot_idx} = (double)({expr});")
327
+ else:
328
+ s_val = arg.value if getattr(arg, 'value', None) is not None else 0.0
329
+ arg_setup_lines.append(f"double scalar_val_{slot_idx} = {s_val};")
295
330
 
296
331
  arg_setup_lines.append("static thread_local const void* null_ptr_arg = nullptr;")
297
332
  arg_setup_lines.append("static thread_local std::vector<void*> kp;")
@@ -28,37 +28,31 @@ class TensorRTPluginGenerator:
28
28
  self.plugin_namespace = plugin_namespace
29
29
  self.plugin_version = plugin_version
30
30
 
31
- def _generate_kernel_h(self, manifest: KernelManifest) -> str:
32
- plugin_name = f"{manifest.kernel_name}Plugin"
33
-
34
- constant_decls = []
35
- for arg in manifest.arguments:
36
- if arg.kind == 'scalar':
37
- ctype = "float" if isinstance(arg.value, float) else "int"
38
- constant_decls.append(f"{ctype} m_{arg.name};")
39
- dynamic_members_cpp = "\n ".join(constant_decls)
40
-
41
- def get_trt_datatype(dtype_obj):
42
- dtype_str = str(dtype_obj).lower()
43
- if 'float8' in dtype_str or 'fp8' in dtype_str or 'e4m3' in dtype_str or 'e5m2' in dtype_str:
44
- return "nvinfer1::DataType::kFP8"
45
- elif 'bool' in dtype_str:
46
- return "nvinfer1::DataType::kBOOL"
47
- elif 'uint8' in dtype_str or 'int8' in dtype_str or 'int4' in dtype_str or 'uint4' in dtype_str:
48
- return "nvinfer1::DataType::kINT8"
49
- elif 'bfloat16' in dtype_str or 'bf16' in dtype_str:
50
- return "nvinfer1::DataType::kBF16"
51
- elif 'float16' in dtype_str or 'half' in dtype_str:
52
- return "nvinfer1::DataType::kHALF"
53
- elif 'float' in dtype_str:
54
- return "nvinfer1::DataType::kFLOAT"
55
- elif 'int64' in dtype_str or 'long' in dtype_str:
56
- return "nvinfer1::DataType::kINT64"
57
- elif 'int' in dtype_str:
58
- return "nvinfer1::DataType::kINT32"
59
- else:
60
- return "nvinfer1::DataType::kFLOAT"
61
-
31
+ @staticmethod
32
+ def trt_datatype(dtype_obj: str) -> str:
33
+ """Map a torch dtype onto the TensorRT DataType."""
34
+ dtype_str = str(dtype_obj).lower()
35
+ if 'float8' in dtype_str or 'fp8' in dtype_str or 'e4m3' in dtype_str or 'e5m2' in dtype_str:
36
+ return "nvinfer1::DataType::kFP8"
37
+ elif 'bool' in dtype_str:
38
+ return "nvinfer1::DataType::kBOOL"
39
+ elif 'uint8' in dtype_str or 'int8' in dtype_str or 'int4' in dtype_str or 'uint4' in dtype_str:
40
+ return "nvinfer1::DataType::kINT8"
41
+ elif 'bfloat16' in dtype_str or 'bf16' in dtype_str:
42
+ return "nvinfer1::DataType::kBF16"
43
+ elif 'float16' in dtype_str or 'half' in dtype_str:
44
+ return "nvinfer1::DataType::kHALF"
45
+ elif 'float' in dtype_str:
46
+ return "nvinfer1::DataType::kFLOAT"
47
+ elif 'int64' in dtype_str or 'long' in dtype_str:
48
+ return "nvinfer1::DataType::kINT64"
49
+ elif 'int' in dtype_str:
50
+ return "nvinfer1::DataType::kINT32"
51
+ else:
52
+ return "nvinfer1::DataType::kFLOAT"
53
+ @staticmethod
54
+ def supports_format_body(manifest: KernelManifest, tensor_prefix: str = "") -> str:
55
+ """The supportsFormatCombination body, shared by both plugin emitters."""
62
56
  in_args = [a for a in manifest.arguments if a.kind in ('input', 'inplace')]
63
57
 
64
58
  # Enforce the dtype the kernel was compiled for, position by position.
@@ -67,7 +61,7 @@ class TensorRTPluginGenerator:
67
61
  _ordered_in = [a for a in manifest.arguments if a.kind in ('input', 'inplace')]
68
62
  _ordered_sc = [a for a in manifest.arguments if a.kind == 'scalar' and not getattr(a, 'is_constexpr', False)]
69
63
  _ordered_out = [a for a in manifest.arguments if a.kind in ('output', 'inplace')]
70
- _pos_types = [get_trt_datatype(a.dtype) for a in (_ordered_in + _ordered_sc + _ordered_out)]
64
+ _pos_types = [TensorRTPluginGenerator.trt_datatype(a.dtype) for a in (_ordered_in + _ordered_sc + _ordered_out)]
71
65
  _pos_is_tensor = ([True] * len(_ordered_in) + [False] * len(_ordered_sc) + [True] * len(_ordered_out))
72
66
  _types_cpp = ", ".join(_pos_types) if _pos_types else "nvinfer1::DataType::kFLOAT"
73
67
  _is_tensor_cpp = ", ".join("true" if t else "false" for t in _pos_is_tensor) if _pos_is_tensor else "true"
@@ -96,14 +90,37 @@ class TensorRTPluginGenerator:
96
90
  )
97
91
  else:
98
92
  supports_format_cxx = type_guard_cxx + "return inOut[pos].format == nvinfer1::TensorFormat::kLINEAR;"
99
-
93
+ # IPluginV3 hands supportsFormatCombination a DynamicPluginTensorDesc,
94
+ # whose fields live under .desc; V2 hands it a PluginTensorDesc directly.
95
+ if tensor_prefix:
96
+ supports_format_cxx = supports_format_cxx.replace("inOut[pos].", f"inOut[pos]{tensor_prefix}.")
97
+ return supports_format_cxx
98
+
99
+ def _generate_kernel_h(self, manifest: KernelManifest) -> str:
100
+ plugin_name = f"{manifest.kernel_name}Plugin"
101
+
102
+ constant_decls = []
103
+ for arg in manifest.arguments:
104
+ if arg.kind == 'scalar':
105
+ ctype = "float" if isinstance(arg.value, float) else "int"
106
+ constant_decls.append(f"{ctype} m_{arg.name};")
107
+ dynamic_members_cpp = "\n ".join(constant_decls)
108
+
109
+ get_trt_datatype = TensorRTPluginGenerator.trt_datatype
110
+ supports_format_cxx = TensorRTPluginGenerator.supports_format_body(manifest)
100
111
  output_dim_cases = []
101
112
  out_args = [a for a in manifest.arguments if a.kind in ('output', 'inplace')]
113
+ _dyn_axes = tuple(getattr(manifest, "dynamic_axes", ()) or ())
102
114
  for out_idx, out_arg in enumerate(out_args):
103
115
  output_dim_cases.append(f"if (outputIndex == {out_idx}) {{")
104
116
  output_dim_cases.append(f" nvinfer1::DimsExprs res;")
105
117
  output_dim_cases.append(f" res.nbDims = {len(out_arg.shape)};")
106
118
  for d_idx, d in enumerate(out_arg.shape):
119
+ if d_idx in _dyn_axes:
120
+ # Declared dynamic: take the dim from the input at run time
121
+ # instead of freezing the traced value.
122
+ output_dim_cases.append(f" res.d[{d_idx}] = inputs[0].d[{d_idx}];")
123
+ continue
107
124
  try:
108
125
  d_int = int(d)
109
126
  output_dim_cases.append(f" res.d[{d_idx}] = exprBuilder.constant({d_int});")
@@ -211,30 +228,27 @@ private:
211
228
  '''
212
229
  return textwrap.dedent(tpl).strip()
213
230
 
214
- def _generate_kernel_cu(self, manifest: KernelManifest) -> str:
215
- plugin_name = f"{manifest.kernel_name}Plugin"
216
- ptx_str = manifest.ptx
217
- ptx_encoded = json.dumps(ptx_str)
218
- block_size = (manifest.num_warps if manifest.num_warps > 0 else 4) * 32
219
-
231
+ @staticmethod
232
+ def build_shared_body(manifest: KernelManifest, block_size: int):
233
+ """Build the pieces the enqueue implementation needs.
234
+
235
+ Shared verbatim between the IPluginV2DynamicExt and the
236
+ IPluginV3 emitters: the PTX entry name, the kernelParams
237
+ assembly and the output-type dispatch are the same; only the
238
+ class hierarchy and the method signatures around them differ.
239
+ """
220
240
  scalars = [arg for arg in manifest.arguments if arg.kind == 'scalar']
221
241
  nb_outputs = sum(1 for arg in manifest.arguments if arg.kind == 'output')
222
242
  nb_outputs = max(1, nb_outputs)
223
243
 
224
244
  # --- LOCAL TRT GRID AST EVALUATION ---
225
245
 
226
- grid_strs = []
227
- if hasattr(manifest, '_sym_grid_asts') and manifest._sym_grid_asts:
228
- for g in manifest._sym_grid_asts:
229
- expr = str(g.node.expr) if hasattr(g, 'node') else str(g)
230
- expr = re.sub(r'([a-zA-Z0-9_]+)\*\*([a-zA-Z0-9_]+)', r'std::pow(\1, \2)', expr)
231
- expr = expr.replace("//", "/")
232
- expr = re.sub(r'floor\((.*?)\)', r'(\1)', expr)
233
-
234
- # Replace any s(\d+) symbol dynamically using regex
235
- expr = re.sub(r'\bs(\d+)\b', r"(int64_t)(inputDesc[0].dims.nbDims > \1 ? inputDesc[0].dims.d[\1] : 1)", expr)
236
- grid_strs.append(expr)
237
-
246
+ _trt_dim = lambda i: f"(int64_t)(inputDesc[0].dims.nbDims > {i} ? inputDesc[0].dims.d[{i}] : 1)" # noqa: E731
247
+ grid_strs = manifest.grid_exprs(_trt_dim)
248
+ # A scalar derived from the shape (n = x.numel()) must be recomputed per
249
+ # enqueue from the runtime input, or the kernel masks against the traced
250
+ # element count and silently does half the work.
251
+ _scalar_exprs = manifest.scalar_exprs(_trt_dim)
238
252
  while len(grid_strs) < 3:
239
253
  grid_strs.append("1")
240
254
 
@@ -356,7 +370,7 @@ private:
356
370
  arg_setup_lines.append(f"kernelParams[{slot_idx}] = (void*)&tmp_null_{slot_idx};")
357
371
  else:
358
372
  if arg and arg.kind == 'scalar':
359
- expr = getattr(arg, 'cxx_expr', '') or f"m_{arg.name}"
373
+ expr = _scalar_exprs.get(arg.name) or getattr(arg, 'cxx_expr', '') or f"m_{arg.name}"
360
374
  scalar_ctype = "int64_t" if "64" in str(arg.dtype) else ("float" if ("float" in str(arg.dtype) or "float" in c_type or "double" in c_type) else "int32_t")
361
375
  arg_setup_lines.append(f"static thread_local {scalar_ctype} tmp_scalar_{slot_idx}; tmp_scalar_{slot_idx} = ({scalar_ctype})({expr});")
362
376
  arg_setup_lines.append(f"kernelParams[{slot_idx}] = (void*)&tmp_scalar_{slot_idx};")
@@ -388,10 +402,41 @@ private:
388
402
 
389
403
  output_type_lines.append(f"if (index == {i}) return {trt_type};")
390
404
 
391
- dynamic_output_types = "\n ".join(output_type_lines)
405
+ return {
406
+ "ptx_entry_name": ptx_entry_name,
407
+ "dynamic_args_cpp": "\n ".join(arg_setup_lines),
408
+ "dynamic_output_types": "\n ".join(output_type_lines),
409
+ "grid_x_cxx": grid_x_cxx,
410
+ "grid_y_cxx": grid_y_cxx,
411
+ "grid_z_cxx": grid_z_cxx,
412
+ "num_ptx_slots": num_ptx_slots,
413
+ "init_str": init_str,
414
+ "size_additions": size_additions,
415
+ "deserialize_cpp": deserialize_cpp,
416
+ "serialize_cpp": serialize_cpp,
417
+ "clone_cpp": clone_cpp,
418
+ }
392
419
 
393
- dynamic_args_cpp = "\n ".join(arg_setup_lines)
420
+ def _generate_kernel_cu(self, manifest: KernelManifest) -> str:
421
+ plugin_name = f"{manifest.kernel_name}Plugin"
422
+ ptx_str = manifest.ptx
423
+ ptx_encoded = json.dumps(ptx_str)
424
+ block_size = (manifest.num_warps if manifest.num_warps > 0 else 4) * 32
394
425
 
426
+ shared = TensorRTPluginGenerator.build_shared_body(manifest, block_size)
427
+ ptx_entry_name = shared["ptx_entry_name"]
428
+ dynamic_args_cpp = shared["dynamic_args_cpp"]
429
+ dynamic_output_types = shared["dynamic_output_types"]
430
+ grid_x_cxx = shared["grid_x_cxx"]
431
+ grid_y_cxx = shared["grid_y_cxx"]
432
+ grid_z_cxx = shared["grid_z_cxx"]
433
+ num_ptx_slots = shared["num_ptx_slots"]
434
+ init_str = shared["init_str"]
435
+ size_additions = shared["size_additions"]
436
+ deserialize_cpp = shared["deserialize_cpp"]
437
+ serialize_cpp = shared["serialize_cpp"]
438
+ clone_cpp = shared["clone_cpp"]
439
+
395
440
  tpl = f'''
396
441
  #include "{manifest.kernel_name}Plugin.h"
397
442
  #include <cuda.h>