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
|
@@ -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")
|