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
@@ -0,0 +1,41 @@
1
+ """Triton backend configuration."""
2
+
3
+ from kernelport.backends.base import BackendConfig
4
+
5
+ PORT_TEMPLATE = '''"""Triton port of <kernel>: <one line of what it computes>."""
6
+
7
+ import triton
8
+ import triton.language as tl
9
+
10
+
11
+ @triton.jit
12
+ def _kernel(x_ptr, y_ptr, n, BLOCK: tl.constexpr):
13
+ # One program per BLOCK elements (CUDA: one thread block).
14
+ offs = tl.program_id(0) * BLOCK + tl.arange(0, BLOCK)
15
+ mask = offs < n # CUDA: if (i < n)
16
+ x = tl.load(x_ptr + offs, mask=mask)
17
+ tl.store(y_ptr + offs, x, mask=mask)
18
+
19
+
20
+ def run(x, y, n):
21
+ """Same parameter names as launch.json. Tensors are on the GPU; write outputs in place."""
22
+ BLOCK = 1024
23
+ _kernel[(triton.cdiv(n, BLOCK),)](x, y, n, BLOCK=BLOCK)
24
+ '''
25
+
26
+ # No tolerance overrides: Triton emits the same IEEE float32 add/mul/fma as CUDA,
27
+ # and tl.exp / tl.log lower to libdevice-accurate or ex2/lg2-based sequences whose
28
+ # error (a few ulp) is inside the float32 default (rtol 1.3e-6 ~ 11 ulp).
29
+ # Note: tl.dot defaults to TF32 for float32 inputs, which is NOT within tolerance;
30
+ # ports must pass input_precision="ieee" for float32 matmuls.
31
+ CONFIG = BackendConfig(
32
+ name="triton",
33
+ extension=".py",
34
+ description="Triton kernels launched from Python (NVIDIA via triton / triton-windows, AMD via ROCm triton)",
35
+ dtypes=("float32", "float16", "bfloat16"),
36
+ tolerances={},
37
+ timing_comparable=True,
38
+ hardware_hint="NVIDIA GPU (CUDA) or AMD GPU (ROCm)",
39
+ port_template=PORT_TEMPLATE,
40
+ tune_space={"BLOCK": [128, 256, 512, 1024, 2048], "num_warps": [2, 4, 8]},
41
+ )
@@ -0,0 +1,44 @@
1
+ """Run a Triton port.
2
+
3
+ A Triton port is a Python file defining::
4
+
5
+ def run(<same parameter names as launch.json>):
6
+ ...launch one or more @triton.jit kernels...
7
+
8
+ Tensor parameters arrive as GPU tensors (outputs pre-filled with NaN, or a
9
+ sentinel for ints) and must be written in place. Scalars arrive as Python
10
+ numbers. The return value is ignored.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+
17
+ import torch
18
+
19
+ from kernelport.backends.base import Prepared, bind_kwargs
20
+ from kernelport.harness.inputs import CaseData
21
+ from kernelport.harness.reference import load_module
22
+ from kernelport.harness.spec import LaunchSpec
23
+
24
+
25
+ def load_port(path: Path):
26
+ """Import the port and return its run() function."""
27
+ fn = getattr(load_module(path, "kp_triton"), "run", None)
28
+ if fn is None:
29
+ raise AttributeError(f"{Path(path).name} does not define run(...)")
30
+ return fn
31
+
32
+
33
+ def prepare(path: Path, spec: LaunchSpec, data: CaseData) -> Prepared:
34
+ """Load the port and bind it to one case's tensors on the GPU."""
35
+ fn = load_port(path)
36
+ dev = {k: v.to("cuda") for k, v in data.tensors.items()}
37
+ kwargs = bind_kwargs(fn, spec, dev, data.scalars)
38
+ outputs = [p.name for p in spec.params if p.is_output]
39
+ return Prepared(
40
+ launch=lambda: fn(**kwargs),
41
+ sync=torch.cuda.synchronize,
42
+ fetch=lambda: {n: dev[n].cpu() for n in outputs},
43
+ timer="cuda_event",
44
+ )
@@ -0,0 +1 @@
1
+ """WebGPU backend: WGSL compute shaders run through wgpu-native."""
@@ -0,0 +1,34 @@
1
+ """Capability check for the webgpu backend (wgpu-native via the `wgpu` package)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+
7
+
8
+ def is_available() -> tuple[bool, str]:
9
+ """(available, reason)."""
10
+ if importlib.util.find_spec("wgpu") is None:
11
+ return False, "wgpu not installed (pip install kernelport[webgpu])"
12
+ if importlib.util.find_spec("torch") is None:
13
+ return False, "torch not installed (needed to build inputs and run references)"
14
+ try:
15
+ from kernelport.backends.webgpu import runner
16
+
17
+ name = runner.adapter_name()
18
+ except Exception as exc:
19
+ return False, f"no usable WebGPU adapter: {type(exc).__name__}: {exc}"
20
+ return True, f"wgpu-native on {name}"
21
+
22
+
23
+ def supported_dtypes() -> tuple[str, ...]:
24
+ """float16 only when the adapter offers the standard shader-f16 feature."""
25
+ from kernelport.backends.webgpu import runner
26
+
27
+ return ("float32", "float16") if runner.has_f16() else ("float32",)
28
+
29
+
30
+ def hardware_name() -> str:
31
+ """Adapter name and native API, e.g. 'NVIDIA GeForce RTX 5060 Laptop GPU (WebGPU/Vulkan)'."""
32
+ from kernelport.backends.webgpu import runner
33
+
34
+ return runner.adapter_name()
@@ -0,0 +1,38 @@
1
+ """webgpu (WGSL) backend configuration."""
2
+
3
+ from kernelport.backends.base import BackendConfig
4
+
5
+ PORT_TEMPLATE = '''"""WebGPU (WGSL) port of <kernel>: <one line of what it computes>."""
6
+
7
+ # The runner appends: alias T, one storage buffer per tensor param (named like the
8
+ # launch.json param), and `params` (a uniform struct holding every scalar param).
9
+ WGSL = """
10
+ @compute @workgroup_size(256)
11
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
12
+ let i = gid.x; // CUDA: blockIdx.x * blockDim.x + threadIdx.x
13
+ if (i < u32(params.n)) { // CUDA: if (i < n)
14
+ y[i] = x[i];
15
+ }
16
+ }
17
+ """
18
+
19
+
20
+ def grid(n):
21
+ """Workgroups per dimension (CUDA: the grid). Each must be <= 65535."""
22
+ return ((n + 255) // 256,)
23
+ '''
24
+
25
+ # No tolerance overrides: WGSL f32 add/mul are IEEE single precision. exp/log/sqrt
26
+ # precision is implementation-defined in WGSL (exp: 3 + 2*|x| ulp), so ports that
27
+ # need tighter results compute those in a numerically safe form; nothing is loosened.
28
+ CONFIG = BackendConfig(
29
+ name="webgpu",
30
+ extension=".py",
31
+ description="WGSL compute shaders, run natively through wgpu-native (core WebGPU features only)",
32
+ dtypes=("float32", "float16"),
33
+ tolerances={},
34
+ timing_comparable=False,
35
+ timing_note="wall-clock time including WebGPU submission; not comparable to native kernel timings",
36
+ hardware_hint="any GPU with Vulkan, Metal or D3D12",
37
+ port_template=PORT_TEMPLATE,
38
+ )
@@ -0,0 +1,285 @@
1
+ """Run a webgpu (WGSL) port through wgpu-native.
2
+
3
+ A webgpu port is a Python file defining::
4
+
5
+ WGSL = '''
6
+ @compute @workgroup_size(256)
7
+ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
8
+ let i = gid.x;
9
+ if (i < u32(params.n)) { y[i] = max(x[i], T(0.0)); }
10
+ }
11
+ '''
12
+
13
+ def grid(n): # launch.json names (scalars, or arrays for .shape)
14
+ return (cdiv(n, 256),) # workgroups per dimension, each <= 65535
15
+
16
+ The runner appends the binding declarations, so the port never writes them:
17
+
18
+ - ``alias T = f32;`` (or ``f16`` for float16 cases; ``enable f16;`` is added)
19
+ - one storage buffer per tensor param, in launch.json order, named like the
20
+ param: ``var<storage, read>`` for inputs, ``read_write`` for outputs/inouts.
21
+ Element type: ``T`` for dtype "T", else f32 / f16 / i32 / u32; int64 arrives
22
+ as ``vec2<u32>`` (low word in ``.x``), since core WGSL has no 64-bit integers.
23
+ - ``var<uniform> params: Params`` holding every scalar param (i32 / u32 / f32).
24
+
25
+ A port may override a buffer's declared type, e.g. for atomics:
26
+ ``BINDINGS = {"out": "array<atomic<u32>>"}``.
27
+
28
+ Only core WebGPU features are enabled (plus shader-f16 when the adapter has it),
29
+ so a port that passes here uses WGSL a browser also accepts.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ from pathlib import Path
35
+
36
+ import numpy as np
37
+ import torch
38
+
39
+ from kernelport.backends.base import Prepared, bind_kwargs
40
+ from kernelport.harness.inputs import CaseData
41
+ from kernelport.harness.reference import file_hash, load_module
42
+ from kernelport.harness.results import CaseNotRunnable
43
+ from kernelport.harness.spec import LaunchSpec
44
+
45
+ MAX_WORKGROUPS = 65535
46
+ PARAVIRTUAL_MAX_BOUND = 500 * 2**20
47
+ _ELEM = {"float32": ("f32", np.float32), "float16": ("f16", np.float16), "int32": ("i32", np.int32),
48
+ "uint32": ("u32", np.uint32), "int64": ("vec2<u32>", np.int64)}
49
+ _SCALAR = {"int32": ("i32", np.int32), "int64": ("i32", np.int32), "uint32": ("u32", np.uint32),
50
+ "float32": ("f32", np.float32), "float64": ("f32", np.float32)}
51
+
52
+ _device = None
53
+ _pipelines: dict[str, tuple] = {}
54
+ _loaded: dict[tuple[str, str], object] = {}
55
+
56
+
57
+ def device():
58
+ """The shared wgpu device (high-performance adapter, core features + shader-f16)."""
59
+ global _device
60
+ if _device is None:
61
+ import wgpu
62
+
63
+ adapter = wgpu.gpu.request_adapter_sync(power_preference="high-performance")
64
+ if adapter is None:
65
+ raise RuntimeError("no WebGPU adapter found")
66
+ lim = adapter.limits
67
+ cap = 2**31 - 4 # what desktop browsers allow at most
68
+ limits = {"max-storage-buffer-binding-size": min(lim["max-storage-buffer-binding-size"], cap),
69
+ "max-buffer-size": min(lim["max-buffer-size"], cap)}
70
+ feats = ["shader-f16"] if "shader-f16" in adapter.features else []
71
+ _device = adapter.request_device_sync(required_features=feats, required_limits=limits)
72
+ _device._kp_adapter_info = adapter.info
73
+ return _device
74
+
75
+
76
+ _fence = None
77
+
78
+
79
+ def sync() -> None:
80
+ """Wait for all submitted work. Reading back a tiny buffer waits for the queue.
81
+
82
+ (queue.on_submitted_work_done_sync is broken in wgpu 0.32: wrong C callback signature.)
83
+ """
84
+ global _fence
85
+ import wgpu
86
+
87
+ dev = device()
88
+ if _fence is None:
89
+ _fence = dev.create_buffer(size=4, usage=wgpu.BufferUsage.COPY_SRC | wgpu.BufferUsage.COPY_DST)
90
+ dev.queue.read_buffer(_fence)
91
+
92
+
93
+ def has_f16() -> bool:
94
+ return "shader-f16" in device().features
95
+
96
+
97
+ def cdiv(a: int, b: int) -> int:
98
+ return -(-a // b)
99
+
100
+
101
+ def load_port(path: Path):
102
+ """Import the port once per content hash; returns the module."""
103
+ path = Path(path).resolve()
104
+ key = (str(path), file_hash(path))
105
+ if key not in _loaded:
106
+ mod = load_module(path, "kp_wgpu")
107
+ if not isinstance(getattr(mod, "WGSL", None), str):
108
+ raise AttributeError(f"{path.name} must define WGSL = '''...''' (the compute shader)")
109
+ if not callable(getattr(mod, "grid", None)):
110
+ raise AttributeError(f"{path.name} must define grid(...) returning workgroup counts")
111
+ _loaded[key] = mod
112
+ return _loaded[key]
113
+
114
+
115
+ def _elem_type(p, case_dtype: str) -> tuple[str, type]:
116
+ dt = p.resolved_dtype(case_dtype)
117
+ if dt not in _ELEM:
118
+ raise TypeError(f"{p.name}: dtype {dt} is not supported by the webgpu backend")
119
+ if dt == "float16" and not has_f16():
120
+ raise TypeError("this WebGPU adapter has no shader-f16 support")
121
+ wgsl, np_t = _ELEM[dt]
122
+ return ("T" if p.dtype == "T" else wgsl), np_t
123
+
124
+
125
+ def bindings_source(spec: LaunchSpec, case_dtype: str, overrides: dict[str, str]) -> str:
126
+ """WGSL declarations the runner appends to the port's shader."""
127
+ t = "f16" if case_dtype == "float16" else "f32"
128
+ lines = [f"alias T = {t};"]
129
+ b = 0
130
+ for p in spec.params:
131
+ if not p.is_tensor:
132
+ continue
133
+ elem, _ = _elem_type(p, case_dtype)
134
+ access = "read" if p.kind == "input" else "read_write"
135
+ ty = overrides.get(p.name, f"array<{elem}>")
136
+ lines.append(f"@group(0) @binding({b}) var<storage, {access}> {p.name}: {ty};")
137
+ b += 1
138
+ scalars = [p for p in spec.params if p.kind == "scalar"]
139
+ if scalars:
140
+ fields = ", ".join(f"{p.name}: {_SCALAR[p.dtype][0]}" for p in scalars)
141
+ lines.append(f"struct Params {{ {fields} }};")
142
+ lines.append(f"@group(0) @binding({b}) var<uniform> params: Params;")
143
+ return "\n".join(lines) + "\n"
144
+
145
+
146
+ def _pipeline(code: str, kinds: list[str]):
147
+ """Compile (cached) shader + explicit bind group layout."""
148
+ import wgpu
149
+
150
+ key = code + "|" + ",".join(kinds)
151
+ if key not in _pipelines:
152
+ dev = device()
153
+ types = {"input": wgpu.BufferBindingType.read_only_storage,
154
+ "rw": wgpu.BufferBindingType.storage,
155
+ "uniform": wgpu.BufferBindingType.uniform}
156
+ bgl = dev.create_bind_group_layout(entries=[
157
+ {"binding": i, "visibility": wgpu.ShaderStage.COMPUTE, "buffer": {"type": types[k]}}
158
+ for i, k in enumerate(kinds)])
159
+ layout = dev.create_pipeline_layout(bind_group_layouts=[bgl])
160
+ module = dev.create_shader_module(code=code)
161
+ pipe = dev.create_compute_pipeline(layout=layout, compute={"module": module, "entry_point": "main"})
162
+ _pipelines[key] = (pipe, bgl)
163
+ return _pipelines[key]
164
+
165
+
166
+ def _padded(raw: bytes, multiple: int = 4) -> bytes:
167
+ extra = (-len(raw)) % multiple
168
+ return raw + b"\0" * extra if extra or not raw else raw
169
+
170
+
171
+ def binding_limits() -> tuple[int, int | None]:
172
+ """(largest single storage binding, largest total bound per dispatch or None) in bytes."""
173
+ dev = device()
174
+ per = min(dev.limits["max-storage-buffer-binding-size"], dev.limits["max-buffer-size"])
175
+ total = None
176
+ if "Paravirtual" in str(dev._kp_adapter_info.get("device", "")):
177
+ # Apple's paravirtual GPU (macOS VMs such as GitHub's macos-14 runners) reports 3.5 GiB
178
+ # per binding, but a dispatch binding 628 MiB in total silently does not run (outputs
179
+ # untouched), while 500 MiB was measured to work. Stay within what was measured.
180
+ per = total = min(per, PARAVIRTUAL_MAX_BOUND)
181
+ return per, total
182
+
183
+
184
+ def _check_binding_sizes(spec: LaunchSpec, data: CaseData, per: int, total: int | None) -> None:
185
+ """Raise CaseNotRunnable if the case's tensors exceed what this adapter can bind."""
186
+ sizes = {}
187
+ for p in spec.params:
188
+ if p.is_tensor:
189
+ t = data.tensors[p.name]
190
+ itemsize = np.dtype(_elem_type(p, data.dtype)[1]).itemsize if t.dtype != torch.bfloat16 else 2
191
+ sizes[p.name] = t.numel() * itemsize
192
+ mib = 2**20
193
+ for name, n in sizes.items():
194
+ if n > per:
195
+ raise CaseNotRunnable(f"{name} needs a {n / mib:.0f} MiB storage binding; "
196
+ f"this adapter allows {per / mib:.0f} MiB")
197
+ if total is not None and sum(sizes.values()) > total:
198
+ raise CaseNotRunnable(f"the case binds {sum(sizes.values()) / mib:.0f} MiB in total; "
199
+ f"this adapter runs at most {total / mib:.0f} MiB per dispatch")
200
+
201
+
202
+ def prepare(path: Path, spec: LaunchSpec, data: CaseData) -> Prepared:
203
+ """Compile the port for this case's dtype and upload the case's buffers."""
204
+ import wgpu
205
+
206
+ mod = load_port(path)
207
+ dev = device()
208
+ enable = "enable f16; " if data.dtype == "float16" or any(
209
+ p.resolved_dtype(data.dtype) == "float16" for p in spec.params if p.is_tensor) else ""
210
+ overrides = dict(getattr(mod, "BINDINGS", {}) or {})
211
+ unknown = set(overrides) - {p.name for p in spec.params if p.is_tensor}
212
+ if unknown:
213
+ raise KeyError(f"BINDINGS names unknown tensor params {sorted(unknown)}")
214
+ code = enable + mod.WGSL + "\n" + bindings_source(spec, data.dtype, overrides)
215
+
216
+ _check_binding_sizes(spec, data, *binding_limits())
217
+
218
+ kinds, buffers, outputs = [], [], {}
219
+ arrays = {}
220
+ for p in spec.params:
221
+ if not p.is_tensor:
222
+ continue
223
+ t = data.tensors[p.name]
224
+ if t.dtype == torch.bfloat16:
225
+ raise TypeError(f"{p.name}: bfloat16 is not a WGSL type; the webgpu backend skips bfloat16 cases")
226
+ _, np_t = _elem_type(p, data.dtype)
227
+ arr = t.contiguous().numpy().astype(np_t, copy=False)
228
+ arrays[p.name] = arr
229
+ usage = wgpu.BufferUsage.STORAGE | wgpu.BufferUsage.COPY_SRC | wgpu.BufferUsage.COPY_DST
230
+ buf = dev.create_buffer_with_data(data=_padded(arr.tobytes()), usage=usage)
231
+ buffers.append(buf)
232
+ kinds.append("input" if p.kind == "input" else "rw")
233
+ if p.is_output:
234
+ outputs[p.name] = (buf, arr)
235
+ scalars = [p for p in spec.params if p.kind == "scalar"]
236
+ if scalars:
237
+ vals = []
238
+ for p in scalars:
239
+ v = data.scalars[p.name]
240
+ if p.dtype in ("int64", "int32") and not (-2**31 <= int(v) < 2**31):
241
+ raise OverflowError(f"scalar {p.name}={v} does not fit in i32")
242
+ vals.append(np.array(v, dtype=_SCALAR[p.dtype][1]).tobytes())
243
+ ubuf = dev.create_buffer_with_data(data=_padded(b"".join(vals), 16), usage=wgpu.BufferUsage.UNIFORM)
244
+ buffers.append(ubuf)
245
+ kinds.append("uniform")
246
+
247
+ pipe, bgl = _pipeline(code, kinds)
248
+ group = dev.create_bind_group(layout=bgl, entries=[
249
+ {"binding": i, "resource": {"buffer": b, "offset": 0, "size": b.size}} for i, b in enumerate(buffers)])
250
+
251
+ kwargs = bind_kwargs(mod.grid, spec, arrays, data.scalars)
252
+ g = mod.grid(**kwargs)
253
+ g = (g,) if isinstance(g, int) else tuple(int(x) for x in g)
254
+ if not 1 <= len(g) <= 3 or any(x < 0 for x in g):
255
+ raise ValueError(f"grid() must return 1 to 3 non-negative ints, got {g}")
256
+ if any(x > MAX_WORKGROUPS for x in g):
257
+ raise ValueError(f"grid() returned {g}: WebGPU allows at most {MAX_WORKGROUPS} workgroups per "
258
+ "dimension; use a 2-D grid or loop inside the shader")
259
+ g = g + (1,) * (3 - len(g))
260
+
261
+ def launch() -> None:
262
+ enc = dev.create_command_encoder()
263
+ cp = enc.begin_compute_pass()
264
+ cp.set_pipeline(pipe)
265
+ cp.set_bind_group(0, group)
266
+ cp.dispatch_workgroups(*g)
267
+ cp.end()
268
+ dev.queue.submit([enc.finish()])
269
+
270
+ def fetch() -> dict:
271
+ out = {}
272
+ for name, (buf, arr) in outputs.items():
273
+ raw = dev.queue.read_buffer(buf)
274
+ got = np.frombuffer(raw, dtype=arr.dtype, count=arr.size).reshape(arr.shape).copy()
275
+ out[name] = torch.from_numpy(got)
276
+ return out
277
+
278
+ return Prepared(launch=launch, sync=sync, fetch=fetch, timer="wall_clock")
279
+
280
+
281
+ def adapter_name() -> str:
282
+ info = device()._kp_adapter_info
283
+ name = info.get("device") or "WebGPU adapter"
284
+ return f"{name} (WebGPU/{info.get('backend_type', '?')})"
285
+
File without changes
@@ -0,0 +1,76 @@
1
+ """Device-synchronised kernel timing.
2
+
3
+ Warm up, then time each launch individually with device events and report
4
+ the median. On NVIDIA and ROCm we use torch.cuda.Event (ROCm maps it to HIP
5
+ events). CPU-side timers are only used for backends with no device events,
6
+ and those numbers are labelled as not comparable.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import statistics
12
+ import time
13
+ from dataclasses import asdict, dataclass
14
+ from typing import Callable
15
+
16
+ DEFAULT_WARMUP = 10
17
+ DEFAULT_ITERS = 50
18
+ GPU_HOLD_CYCLES = 50_000_000 # ~20-40 ms of GPU time: covers queueing 50 launches from Python
19
+
20
+
21
+ @dataclass
22
+ class Timing:
23
+ """Timing summary in milliseconds."""
24
+
25
+ median_ms: float
26
+ min_ms: float
27
+ max_ms: float
28
+ iters: int
29
+ method: str # "cuda_event" or "wall_clock"
30
+
31
+ def to_dict(self) -> dict:
32
+ """JSON-serialisable dict."""
33
+ return asdict(self)
34
+
35
+
36
+ def time_cuda(launch: Callable[[], None], warmup: int = DEFAULT_WARMUP,
37
+ iters: int = DEFAULT_ITERS) -> Timing:
38
+ """Median time of ``launch()`` measured with CUDA/HIP events."""
39
+ import torch
40
+
41
+ for _ in range(warmup):
42
+ launch()
43
+ torch.cuda.synchronize()
44
+ starts = [torch.cuda.Event(enable_timing=True) for _ in range(iters)]
45
+ ends = [torch.cuda.Event(enable_timing=True) for _ in range(iters)]
46
+ # Keep the GPU busy while the CPU queues every timed launch. Otherwise a short kernel
47
+ # waits for the next Python launch (tens of microseconds for Triton) and the events
48
+ # measure CPU launch overhead instead of the kernel.
49
+ sleep = getattr(torch.cuda, "_sleep", None)
50
+ if sleep is not None:
51
+ try:
52
+ sleep(GPU_HOLD_CYCLES)
53
+ except Exception: # private API; ROCm or future builds may lack it. Timing still works.
54
+ pass
55
+ for s, e in zip(starts, ends):
56
+ s.record()
57
+ launch()
58
+ e.record()
59
+ torch.cuda.synchronize()
60
+ times = [s.elapsed_time(e) for s, e in zip(starts, ends)]
61
+ return Timing(statistics.median(times), min(times), max(times), iters, "cuda_event")
62
+
63
+
64
+ def time_wall(launch: Callable[[], None], sync: Callable[[], None] = lambda: None,
65
+ warmup: int = DEFAULT_WARMUP, iters: int = DEFAULT_ITERS) -> Timing:
66
+ """Median wall-clock time of ``launch(); sync()``."""
67
+ for _ in range(warmup):
68
+ launch()
69
+ sync()
70
+ times = []
71
+ for _ in range(iters):
72
+ t0 = time.perf_counter()
73
+ launch()
74
+ sync()
75
+ times.append((time.perf_counter() - t0) * 1e3)
76
+ return Timing(statistics.median(times), min(times), max(times), iters, "wall_clock")