kernelport 0.1.0__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.
- kernelport/__init__.py +3 -0
- kernelport/__main__.py +5 -0
- kernelport/_skills/kernelport/SKILL.md +200 -0
- kernelport/_skills/kernelport/backends/cpu.md +138 -0
- kernelport/_skills/kernelport/backends/hip.md +53 -0
- kernelport/_skills/kernelport/backends/metal.md +76 -0
- kernelport/_skills/kernelport/backends/triton.md +117 -0
- kernelport/_skills/kernelport/backends/webgpu.md +146 -0
- kernelport/_skills/kernelport/common_bugs.md +40 -0
- kernelport/agent.py +178 -0
- kernelport/analyzer/__init__.py +0 -0
- kernelport/analyzer/cuda_parser.py +480 -0
- kernelport/analyzer/models.py +80 -0
- kernelport/analyzer/suggest.py +120 -0
- kernelport/backends/__init__.py +5 -0
- kernelport/backends/base.py +138 -0
- kernelport/backends/cpu/__init__.py +1 -0
- kernelport/backends/cpu/capability.py +25 -0
- kernelport/backends/cpu/config.py +36 -0
- kernelport/backends/cpu/runner.py +67 -0
- kernelport/backends/hip/__init__.py +1 -0
- kernelport/backends/hip/capability.py +34 -0
- kernelport/backends/hip/config.py +28 -0
- kernelport/backends/hip/runner.py +48 -0
- kernelport/backends/metal/__init__.py +1 -0
- kernelport/backends/metal/capability.py +32 -0
- kernelport/backends/metal/config.py +35 -0
- kernelport/backends/metal/runner.py +142 -0
- kernelport/backends/triton/__init__.py +1 -0
- kernelport/backends/triton/capability.py +29 -0
- kernelport/backends/triton/config.py +41 -0
- kernelport/backends/triton/runner.py +44 -0
- kernelport/backends/webgpu/__init__.py +1 -0
- kernelport/backends/webgpu/capability.py +34 -0
- kernelport/backends/webgpu/config.py +38 -0
- kernelport/backends/webgpu/runner.py +285 -0
- kernelport/bench/__init__.py +0 -0
- kernelport/bench/timer.py +76 -0
- kernelport/cli.py +501 -0
- kernelport/doctor.py +285 -0
- kernelport/hardware.py +60 -0
- kernelport/harness/__init__.py +0 -0
- kernelport/harness/compare.py +152 -0
- kernelport/harness/device_source.py +273 -0
- kernelport/harness/expr.py +80 -0
- kernelport/harness/inputs.py +194 -0
- kernelport/harness/reference.py +76 -0
- kernelport/harness/results.py +118 -0
- kernelport/harness/run_cuda.py +126 -0
- kernelport/harness/spec.py +199 -0
- kernelport/harness/tolerances.py +48 -0
- kernelport/harness/verify.py +131 -0
- kernelport/hashing.py +28 -0
- kernelport/mcp_server.py +95 -0
- kernelport/report/__init__.py +1 -0
- kernelport/report/data.py +147 -0
- kernelport/report/html.py +174 -0
- kernelport/report/markdown.py +75 -0
- kernelport/setup_agents/__init__.py +1 -0
- kernelport/setup_agents/backup.py +59 -0
- kernelport/setup_agents/detect.py +78 -0
- kernelport/setup_agents/register.py +302 -0
- kernelport/setup_agents/skills_install.py +31 -0
- kernelport/tools.py +455 -0
- kernelport/tune.py +99 -0
- kernelport/workspace.py +400 -0
- kernelport/zoo.py +176 -0
- kernelport-0.1.0.dist-info/METADATA +228 -0
- kernelport-0.1.0.dist-info/RECORD +72 -0
- kernelport-0.1.0.dist-info/WHEEL +4 -0
- kernelport-0.1.0.dist-info/entry_points.txt +2 -0
- kernelport-0.1.0.dist-info/licenses/LICENSE +21 -0
kernelport/__init__.py
ADDED
kernelport/__main__.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kernelport
|
|
3
|
+
description: Port CUDA kernels to Triton, Metal (MLX), WebGPU (WGSL), CPU (Numba) or HIP and PROVE each port correct with the kernelport MCP tools. Use when the user asks to port, convert, translate or verify CUDA kernels on other hardware.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# kernelport: port CUDA kernels and prove they work
|
|
7
|
+
|
|
8
|
+
You write the code. kernelport runs it on real hardware and checks it against a
|
|
9
|
+
trusted reference. **A port only counts as working when `verify_port` returns
|
|
10
|
+
`PASS`.** Nothing else (compiling, looking right, passing your own test) counts.
|
|
11
|
+
|
|
12
|
+
## Hard rules
|
|
13
|
+
|
|
14
|
+
1. The **original CUDA kernel is the ground truth for behaviour**. Never edit it.
|
|
15
|
+
2. The **reference is the source of truth for ports**. If the reference and the
|
|
16
|
+
original disagree, fix the reference, never the original.
|
|
17
|
+
3. Never say a port works unless `verify_port` returned `PASS` for its current code.
|
|
18
|
+
Report `FAIL`, `UNVERIFIED` and `SKIPPED` exactly as returned.
|
|
19
|
+
4. Before every retry, write one or two sentences explaining the bug you suspect,
|
|
20
|
+
based on the failure report. Then fix and verify again.
|
|
21
|
+
5. Stop after **6 failed `verify_port` attempts** for a kernel/backend and report it as FAIL
|
|
22
|
+
(the tool tells you `attempts_remaining`).
|
|
23
|
+
6. Do not try to change tolerances or inputs to make a port pass. Tolerances are
|
|
24
|
+
fixed by kernelport. If you believe a tolerance or a test case is wrong, stop
|
|
25
|
+
and explain to the user instead.
|
|
26
|
+
7. Keep ports readable: short comments mapping each part to the CUDA original.
|
|
27
|
+
|
|
28
|
+
## Workflow (call the tools in this order)
|
|
29
|
+
|
|
30
|
+
| Step | Do this | Tool |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| 0 | Check which backends can run here | `kernelport_doctor()` |
|
|
33
|
+
| 1 SCAN | Find kernels | `scan_kernels(path)` |
|
|
34
|
+
| 2 READ | Get source, parsed params, launch sites, suggested input spec | `read_kernel(kernel_id)` |
|
|
35
|
+
| 3 REFERENCE | Write a plain-PyTorch reference + complete input spec | `write_reference(kernel_id, code, spec)` |
|
|
36
|
+
| 4 VALIDATE | Run the ORIGINAL CUDA kernel vs your reference. PASS freezes it | `validate_reference(kernel_id)` |
|
|
37
|
+
| 5 TRANSLATE | Read the backend guide, write the port | `read_skill("<backend>")`, `write_port(kernel_id, backend, code)` |
|
|
38
|
+
| 6 VERIFY | Port vs frozen reference on every case | `verify_port(kernel_id, backend)` |
|
|
39
|
+
| 7 REPAIR | Explain suspected bug, fix, verify again (max 6 failures) | `write_port`, `verify_port` |
|
|
40
|
+
| 8 BENCHMARK | Only after PASS | `bench_port(kernel_id, backend)` |
|
|
41
|
+
| 8b TUNE (optional) | Python ports with a `TUNE` dict: verify + time each variant, keep the fastest passing one | `tune_port(kernel_id, backend)` |
|
|
42
|
+
| 9 REPORT | Show the user the status table and the report path | `list_status()`, `make_report()` |
|
|
43
|
+
|
|
44
|
+
Do the steps for one kernel at a time (validate it, port it, verify it) before
|
|
45
|
+
moving to the next; it keeps context small. Read `read_skill("common_bugs")` once
|
|
46
|
+
before your first port.
|
|
47
|
+
|
|
48
|
+
If `validate_reference` returns `UNAVAILABLE` (no NVIDIA GPU here), the reference
|
|
49
|
+
cannot be frozen on this machine. Still write the references and specs for every
|
|
50
|
+
kernel, then tell the user how to freeze them all at once on an NVIDIA machine:
|
|
51
|
+
commit `.kernelport/` and run `kernelport validate --all` there (a free Colab T4
|
|
52
|
+
works: `notebooks/freeze_references_colab.ipynb` in the kernelport repo). Once the
|
|
53
|
+
frozen `.kernelport/` is back in the repo, porting and verifying continue here.
|
|
54
|
+
Do not write ports for a kernel whose reference is not frozen: `verify_port` refuses.
|
|
55
|
+
|
|
56
|
+
If the target backend is not available here, still write the port; `verify_port`
|
|
57
|
+
returns `UNVERIFIED` and the port can be verified later with `kernelport verify` on
|
|
58
|
+
the right hardware. Say clearly that it is unverified.
|
|
59
|
+
|
|
60
|
+
## Step 3 in detail: the input spec (launch.json)
|
|
61
|
+
|
|
62
|
+
`read_kernel` returns `input_spec`: a suggestion with `"TODO"` wherever it could not
|
|
63
|
+
infer a value. Fill every TODO; `write_reference` rejects a spec with TODOs.
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
{
|
|
67
|
+
"name": "softmax",
|
|
68
|
+
"source": "softmax.cu",
|
|
69
|
+
"entry": "softmax<{T}>",
|
|
70
|
+
"dtypes": ["float32", "float16", "bfloat16"],
|
|
71
|
+
"params": [
|
|
72
|
+
{"name": "x", "kind": "input", "dtype": "T", "shape": ["rows", "cols"]},
|
|
73
|
+
{"name": "y", "kind": "output", "dtype": "T", "shape": ["rows", "cols"]},
|
|
74
|
+
{"name": "rows", "kind": "scalar", "dtype": "int32", "value": "rows"},
|
|
75
|
+
{"name": "cols", "kind": "scalar", "dtype": "int32", "value": "cols"}
|
|
76
|
+
],
|
|
77
|
+
"launch": {"grid": ["rows"], "block": [256], "shared_mem": 0},
|
|
78
|
+
"cases": [
|
|
79
|
+
{"name": "tiny_1x1", "dims": {"rows": 1, "cols": 1}},
|
|
80
|
+
{"name": "odd_5x1023", "dims": {"rows": 5, "cols": 1023}},
|
|
81
|
+
{"name": "large_4096x4096", "dims": {"rows": 4096, "cols": 4096}}
|
|
82
|
+
],
|
|
83
|
+
"init": {"x": {"dist": "normal", "std": 3.0, "edges": true}},
|
|
84
|
+
"bench_case": "large_4096x4096"
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- **params**: in the exact order of the CUDA signature. `kind` is `input`, `output`
|
|
89
|
+
(kernel only writes it), `inout` (kernel reads and writes it, e.g. `y = a*x + y`,
|
|
90
|
+
`atomicAdd(out, ...)`), or `scalar`. Check the suggested kinds against the source.
|
|
91
|
+
- **dtype**: `"T"` means "each entry of `dtypes`" (for templated kernels). Fixed types:
|
|
92
|
+
`float32 float16 bfloat16 float64 int8 uint8 int16 int32 int64`.
|
|
93
|
+
- **entry**: kernel name; for templates use `{T}` for the type, e.g. `"rmsnorm<{T}, 256>"`.
|
|
94
|
+
Only list `dtypes` the original kernel actually supports (bfloat16 needs `__nv_bfloat16`
|
|
95
|
+
instantiation to make sense).
|
|
96
|
+
- **source**: informational in a repo; kernelport compiles the file the kernel was
|
|
97
|
+
scanned from (`file` in `read_kernel`), with host code stripped, so `main()`, cuBLAS
|
|
98
|
+
calls and `#include <stdio.h>` in that file are fine.
|
|
99
|
+
- **shape / value / grid / block / shared_mem**: expressions over the case `dims`:
|
|
100
|
+
`+ - * / // % **`, `cdiv(a, b)`, `min`, `max`, `next_pow2`. `grid`/`block` must reproduce
|
|
101
|
+
exactly how the host code launches the kernel (see `launches` from `read_kernel`; it
|
|
102
|
+
shows the expressions, so read the host code for the values). If the host picks the
|
|
103
|
+
block size at run time (e.g. benchmarks a list of sizes), choose one it uses, 256 if
|
|
104
|
+
present, and keep grid consistent with it.
|
|
105
|
+
- **dims** may be floats for float scalars: `{"n": 1000, "lr": 0.001, "eps": 1e-8}` with
|
|
106
|
+
`{"name": "lr", "kind": "scalar", "dtype": "float32", "value": "lr"}`. Every reader of
|
|
107
|
+
the case (original, reference, ports) gets the value rounded to the param's dtype, the
|
|
108
|
+
way the kernel receives it (0.999 as float32 is 0.99900001...).
|
|
109
|
+
- **cases**: always include tiny (1 to 7 elements), odd sizes (1000, 1023, 4097),
|
|
110
|
+
a power of two, and one large case. Use sizes that hit every code path (e.g. a row
|
|
111
|
+
longer than one block, a dimension not divisible by the tile size). Keep every case
|
|
112
|
+
inside the kernel's own limits: 32-bit `int` index math (`b*T*V` overflows past
|
|
113
|
+
2^31), vectorised loads that need sizes divisible by 4 or 8, a block size that must
|
|
114
|
+
not exceed some dimension. The reference runs on the CPU in float64, so a large case
|
|
115
|
+
of ~50M elements is plenty. If the original fails only outside such a limit, say so
|
|
116
|
+
in your report and keep the cases inside it; never edit the original.
|
|
117
|
+
- **init** must respect the kernel's input domain. The suggestion from `read_kernel` is
|
|
118
|
+
`normal` + `edges` everywhere; change it where that is invalid: variances and second
|
|
119
|
+
moments `>= 0` (`uniform`, `low: 0`), probabilities in (0, 1] before a `log`, indices
|
|
120
|
+
with `randint` in range.
|
|
121
|
+
- **init** per input param: `dist` = `normal` (`mean`, `std`), `uniform` (`low`, `high`),
|
|
122
|
+
`randint` (`low`, `high` exclusive, for index arrays: keep indices in range!),
|
|
123
|
+
`zeros`, `ones`, `arange`. `edges: true` puts 0, -0, ±1, ±big, ±1/big first
|
|
124
|
+
(`edge_big`, default 1000). Values may be expressions over dims: `"high": "vocab"`.
|
|
125
|
+
Per-case overrides go in the case's own `"init"`.
|
|
126
|
+
- **step** (accumulating kernels: sums, matmuls, scans, atomics): add `"step": 0.0625`
|
|
127
|
+
to every float input. Values become multiples of 1/16, so float32 sums are exact in
|
|
128
|
+
any order and the comparison is not polluted by summation-order rounding.
|
|
129
|
+
- Outputs are pre-filled with NaN so unwritten elements are detected; inouts get
|
|
130
|
+
generated values. Keep `"name"` values valid Python identifiers (`in` -> `in_`).
|
|
131
|
+
|
|
132
|
+
## Step 3 in detail: the reference
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
import torch
|
|
136
|
+
|
|
137
|
+
def reference(x, rows, cols): # names = launch.json param names
|
|
138
|
+
return torch.softmax(x, dim=-1) # one output: return the tensor
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- Arguments: any subset of the `input`/`inout`/`scalar` params, by name.
|
|
142
|
+
- Float inputs arrive as **float64 CPU tensors** (integers keep their dtype). Write
|
|
143
|
+
plain math; do not cast to the kernel dtype; kernelport rounds your result to the
|
|
144
|
+
output dtype before comparing. Scalars arrive as Python numbers, already rounded to
|
|
145
|
+
their declared dtype.
|
|
146
|
+
- Return one tensor (single output) or a dict `{output_name: tensor}`. For `inout`
|
|
147
|
+
params return the **final** value (e.g. `{"y": a * x + y}`).
|
|
148
|
+
- Model what the kernel computes, including its quirks (e.g. biased vs unbiased
|
|
149
|
+
variance, where eps is added, integer division, clamping).
|
|
150
|
+
|
|
151
|
+
`validate_reference` runs the original on every case. On FAIL, read the report,
|
|
152
|
+
fix the **reference** (or the spec: wrong kind, wrong shape, wrong grid), and retry.
|
|
153
|
+
|
|
154
|
+
## Step 6-7: reading a failure report
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
softmax/triton on <GPU>: FAIL (18/24 cases passed)
|
|
158
|
+
- case odd_3x4097 dtype float32:
|
|
159
|
+
y shape [3, 4097]: 4368/12291 values outside tolerance ...; worst at [2, 3075]
|
|
160
|
+
flat[11265:] expected [...]
|
|
161
|
+
flat[11265:] actual [...]
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Use it: which cases fail (only wide rows? only fp16? only tiny?), where the worst
|
|
165
|
+
index is (tail of a row = masking or loop bounds; every element = formula), and
|
|
166
|
+
whether actual is NaN ("unexpected NaN" usually means an element was never written
|
|
167
|
+
or a 0/0). Errors are compact; an `error:` line means a crash or compile error with
|
|
168
|
+
the offending line of your port.
|
|
169
|
+
|
|
170
|
+
## Step 8b: tuning (optional)
|
|
171
|
+
|
|
172
|
+
Only worth it for kernels where speed matters, and only after PASS. Lift the knobs
|
|
173
|
+
you would tune (block size, warps, rows per program) to module-level constants and
|
|
174
|
+
list values to try in a literal `TUNE` dict (at most 32 combinations):
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
BLOCK = 1024
|
|
178
|
+
NUM_WARPS = 4
|
|
179
|
+
TUNE = {"BLOCK": [256, 512, 1024, 2048], "NUM_WARPS": [4, 8]}
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`write_port`, `verify_port` (the port must PASS as written), then `tune_port(kernel_id, backend)`.
|
|
183
|
+
Every variant is verified on every case before it is timed; a variant that FAILs is
|
|
184
|
+
just reported, it does not use your repair budget. If one is at least 3% faster, it
|
|
185
|
+
replaces the port, but only if each of its 5 timing rounds also beat each round of the
|
|
186
|
+
current port; otherwise the summary says the difference was within timing noise and the
|
|
187
|
+
port is kept. Report the result it returns, not your own estimate.
|
|
188
|
+
|
|
189
|
+
`bench_port` times port and original in 3 alternating runs and reports the median with
|
|
190
|
+
the range of the runs. On laptops and shared GPUs the runs can differ a lot; when the
|
|
191
|
+
summary says the runs differed by more than 20%, report the speedup as approximate (with
|
|
192
|
+
the range), or run `bench_port` again on a quieter machine.
|
|
193
|
+
|
|
194
|
+
## Step 9: what to tell the user
|
|
195
|
+
|
|
196
|
+
Call `make_report(format="markdown")` and show its `content` (the status matrix, benchmarks,
|
|
197
|
+
and what needs attention), then give the path of the HTML report from `make_report()`. For each kernel and
|
|
198
|
+
backend state PASS / FAIL / UNVERIFIED / SKIPPED exactly as reported, with the
|
|
199
|
+
hardware it was verified on and the speedup if benchmarked. For FAILs, include the
|
|
200
|
+
last suspected bug. Never round UNVERIFIED up to "should work".
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Porting CUDA to the CPU (Numba)
|
|
2
|
+
|
|
3
|
+
A cpu port is plain Python whose hot loops are compiled by Numba (`@njit`).
|
|
4
|
+
`prange` loops are split across CPU threads, so one `prange` iteration plays the
|
|
5
|
+
role of one CUDA thread block (or one thread, for element-wise kernels). There is
|
|
6
|
+
no shared memory, no `__syncthreads`, no warps: code inside one iteration runs
|
|
7
|
+
sequentially, which makes most block-level CUDA tricks unnecessary.
|
|
8
|
+
|
|
9
|
+
Runs on any machine with Numba (verified so far on Windows x86-64).
|
|
10
|
+
Timings are wall-clock and **not comparable** to GPU kernel times; the report says so.
|
|
11
|
+
|
|
12
|
+
## Port file format
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
"""CPU (Numba) port of <kernel>: <what it computes>."""
|
|
16
|
+
|
|
17
|
+
import math
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
from numba import njit, prange
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@njit(parallel=True)
|
|
24
|
+
def _softmax(x, y, rows, cols):
|
|
25
|
+
for r in prange(rows): # CUDA: one block per row (blockIdx.x)
|
|
26
|
+
m = -math.inf
|
|
27
|
+
for c in range(cols): # CUDA: threads striding over c, then a block reduction
|
|
28
|
+
m = max(m, float(x[r, c]))
|
|
29
|
+
s = 0.0
|
|
30
|
+
for c in range(cols):
|
|
31
|
+
s += math.exp(float(x[r, c]) - m)
|
|
32
|
+
for c in range(cols):
|
|
33
|
+
y[r, c] = math.exp(float(x[r, c]) - m) / s
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def run(x, y, rows, cols):
|
|
37
|
+
_softmax(x, y, rows, cols)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- `run(...)` takes **the launch.json parameter names** (any subset, any order).
|
|
41
|
+
- Tensors arrive as **C-contiguous NumPy arrays with the launch.json shapes**
|
|
42
|
+
(`["rows", "cols"]` gives a 2-D array; index it as `x[r, c]`, or `x.ravel()` for
|
|
43
|
+
the CUDA flat index). Integer params (`int64` ids, positions) keep their dtype.
|
|
44
|
+
- **Write outputs in place** (`y[i] = ...`, `y[:] = ...`). Outputs are pre-filled
|
|
45
|
+
with NaN, so an element you never write shows up as "unexpected NaN".
|
|
46
|
+
`y = np.maximum(x, 0)` only rebinds the local name, and **returning an array is
|
|
47
|
+
rejected** with an error.
|
|
48
|
+
- `inout` params hold their initial values: add to them, do not overwrite
|
|
49
|
+
(`out[0] += total`).
|
|
50
|
+
- `run` itself is ordinary Python and may call several `@njit` functions. The
|
|
51
|
+
kernel's work belongs in `@njit` loops; use NumPy in `run` only for glue
|
|
52
|
+
(views, `ravel`, zero-filling scratch), never `np.cumsum`/`np.matmul`-style
|
|
53
|
+
calls that replace the kernel.
|
|
54
|
+
- Supported dtypes: **float32, float64**. float16/bfloat16 cases are skipped and
|
|
55
|
+
named in the result (NumPy has no bfloat16; Numba does not compile float16 math).
|
|
56
|
+
|
|
57
|
+
## CUDA → Numba mapping
|
|
58
|
+
|
|
59
|
+
| CUDA | Numba (CPU) |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `__global__ void k(...)` + `<<<grid, block>>>` | `@njit(parallel=True) def _k(...)` called from `run` |
|
|
62
|
+
| `i = blockIdx.x * blockDim.x + threadIdx.x; if (i < n)` | `for i in prange(n):` (no guard needed, the range is exact) |
|
|
63
|
+
| one block per row (`blockIdx.x` = row) | `for r in prange(rows):` with sequential inner loops |
|
|
64
|
+
| 2-D grid (`blockIdx.y`, `blockIdx.x`) | `prange` over the outer index, `range` over the inner one |
|
|
65
|
+
| threads striding over a row + shared-memory reduction | a plain `for c in range(cols): acc += ...` inside the iteration |
|
|
66
|
+
| `__shfl_xor_sync` / warp reductions | same: a sequential loop; no warps exist |
|
|
67
|
+
| `__syncthreads()` between phases | nothing: phases inside one iteration already run in order |
|
|
68
|
+
| `__shared__ float tile[32][33]` | not needed; for cache-friendly access loop over tiles (see transpose) |
|
|
69
|
+
| per-block scratch (`__shared__` buffer) | `buf = np.empty(n)` inside the `prange` body (one per iteration) |
|
|
70
|
+
| `atomicAdd(out, partial)` from every block | prange **scalar** reduction, then one write: `acc += v` in the loop, `out[0] += acc` after |
|
|
71
|
+
| `atomicAdd(&hist[bin], 1)` (scatter) | **no atomics**: serial loop, or per-thread private histograms merged after |
|
|
72
|
+
| `expf`, `logf`, `sqrtf`, `rsqrtf(x)` | `math.exp`, `math.log`, `math.sqrt`, `1.0 / math.sqrt(x)` |
|
|
73
|
+
| `fmaxf(a, b)` / `fminf` | `max(a, b)` / `min(a, b)` |
|
|
74
|
+
| `(float)x[i]` on half inputs | `float(x[i])` (computes in float64) |
|
|
75
|
+
| `long long` index / `size_t` offsets | Python ints in Numba are int64; no overflow at these sizes |
|
|
76
|
+
| `__launch_bounds__`, block size, `#pragma unroll` | ignore; Numba/LLVM vectorises inner loops itself |
|
|
77
|
+
|
|
78
|
+
## Patterns
|
|
79
|
+
|
|
80
|
+
**Element-wise** (vector_add, relu, saxpy): one `prange` over `n`.
|
|
81
|
+
|
|
82
|
+
**Row-wise reductions** (softmax, layernorm, rmsnorm): `prange` over rows; each
|
|
83
|
+
iteration does the kernel's phases (max, sum, normalise) as sequential loops over
|
|
84
|
+
the row. Keep the kernel's math: two-pass mean/variance stays two-pass, biased
|
|
85
|
+
variance stays biased (`/ cols`, not `/ (cols - 1)`).
|
|
86
|
+
|
|
87
|
+
**Whole-array reduction** (reduce_sum): scalar reduction across a `prange`:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
@njit(parallel=True)
|
|
91
|
+
def _sum(x, n):
|
|
92
|
+
acc = 0.0
|
|
93
|
+
for i in prange(n):
|
|
94
|
+
acc += x[i] # Numba recognises scalar +=, *=, and m = max(m, ...) as reductions
|
|
95
|
+
return acc
|
|
96
|
+
|
|
97
|
+
def run(x, out, n):
|
|
98
|
+
out[0] += _sum(x, n) # out is inout: add
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Scans** (cumsum): `prange` over rows, a running sum along the row. A
|
|
102
|
+
Hillis-Steele or Blelloch scan is a GPU technique; on the CPU the sequential scan
|
|
103
|
+
is both simpler and faster. Inclusive vs exclusive must match the kernel.
|
|
104
|
+
|
|
105
|
+
**Matmul**: `prange` over output rows; put `k` outside `j` so `B[k, :]` is read
|
|
106
|
+
contiguously and the `j` loop vectorises; accumulate a row in a float64 buffer.
|
|
107
|
+
|
|
108
|
+
**Transpose / strided access**: tile both loops (e.g. 32×32) so reads and writes
|
|
109
|
+
stay in cache. Correctness does not depend on it, only speed.
|
|
110
|
+
|
|
111
|
+
**Gathers** (embedding, rope): `prange` over tokens; index with the int64 ids
|
|
112
|
+
directly (`table[ids[t], d]`).
|
|
113
|
+
|
|
114
|
+
## Precision notes
|
|
115
|
+
|
|
116
|
+
- Float literals are float64: `0.5 * x[i]` with a float32 `x` computes in float64.
|
|
117
|
+
`float32 * float32` stays float32; `math.exp` of a float32 returns float32.
|
|
118
|
+
- Computing in float64 (`float(x[i])`, `acc = 0.0`) is **always allowed and
|
|
119
|
+
recommended**: it costs little on a CPU and is at least as accurate as the
|
|
120
|
+
kernel's float math. The result is rounded to float32 on store.
|
|
121
|
+
- Leave `fastmath` off (the default). `fastmath=True` lets LLVM reorder sums and
|
|
122
|
+
assume no NaN/inf; the tolerances are not loosened for it.
|
|
123
|
+
- Scalar Python args (`a`, `eps`) arrive as Python floats, i.e. float64.
|
|
124
|
+
|
|
125
|
+
## CPU-specific bugs checklist
|
|
126
|
+
|
|
127
|
+
- [ ] Every output element written in place; nothing returned from `run`.
|
|
128
|
+
- [ ] `inout` params added to (or updated), not overwritten.
|
|
129
|
+
- [ ] No writes to the **same array element from different `prange` iterations**
|
|
130
|
+
(`hist[0] += 1` or `out[0] += v` inside `prange` is a data race; Numba has no
|
|
131
|
+
atomics on the CPU and silently loses updates). Reduce into a scalar instead.
|
|
132
|
+
- [ ] Inner loops use `range`, not `prange` (nested `prange` runs serially anyway,
|
|
133
|
+
and races are easy to introduce).
|
|
134
|
+
- [ ] Loop bounds match the kernel's guards (`range(n)`, not `range(n - 1)`).
|
|
135
|
+
- [ ] No `import` inside an `@njit` function (unsupported); import at module top.
|
|
136
|
+
- [ ] Only NumPy/`math` calls Numba supports inside `@njit` (no torch, no Python
|
|
137
|
+
objects, no `print` of arrays in hot loops).
|
|
138
|
+
- [ ] No `cache=True`: it writes cache files next to the port inside `.kernelport/`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Porting CUDA to HIP (AMD GPUs)
|
|
2
|
+
|
|
3
|
+
HIP is AMD's CUDA-like C++ dialect: most CUDA kernels port by swapping headers and a
|
|
4
|
+
few intrinsics. A hip port is a `.hip` source file that kernelport compiles at runtime
|
|
5
|
+
with hipRTC (through CuPy's ROCm build) and **launches exactly like the original**:
|
|
6
|
+
same entry name (`launch.json` `entry`, `{T}` filled with HIP type names), same
|
|
7
|
+
parameter order, same grid, block and shared memory.
|
|
8
|
+
|
|
9
|
+
Verification needs an AMD GPU with ROCm torch and ROCm CuPy (e.g. `cupy-rocm-6-0`). On
|
|
10
|
+
other machines `verify_port` returns UNVERIFIED. **Status: the runner's launch path is
|
|
11
|
+
exercised on NVIDIA in tests, but hipRTC compilation and the zoo ports have not yet been
|
|
12
|
+
verified on AMD hardware.**
|
|
13
|
+
|
|
14
|
+
If you only need AMD support, a Triton port also runs on ROCm unchanged; HIP is for
|
|
15
|
+
staying close to the CUDA source.
|
|
16
|
+
|
|
17
|
+
## Port file format
|
|
18
|
+
|
|
19
|
+
```cpp
|
|
20
|
+
// HIP port of vector_add.
|
|
21
|
+
#include <hip/hip_fp16.h>
|
|
22
|
+
#include <hip/hip_bf16.h>
|
|
23
|
+
|
|
24
|
+
template <typename T>
|
|
25
|
+
__global__ void vector_add(const T* __restrict__ a, const T* __restrict__ b, T* __restrict__ out, int n) {
|
|
26
|
+
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
|
27
|
+
if (i < n) out[i] = a[i] + b[i];
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- Keep the kernel name, template parameters and parameter order of the original.
|
|
32
|
+
The launch comes from launch.json, so the kernel must work with the original
|
|
33
|
+
grid/block (loop inside the kernel if you need a different mapping).
|
|
34
|
+
- `{T}` becomes `float`, `__half` or `__hip_bfloat16`.
|
|
35
|
+
|
|
36
|
+
## CUDA → HIP mapping
|
|
37
|
+
|
|
38
|
+
| CUDA | HIP |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `#include <cuda_fp16.h>` / `<cuda_bf16.h>` | `#include <hip/hip_fp16.h>` / `<hip/hip_bf16.h>` |
|
|
41
|
+
| `__nv_bfloat16` | `__hip_bfloat16` |
|
|
42
|
+
| `blockIdx`, `threadIdx`, `__syncthreads`, `__shared__`, `atomicAdd` | same |
|
|
43
|
+
| `__shfl_xor_sync(0xffffffff, v, off)` | `__shfl_xor(v, off)`. **Wavefronts are 64 lanes on CDNA (MI-series), 32 on RDNA**: use `warpSize`, never a hard-coded 32 |
|
|
44
|
+
| warp-level reductions sized for 32 lanes | a shared-memory tree reduction (wave-size independent), or loops over `warpSize` |
|
|
45
|
+
| `__ballot_sync` / masks (32-bit) | `__ballot` returns a 64-bit mask |
|
|
46
|
+
| `cudaStream_t`, host API | not used: kernelport does the launch |
|
|
47
|
+
|
|
48
|
+
## Bugs checklist
|
|
49
|
+
|
|
50
|
+
- [ ] No `0xffffffff`-masked `*_sync` intrinsics and no hard-coded warp size 32.
|
|
51
|
+
- [ ] Shared arrays sized by the block size, not by warp counts that assume 32 lanes.
|
|
52
|
+
- [ ] Same entry name, template parameters and parameter order as launch.json.
|
|
53
|
+
- [ ] HIP headers and type names (`__hip_bfloat16`).
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Porting CUDA to Metal (MLX custom kernels)
|
|
2
|
+
|
|
3
|
+
A metal port is the **body** of a Metal compute kernel. MLX (`mx.fast.metal_kernel`)
|
|
4
|
+
writes the signature, compiles it, and launches it on the Apple GPU. A CUDA thread
|
|
5
|
+
block is a Metal **threadgroup**, `__shared__` is `threadgroup`, `__syncthreads()` is
|
|
6
|
+
`threadgroup_barrier(mem_flags::mem_threadgroup)`.
|
|
7
|
+
|
|
8
|
+
Verification needs an Apple Silicon Mac with MLX. On other machines `verify_port`
|
|
9
|
+
returns UNVERIFIED. Status: all 12 zoo metal ports PASS every case in fp32, fp16 and
|
|
10
|
+
bf16 on Apple Silicon (an M1 on GitHub's macOS runners, checked in CI on every push).
|
|
11
|
+
Treat the zoo ports as working examples of the patterns below.
|
|
12
|
+
|
|
13
|
+
## Port file format
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
"""Metal (MLX) port of saxpy: y = a * x + y."""
|
|
17
|
+
|
|
18
|
+
SOURCE = """
|
|
19
|
+
uint i = thread_position_in_grid.x;
|
|
20
|
+
if (i < uint(n[0])) {
|
|
21
|
+
y_out[i] = static_cast<T>(a[0] * static_cast<float>(x[i]) + static_cast<float>(y[i]));
|
|
22
|
+
}
|
|
23
|
+
"""
|
|
24
|
+
THREADGROUP = (256, 1, 1)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def grid(n):
|
|
28
|
+
return (n, 1, 1) # TOTAL threads, not threadgroups
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
- **`grid()` returns total threads per dimension** (Metal `dispatchThreads`), unlike
|
|
32
|
+
CUDA's block counts. One threadgroup per row of 256 threads: `(rows * 256, 1, 1)`.
|
|
33
|
+
- Names (launch.json order), generated by MLX:
|
|
34
|
+
- inputs: every `input` and `inout` tensor, then **every scalar as a one-element
|
|
35
|
+
array**: read `n[0]`, `eps[0]`;
|
|
36
|
+
- outputs: every `output` by name; **every `inout` as `<name>_out`**. MLX outputs are
|
|
37
|
+
fresh buffers, so read the old value from the input `<name>` and write `<name>_out`.
|
|
38
|
+
- `T` is a template parameter when launch.json uses dtype "T" (`float`, `half`,
|
|
39
|
+
`bfloat16_t`). Do arithmetic in float: `static_cast<float>(x[i])`, store with
|
|
40
|
+
`static_cast<T>(v)`.
|
|
41
|
+
- Outputs start as NaN (`INIT_VALUE`, override if the algorithm needs another start).
|
|
42
|
+
- `ATOMIC_OUTPUTS = True` declares every output `device atomic<...>*` (for atomicAdd).
|
|
43
|
+
- `HEADER = """..."""` holds helper functions (placed before the kernel).
|
|
44
|
+
- Do not name a local variable like a param (`rows`, `cols`): params are already in scope.
|
|
45
|
+
|
|
46
|
+
## CUDA → Metal mapping
|
|
47
|
+
|
|
48
|
+
| CUDA | Metal (MLX body) |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `blockIdx.x` | `threadgroup_position_in_grid.x` |
|
|
51
|
+
| `threadIdx.x` | `thread_position_in_threadgroup.x` |
|
|
52
|
+
| `blockIdx.x * blockDim.x + threadIdx.x` | `thread_position_in_grid.x` |
|
|
53
|
+
| `blockDim.x` / `gridDim.x` | `threads_per_threadgroup.x` / `threadgroups_per_grid.x` |
|
|
54
|
+
| `__shared__ float red[256];` | `threadgroup float red[256];` |
|
|
55
|
+
| `__syncthreads()` | `threadgroup_barrier(mem_flags::mem_threadgroup);` |
|
|
56
|
+
| warp shuffles (32 lanes) | `simd_sum`, `simd_max`, `simd_shuffle_xor` (SIMD width 32 on Apple GPUs), or a threadgroup tree |
|
|
57
|
+
| `atomicAdd(float*, v)` | `ATOMIC_OUTPUTS = True` + `atomic_fetch_add_explicit(&out_out[0], v, memory_order_relaxed)` |
|
|
58
|
+
| `expf`, `sqrtf`, `rsqrtf` | `metal::precise::exp`, `metal::precise::sqrt`, `metal::precise::rsqrt` |
|
|
59
|
+
| `fmaxf` | `metal::max` |
|
|
60
|
+
| `long long` | `int64_t` |
|
|
61
|
+
| scalar param `n` | `n[0]` |
|
|
62
|
+
|
|
63
|
+
## Precision notes
|
|
64
|
+
|
|
65
|
+
- Metal may compile with fast-math, so use **`metal::precise::`** for exp, log, sqrt
|
|
66
|
+
and rsqrt. Tolerances are not loosened.
|
|
67
|
+
- Accumulate in float for half/bfloat16 data.
|
|
68
|
+
|
|
69
|
+
## Metal-specific bugs checklist
|
|
70
|
+
|
|
71
|
+
- [ ] `grid()` is in threads (rows × threadgroup width), not threadgroups.
|
|
72
|
+
- [ ] Scalars read as `name[0]`; inouts written to `name_out`, with the old value read from `name`.
|
|
73
|
+
- [ ] Every `threadgroup_barrier` is reached by all threads (not inside `if (tid < s)`).
|
|
74
|
+
- [ ] Atomic outputs: `INIT_VALUE` set to the identity (0 for sums), and an inout's
|
|
75
|
+
initial value added exactly once.
|
|
76
|
+
- [ ] `metal::precise::` math functions.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Porting CUDA to Triton
|
|
2
|
+
|
|
3
|
+
Triton programs operate on **blocks of values**, not single threads. One Triton
|
|
4
|
+
*program* (`tl.program_id`) does the work of one CUDA *thread block*; the block's
|
|
5
|
+
threads become the lanes of `tl.arange` vectors. You never write `__syncthreads`
|
|
6
|
+
or shared memory by hand: block-wide operations (`tl.sum`, `tl.max`, `tl.cumsum`)
|
|
7
|
+
do it for you.
|
|
8
|
+
|
|
9
|
+
Verified on NVIDIA (triton / triton-windows) and AMD ROCm (same port file).
|
|
10
|
+
|
|
11
|
+
## Port file format
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
"""Triton port of <kernel>: <what it computes>."""
|
|
15
|
+
|
|
16
|
+
import triton
|
|
17
|
+
import triton.language as tl
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@triton.jit
|
|
21
|
+
def _kernel(x_ptr, y_ptr, n, BLOCK: tl.constexpr):
|
|
22
|
+
offs = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK) # CUDA: blockIdx.x * blockDim.x + threadIdx.x
|
|
23
|
+
mask = offs < n # CUDA: if (i < n)
|
|
24
|
+
x = tl.load(x_ptr + offs, mask=mask)
|
|
25
|
+
tl.store(y_ptr + offs, x, mask=mask)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def run(x, y, n):
|
|
29
|
+
BLOCK = 1024
|
|
30
|
+
_kernel[(triton.cdiv(n, BLOCK),)](x, y, n, BLOCK=BLOCK)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- `run(...)` takes the **launch.json param names** (any subset). Tensors are GPU
|
|
34
|
+
torch tensors; outputs are pre-filled with NaN and must be written **in place**.
|
|
35
|
+
Scalars are plain Python numbers. Return value is ignored.
|
|
36
|
+
- Choose your own grid; it does not have to match the CUDA grid.
|
|
37
|
+
- The output dtype is fixed by the tensor you store into: `tl.store` casts a float32
|
|
38
|
+
value to the pointer's element type (fp16/bf16) automatically.
|
|
39
|
+
|
|
40
|
+
## CUDA → Triton mapping
|
|
41
|
+
|
|
42
|
+
| CUDA | Triton |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `blockIdx.x`, `gridDim.x` | `tl.program_id(0)`, `tl.num_programs(0)` |
|
|
45
|
+
| `threadIdx.x` over `blockDim.x` | `tl.arange(0, BLOCK)` (BLOCK must be a power of 2 `tl.constexpr`) |
|
|
46
|
+
| `if (i < n)` bounds check | `mask = offs < n`, pass `mask=` to every `tl.load`/`tl.store` |
|
|
47
|
+
| 2D tile `(ty, tx)` | `rows = r0 + tl.arange(0, BM)[:, None]`, `cols = c0 + tl.arange(0, BN)[None, :]`, `ptr + rows * stride + cols` |
|
|
48
|
+
| grid-stride / chunk loop over a row | `for start in range(0, cols, BLOCK): offs = start + tl.arange(0, BLOCK)` |
|
|
49
|
+
| `__shared__` buffers + `__syncthreads` | keep values in block tensors; no sync needed |
|
|
50
|
+
| block reduction (shared-memory tree) | `tl.sum(x, axis=0)`, `tl.max(x, axis=0)`, `tl.min`, `tl.reduce(x, 0, combine_fn)` |
|
|
51
|
+
| warp shuffles `__shfl_xor_sync` reduce | same as block reduction: `tl.sum` / `tl.max` over the block tensor |
|
|
52
|
+
| prefix sum / scan | `tl.cumsum(x, axis=0)` or `tl.associative_scan(x, 0, combine_fn)`; carry the running total across chunks yourself |
|
|
53
|
+
| `atomicAdd(&p[i], v)` | `tl.atomic_add(p_ptr + i, v, mask=m)`; also `tl.atomic_max/min/xchg/cas` |
|
|
54
|
+
| `float4` vector loads | not needed; contiguous `tl.arange` loads are vectorised automatically |
|
|
55
|
+
| `(float)h` for `__half`/bf16 | `x.to(tl.float32)` |
|
|
56
|
+
| `(T)acc` store | just `tl.store(ptr, acc)`; it is cast to the pointer dtype |
|
|
57
|
+
| `expf`, `rsqrtf`, `sqrtf`, `fmaxf` | `tl.exp`, `tl.rsqrt`, `tl.sqrt_rn` (exact) / `tl.sqrt` (approx), `tl.maximum` |
|
|
58
|
+
| `-INFINITY` | `float("-inf")` |
|
|
59
|
+
| `size_t` index math | cast before multiplying: `row = tl.program_id(0).to(tl.int64)` |
|
|
60
|
+
| gather `table[ids[t] * dim + d]` | `idx = tl.load(ids_ptr + t)`; `tl.load(table_ptr + idx * dim + offs, mask=...)` |
|
|
61
|
+
| matmul inner loop | `acc += tl.dot(a, b, input_precision="ieee")` (see TF32 warning) |
|
|
62
|
+
| `template <typename T>` | nothing to do: Triton specialises on the pointer dtype |
|
|
63
|
+
| `#define BLOCK 256` | `BLOCK: tl.constexpr` argument |
|
|
64
|
+
|
|
65
|
+
## Patterns
|
|
66
|
+
|
|
67
|
+
**Row-wise op on a [rows, cols] matrix (softmax, layernorm, rmsnorm):**
|
|
68
|
+
one program per row. If `cols` may exceed one block, loop over the row in chunks;
|
|
69
|
+
for softmax use an online max/sum (rescale the running sum when the max grows),
|
|
70
|
+
or do one pass for the max and a second for the sum.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
row = tl.program_id(0).to(tl.int64)
|
|
74
|
+
acc = tl.zeros((BLOCK,), tl.float32)
|
|
75
|
+
for start in range(0, cols, BLOCK):
|
|
76
|
+
offs = start + tl.arange(0, BLOCK)
|
|
77
|
+
x = tl.load(x_ptr + row * cols + offs, mask=offs < cols, other=0.0).to(tl.float32)
|
|
78
|
+
acc += x * x
|
|
79
|
+
mean_sq = tl.sum(acc, axis=0) / cols
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Masked loads feeding a reduction:** choose `other=` so masked lanes are neutral:
|
|
83
|
+
`0.0` for sums, `float("-inf")` for max, `float("inf")` for min.
|
|
84
|
+
|
|
85
|
+
**Full reduction to one value:** either `tl.atomic_add` each program's partial into
|
|
86
|
+
the output (the output must start at the value the CUDA kernel expects; inout
|
|
87
|
+
params arrive with their initial values), or one program looping over everything.
|
|
88
|
+
|
|
89
|
+
**Scan across chunks:** `tl.cumsum` within the chunk, add the carry, then
|
|
90
|
+
`carry += tl.sum(chunk)`.
|
|
91
|
+
|
|
92
|
+
**Matmul:** tile with `tl.dot` on `[BM, BK] x [BK, BN]` blocks, accumulate in a
|
|
93
|
+
float32 `tl.zeros((BM, BN), tl.float32)`, mask the K tail with `other=0.0`.
|
|
94
|
+
`tl.dot` needs every block dim ≥ 16.
|
|
95
|
+
|
|
96
|
+
## Precision notes
|
|
97
|
+
|
|
98
|
+
- **TF32:** `tl.dot` on float32 inputs uses TF32 by default (~1e-3 relative error).
|
|
99
|
+
That FAILS verification. Always pass `input_precision="ieee"` for float32.
|
|
100
|
+
- Accumulate in float32 (`.to(tl.float32)`) like the CUDA kernel does; accumulating
|
|
101
|
+
in fp16/bf16 will fail.
|
|
102
|
+
- `tl.sqrt` and `/` use fast approximate instructions (≤ 2 ulp); that is within
|
|
103
|
+
tolerance. If a result must be correctly rounded, use `tl.sqrt_rn` / `tl.div_rn`.
|
|
104
|
+
|
|
105
|
+
## Triton-specific bugs checklist
|
|
106
|
+
|
|
107
|
+
- [ ] Every `tl.load`/`tl.store` that can go out of bounds has a `mask`.
|
|
108
|
+
- [ ] `tl.arange` size is a power of two `constexpr`; use `triton.next_power_of_2(n)`
|
|
109
|
+
and mask the excess, or loop in chunks.
|
|
110
|
+
- [ ] Masked lanes use a neutral `other=` value for the reduction they feed.
|
|
111
|
+
- [ ] Large offsets cast to int64 before multiplying (rows × cols > 2^31).
|
|
112
|
+
- [ ] `run()` writes outputs in place; it does not allocate and return new tensors.
|
|
113
|
+
- [ ] `run()` parameter names match launch.json exactly.
|
|
114
|
+
- [ ] float32 `tl.dot` uses `input_precision="ieee"`.
|
|
115
|
+
- [ ] Integer outputs store integer values (cast with `.to(tl.int32)` etc. if needed).
|
|
116
|
+
- [ ] No Python-side work on tensor values inside `run()` (no `.item()`, no torch
|
|
117
|
+
math replacing the kernel); the port must compute on the GPU in Triton.
|