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.
Files changed (72) hide show
  1. kernelport/__init__.py +3 -0
  2. kernelport/__main__.py +5 -0
  3. kernelport/_skills/kernelport/SKILL.md +200 -0
  4. kernelport/_skills/kernelport/backends/cpu.md +138 -0
  5. kernelport/_skills/kernelport/backends/hip.md +53 -0
  6. kernelport/_skills/kernelport/backends/metal.md +76 -0
  7. kernelport/_skills/kernelport/backends/triton.md +117 -0
  8. kernelport/_skills/kernelport/backends/webgpu.md +146 -0
  9. kernelport/_skills/kernelport/common_bugs.md +40 -0
  10. kernelport/agent.py +178 -0
  11. kernelport/analyzer/__init__.py +0 -0
  12. kernelport/analyzer/cuda_parser.py +480 -0
  13. kernelport/analyzer/models.py +80 -0
  14. kernelport/analyzer/suggest.py +120 -0
  15. kernelport/backends/__init__.py +5 -0
  16. kernelport/backends/base.py +138 -0
  17. kernelport/backends/cpu/__init__.py +1 -0
  18. kernelport/backends/cpu/capability.py +25 -0
  19. kernelport/backends/cpu/config.py +36 -0
  20. kernelport/backends/cpu/runner.py +67 -0
  21. kernelport/backends/hip/__init__.py +1 -0
  22. kernelport/backends/hip/capability.py +34 -0
  23. kernelport/backends/hip/config.py +28 -0
  24. kernelport/backends/hip/runner.py +48 -0
  25. kernelport/backends/metal/__init__.py +1 -0
  26. kernelport/backends/metal/capability.py +32 -0
  27. kernelport/backends/metal/config.py +35 -0
  28. kernelport/backends/metal/runner.py +142 -0
  29. kernelport/backends/triton/__init__.py +1 -0
  30. kernelport/backends/triton/capability.py +29 -0
  31. kernelport/backends/triton/config.py +41 -0
  32. kernelport/backends/triton/runner.py +44 -0
  33. kernelport/backends/webgpu/__init__.py +1 -0
  34. kernelport/backends/webgpu/capability.py +34 -0
  35. kernelport/backends/webgpu/config.py +38 -0
  36. kernelport/backends/webgpu/runner.py +285 -0
  37. kernelport/bench/__init__.py +0 -0
  38. kernelport/bench/timer.py +76 -0
  39. kernelport/cli.py +501 -0
  40. kernelport/doctor.py +285 -0
  41. kernelport/hardware.py +60 -0
  42. kernelport/harness/__init__.py +0 -0
  43. kernelport/harness/compare.py +152 -0
  44. kernelport/harness/device_source.py +273 -0
  45. kernelport/harness/expr.py +80 -0
  46. kernelport/harness/inputs.py +194 -0
  47. kernelport/harness/reference.py +76 -0
  48. kernelport/harness/results.py +118 -0
  49. kernelport/harness/run_cuda.py +126 -0
  50. kernelport/harness/spec.py +199 -0
  51. kernelport/harness/tolerances.py +48 -0
  52. kernelport/harness/verify.py +131 -0
  53. kernelport/hashing.py +28 -0
  54. kernelport/mcp_server.py +95 -0
  55. kernelport/report/__init__.py +1 -0
  56. kernelport/report/data.py +147 -0
  57. kernelport/report/html.py +174 -0
  58. kernelport/report/markdown.py +75 -0
  59. kernelport/setup_agents/__init__.py +1 -0
  60. kernelport/setup_agents/backup.py +59 -0
  61. kernelport/setup_agents/detect.py +78 -0
  62. kernelport/setup_agents/register.py +302 -0
  63. kernelport/setup_agents/skills_install.py +31 -0
  64. kernelport/tools.py +455 -0
  65. kernelport/tune.py +99 -0
  66. kernelport/workspace.py +400 -0
  67. kernelport/zoo.py +176 -0
  68. kernelport-0.1.0.dist-info/METADATA +228 -0
  69. kernelport-0.1.0.dist-info/RECORD +72 -0
  70. kernelport-0.1.0.dist-info/WHEEL +4 -0
  71. kernelport-0.1.0.dist-info/entry_points.txt +2 -0
  72. kernelport-0.1.0.dist-info/licenses/LICENSE +21 -0
kernelport/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """kernelport: port CUDA kernels to other backends and prove they are correct."""
2
+
3
+ __version__ = "0.1.0"
kernelport/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """`python -m kernelport` == the `kernelport` CLI."""
2
+
3
+ from kernelport.cli import app
4
+
5
+ app()
@@ -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.