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,138 @@
|
|
|
1
|
+
"""Backend interface and registry.
|
|
2
|
+
|
|
3
|
+
A backend lives in ``kernelport/backends/<name>/`` with three modules:
|
|
4
|
+
|
|
5
|
+
- ``config.py`` CONFIG: BackendConfig (no heavy imports)
|
|
6
|
+
- ``capability.py`` is_available() -> (bool, reason) and hardware_name() -> str
|
|
7
|
+
(must import cleanly even when the backend's packages are missing)
|
|
8
|
+
- ``runner.py`` prepare(port_path, spec, data) -> Prepared (imports heavy packages lazily)
|
|
9
|
+
|
|
10
|
+
The registry imports config and capability eagerly-on-demand and the runner
|
|
11
|
+
only when something is actually run, so ``kernelport`` works on machines that
|
|
12
|
+
have none of the GPU packages.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import importlib
|
|
18
|
+
import inspect
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Callable
|
|
22
|
+
|
|
23
|
+
from kernelport.harness.tolerances import Tolerance
|
|
24
|
+
|
|
25
|
+
BACKEND_NAMES = ("triton", "cpu", "metal", "webgpu", "hip")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class BackendConfig:
|
|
30
|
+
"""Static description of a backend."""
|
|
31
|
+
|
|
32
|
+
name: str
|
|
33
|
+
extension: str # port file extension, e.g. ".py"
|
|
34
|
+
description: str
|
|
35
|
+
dtypes: tuple[str, ...] # output/compute dtypes this backend can run
|
|
36
|
+
tolerances: dict[str, Tolerance] = field(default_factory=dict) # overrides, each with a reason
|
|
37
|
+
timing_comparable: bool = True # False for CPU / browser numbers
|
|
38
|
+
timing_note: str = ""
|
|
39
|
+
hardware_hint: str = "" # where verification can happen, e.g. "Apple Silicon Mac"
|
|
40
|
+
port_template: str = "" # skeleton shown to agents
|
|
41
|
+
tune_space: dict[str, list[Any]] = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class Prepared:
|
|
46
|
+
"""A port loaded and bound to one case's inputs, ready to launch repeatedly."""
|
|
47
|
+
|
|
48
|
+
launch: Callable[[], None] # run once (may be async on GPUs)
|
|
49
|
+
sync: Callable[[], None] # wait for completion
|
|
50
|
+
fetch: Callable[[], dict[str, Any]] # output tensors as CPU torch tensors
|
|
51
|
+
timer: str = "wall_clock" # "cuda_event" when launch runs on the current torch CUDA stream
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Backend:
|
|
55
|
+
"""Facade over a backend's config, capability and runner modules."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, name: str) -> None:
|
|
58
|
+
if name not in BACKEND_NAMES:
|
|
59
|
+
raise KeyError(f"unknown backend {name!r}; known: {', '.join(BACKEND_NAMES)}")
|
|
60
|
+
self.name = name
|
|
61
|
+
self._pkg = f"kernelport.backends.{name}"
|
|
62
|
+
self.config: BackendConfig = importlib.import_module(f"{self._pkg}.config").CONFIG
|
|
63
|
+
self._cap = importlib.import_module(f"{self._pkg}.capability")
|
|
64
|
+
|
|
65
|
+
def is_available(self) -> tuple[bool, str]:
|
|
66
|
+
"""(available, reason). Never raises."""
|
|
67
|
+
try:
|
|
68
|
+
return self._cap.is_available()
|
|
69
|
+
except Exception as exc: # a broken install must not crash callers
|
|
70
|
+
return False, f"capability check failed: {type(exc).__name__}: {exc}"
|
|
71
|
+
|
|
72
|
+
def hardware_name(self) -> str:
|
|
73
|
+
"""Name of the device ports run on here."""
|
|
74
|
+
try:
|
|
75
|
+
return self._cap.hardware_name()
|
|
76
|
+
except Exception:
|
|
77
|
+
return "unknown"
|
|
78
|
+
|
|
79
|
+
def prepare(self, port: Path, spec, data) -> Prepared:
|
|
80
|
+
"""Load ``port`` and bind it to one case."""
|
|
81
|
+
runner = importlib.import_module(f"{self._pkg}.runner")
|
|
82
|
+
return runner.prepare(Path(port), spec, data)
|
|
83
|
+
|
|
84
|
+
def run(self, port: Path, spec, data) -> dict[str, Any]:
|
|
85
|
+
"""Run once and return outputs as CPU tensors."""
|
|
86
|
+
p = self.prepare(port, spec, data)
|
|
87
|
+
p.launch()
|
|
88
|
+
p.sync()
|
|
89
|
+
return p.fetch()
|
|
90
|
+
|
|
91
|
+
def dtypes(self) -> tuple[str, ...]:
|
|
92
|
+
"""dtypes runnable here: the config's, narrowed by the capability module if it knows better
|
|
93
|
+
(e.g. float16 only on WebGPU adapters with shader-f16)."""
|
|
94
|
+
fn = getattr(self._cap, "supported_dtypes", None)
|
|
95
|
+
if fn is None:
|
|
96
|
+
return self.config.dtypes
|
|
97
|
+
try:
|
|
98
|
+
return tuple(d for d in self.config.dtypes if d in fn())
|
|
99
|
+
except Exception:
|
|
100
|
+
return self.config.dtypes
|
|
101
|
+
|
|
102
|
+
def supports(self, dtype: str) -> bool:
|
|
103
|
+
"""Whether this backend runs cases of ``dtype`` here."""
|
|
104
|
+
return dtype in self.dtypes()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def bind_kwargs(fn, spec, tensors: dict, scalars: dict) -> dict:
|
|
108
|
+
"""Match launch.json params to run()'s signature, by name."""
|
|
109
|
+
available = {p.name: (tensors[p.name] if p.is_tensor else scalars[p.name]) for p in spec.params}
|
|
110
|
+
accepted = inspect.signature(fn).parameters
|
|
111
|
+
if any(v.kind == v.VAR_KEYWORD for v in accepted.values()):
|
|
112
|
+
return available
|
|
113
|
+
missing = [k for k, v in accepted.items() if k not in available and v.default is inspect.Parameter.empty]
|
|
114
|
+
if missing:
|
|
115
|
+
raise TypeError(f"run() asks for unknown params {missing}; launch.json params are {list(available)}")
|
|
116
|
+
return {k: v for k, v in available.items() if k in accepted}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
_cache: dict[str, Backend] = {}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_backend(name: str) -> Backend:
|
|
123
|
+
"""Backend by name (cached)."""
|
|
124
|
+
if name not in _cache:
|
|
125
|
+
_cache[name] = Backend(name)
|
|
126
|
+
return _cache[name]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def registered_backends() -> list[str]:
|
|
130
|
+
"""Names of backends whose modules exist in this install (implemented or not)."""
|
|
131
|
+
names = []
|
|
132
|
+
for n in BACKEND_NAMES:
|
|
133
|
+
try:
|
|
134
|
+
importlib.import_module(f"kernelport.backends.{n}.config")
|
|
135
|
+
names.append(n)
|
|
136
|
+
except ModuleNotFoundError:
|
|
137
|
+
continue
|
|
138
|
+
return names
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Numba-compiled CPU kernels backend."""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Capability check for the cpu (Numba) backend."""
|
|
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("numba") is None:
|
|
11
|
+
return False, "numba not installed (pip install kernelport[cpu])"
|
|
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
|
+
import numba
|
|
16
|
+
except Exception as exc: # e.g. numba built against a different NumPy
|
|
17
|
+
return False, f"numba failed to import: {exc}"
|
|
18
|
+
return True, f"Numba {numba.__version__} on CPU, {numba.config.NUMBA_NUM_THREADS} threads"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def hardware_name() -> str:
|
|
22
|
+
"""CPU model name, e.g. 'Intel(R) Core(TM) i7-13620H (16 threads)'."""
|
|
23
|
+
from kernelport.hardware import cpu_name
|
|
24
|
+
|
|
25
|
+
return cpu_name()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""cpu (Numba) backend configuration."""
|
|
2
|
+
|
|
3
|
+
from kernelport.backends.base import BackendConfig
|
|
4
|
+
|
|
5
|
+
PORT_TEMPLATE = '''"""CPU (Numba) port of <kernel>: <one line of what it computes>."""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
from numba import njit, prange
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@njit(parallel=True)
|
|
12
|
+
def _kernel(x, y, n):
|
|
13
|
+
# CUDA: one thread per i. prange splits the loop across CPU threads.
|
|
14
|
+
for i in prange(n):
|
|
15
|
+
y[i] = x[i]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run(x, y, n):
|
|
19
|
+
"""Same parameter names as launch.json. Arrays are NumPy with launch.json shapes; write outputs in place."""
|
|
20
|
+
_kernel(x, y, n)
|
|
21
|
+
'''
|
|
22
|
+
|
|
23
|
+
# No tolerance overrides: Numba compiles float32 arithmetic to IEEE float32 ops
|
|
24
|
+
# (fastmath is off unless a port turns it on), and math.exp/log/sqrt on float32
|
|
25
|
+
# call the C library's correctly-rounded-or-near versions.
|
|
26
|
+
CONFIG = BackendConfig(
|
|
27
|
+
name="cpu",
|
|
28
|
+
extension=".py",
|
|
29
|
+
description="Numba-compiled CPU kernels (any machine)",
|
|
30
|
+
dtypes=("float32", "float64"),
|
|
31
|
+
tolerances={},
|
|
32
|
+
timing_comparable=False,
|
|
33
|
+
timing_note="CPU wall-clock time; not comparable to GPU kernel timings",
|
|
34
|
+
hardware_hint="any machine with Numba",
|
|
35
|
+
port_template=PORT_TEMPLATE,
|
|
36
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Run a cpu (Numba) port.
|
|
2
|
+
|
|
3
|
+
A cpu port is a Python file defining::
|
|
4
|
+
|
|
5
|
+
def run(<same parameter names as launch.json>):
|
|
6
|
+
...call one or more @numba.njit functions...
|
|
7
|
+
|
|
8
|
+
Tensor parameters arrive as C-contiguous NumPy arrays with the launch.json
|
|
9
|
+
shapes (outputs pre-filled with NaN, or a sentinel for ints) and must be
|
|
10
|
+
written in place. Scalars arrive as Python numbers. run() must return None.
|
|
11
|
+
|
|
12
|
+
Numba compiles on the first call for each argument type, so the loaded run()
|
|
13
|
+
is cached per file content: verification compiles once per dtype, not once
|
|
14
|
+
per case, and benchmark warm-up excludes compilation.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
import torch
|
|
22
|
+
|
|
23
|
+
from kernelport.backends.base import Prepared, bind_kwargs
|
|
24
|
+
from kernelport.harness.inputs import CaseData
|
|
25
|
+
from kernelport.harness.reference import file_hash, load_module
|
|
26
|
+
from kernelport.harness.spec import LaunchSpec
|
|
27
|
+
|
|
28
|
+
_loaded: dict[tuple[str, str], object] = {}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_port(path: Path):
|
|
32
|
+
"""Import the port (once per content hash) and return its run() function."""
|
|
33
|
+
path = Path(path).resolve()
|
|
34
|
+
key = (str(path), file_hash(path))
|
|
35
|
+
if key not in _loaded:
|
|
36
|
+
fn = getattr(load_module(path, "kp_cpu"), "run", None)
|
|
37
|
+
if fn is None:
|
|
38
|
+
raise AttributeError(f"{path.name} does not define run(...)")
|
|
39
|
+
_loaded[key] = fn
|
|
40
|
+
return _loaded[key]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _to_numpy(name: str, t: torch.Tensor):
|
|
44
|
+
if t.dtype == torch.bfloat16:
|
|
45
|
+
raise TypeError(f"{name}: bfloat16 has no NumPy dtype; the cpu backend does not run bfloat16 cases")
|
|
46
|
+
return t.clone().contiguous().numpy()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def prepare(path: Path, spec: LaunchSpec, data: CaseData) -> Prepared:
|
|
50
|
+
"""Load the port and bind it to one case's arrays."""
|
|
51
|
+
fn = load_port(path)
|
|
52
|
+
arrays = {k: _to_numpy(k, v) for k, v in data.tensors.items()}
|
|
53
|
+
kwargs = bind_kwargs(fn, spec, arrays, data.scalars)
|
|
54
|
+
outputs = [p.name for p in spec.params if p.is_output]
|
|
55
|
+
|
|
56
|
+
def launch() -> None:
|
|
57
|
+
ret = fn(**kwargs)
|
|
58
|
+
if ret is not None:
|
|
59
|
+
raise TypeError("run() returned a value; outputs must be written in place "
|
|
60
|
+
"(e.g. y[:] = ... or y[i] = ...), and run() must return None")
|
|
61
|
+
|
|
62
|
+
return Prepared(
|
|
63
|
+
launch=launch,
|
|
64
|
+
sync=lambda: None,
|
|
65
|
+
fetch=lambda: {n: torch.from_numpy(arrays[n].copy()) for n in outputs},
|
|
66
|
+
timer="wall_clock",
|
|
67
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""HIP C++ kernels on AMD GPUs backend."""
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Capability check for the hip backend (ROCm torch + ROCm CuPy with hipRTC)."""
|
|
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("torch") is None:
|
|
11
|
+
return False, "torch not installed"
|
|
12
|
+
import torch
|
|
13
|
+
|
|
14
|
+
if not getattr(torch.version, "hip", None):
|
|
15
|
+
return False, "needs a ROCm build of torch on an AMD GPU"
|
|
16
|
+
if not torch.cuda.is_available():
|
|
17
|
+
return False, "ROCm torch sees no AMD GPU"
|
|
18
|
+
if importlib.util.find_spec("cupy") is None:
|
|
19
|
+
return False, "cupy (ROCm build, e.g. cupy-rocm-6-0) not installed; it provides hipRTC compilation"
|
|
20
|
+
try:
|
|
21
|
+
import cupy
|
|
22
|
+
|
|
23
|
+
if not getattr(cupy.cuda.runtime, "is_hip", False):
|
|
24
|
+
return False, "cupy is the CUDA build; install the ROCm build (e.g. cupy-rocm-6-0)"
|
|
25
|
+
except Exception as exc:
|
|
26
|
+
return False, f"cupy failed to import: {exc}"
|
|
27
|
+
return True, f"hipRTC via CuPy on ROCm {torch.version.hip}"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def hardware_name() -> str:
|
|
31
|
+
"""AMD GPU name with gfx arch."""
|
|
32
|
+
from kernelport.hardware import gpu_name
|
|
33
|
+
|
|
34
|
+
return gpu_name() or "no GPU"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""hip backend configuration."""
|
|
2
|
+
|
|
3
|
+
from kernelport.backends.base import BackendConfig
|
|
4
|
+
|
|
5
|
+
PORT_TEMPLATE = '''// HIP port of <kernel>: <one line of what it computes>.
|
|
6
|
+
// Same entry name, parameter order and launch (grid/block/shared mem) as launch.json.
|
|
7
|
+
#include <hip/hip_fp16.h>
|
|
8
|
+
#include <hip/hip_bf16.h>
|
|
9
|
+
|
|
10
|
+
template <typename T>
|
|
11
|
+
__global__ void vector_add(const T* __restrict__ a, const T* __restrict__ b, T* __restrict__ out, int n) {
|
|
12
|
+
int i = blockIdx.x * blockDim.x + threadIdx.x;
|
|
13
|
+
if (i < n) out[i] = a[i] + b[i];
|
|
14
|
+
}
|
|
15
|
+
'''
|
|
16
|
+
|
|
17
|
+
# No tolerance overrides: HIP float arithmetic is IEEE single precision like CUDA.
|
|
18
|
+
CONFIG = BackendConfig(
|
|
19
|
+
name="hip",
|
|
20
|
+
extension=".hip",
|
|
21
|
+
description="HIP C++ kernels on AMD GPUs, compiled at runtime with hipRTC",
|
|
22
|
+
dtypes=("float32", "float16", "bfloat16"),
|
|
23
|
+
tolerances={},
|
|
24
|
+
timing_comparable=True,
|
|
25
|
+
timing_note="",
|
|
26
|
+
hardware_hint="AMD GPU with ROCm (ROCm torch + cupy-rocm)",
|
|
27
|
+
port_template=PORT_TEMPLATE,
|
|
28
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Run a hip port: HIP C++ compiled at runtime with hipRTC (through CuPy's ROCm build).
|
|
2
|
+
|
|
3
|
+
A hip port is a ``.hip`` source file containing the kernel(s). It is launched
|
|
4
|
+
exactly like the original CUDA kernel: same entry (``launch.json`` "entry",
|
|
5
|
+
with ``{T}`` filled from HIP type names), same parameter order, same grid,
|
|
6
|
+
block and shared memory from ``launch.json``. Tensors are device buffers on the
|
|
7
|
+
AMD GPU (outputs pre-filled with NaN); scalars are passed by value.
|
|
8
|
+
|
|
9
|
+
Requirements: a ROCm build of torch and a ROCm build of CuPy (e.g. cupy-rocm-6-0),
|
|
10
|
+
which compiles RawModule sources with hipRTC.
|
|
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
|
|
20
|
+
from kernelport.harness import run_cuda
|
|
21
|
+
from kernelport.harness.inputs import CaseData
|
|
22
|
+
from kernelport.harness.spec import LaunchSpec
|
|
23
|
+
|
|
24
|
+
# C++ names substituted for {T} in the entry. HIP spells bf16 differently from CUDA.
|
|
25
|
+
HIP_TYPE_NAMES = {
|
|
26
|
+
"float32": "float", "float64": "double", "float16": "__half", "bfloat16": "__hip_bfloat16",
|
|
27
|
+
"int8": "int8_t", "uint8": "uint8_t", "int16": "int16_t", "int32": "int", "int64": "long long",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# hipRTC, like NVRTC, has no host math headers. hip_runtime is implicit in hipRTC.
|
|
31
|
+
HIPRTC_PRELUDE = run_cuda.NVRTC_PRELUDE
|
|
32
|
+
HIPRTC_OPTIONS = ("-std=c++17",)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def prepare(path: Path, spec: LaunchSpec, data: CaseData) -> Prepared:
|
|
36
|
+
"""Compile (cached) and bind the port to one case on the GPU."""
|
|
37
|
+
kern = run_cuda.get_kernel(path, spec, data.dtype, type_names=HIP_TYPE_NAMES,
|
|
38
|
+
prelude=HIPRTC_PRELUDE, options=HIPRTC_OPTIONS, device_only=False)
|
|
39
|
+
dev = {k: v.to("cuda") for k, v in data.tensors.items()} # torch's ROCm build keeps the "cuda" name
|
|
40
|
+
grid, block, shmem = spec.grid_block(data.dims)
|
|
41
|
+
args = tuple(run_cuda.kernel_args(spec, data, dev))
|
|
42
|
+
outputs = [p.name for p in spec.params if p.is_output]
|
|
43
|
+
return Prepared(
|
|
44
|
+
launch=lambda: kern(grid, block, args, shared_mem=shmem),
|
|
45
|
+
sync=torch.cuda.synchronize,
|
|
46
|
+
fetch=lambda: {n: dev[n].cpu() for n in outputs},
|
|
47
|
+
timer="cuda_event", # torch.cuda.Event maps to hipEvent on ROCm
|
|
48
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MLX custom Metal kernels (mx.fast.metal_kernel) backend."""
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Capability check for the metal (MLX) backend."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import platform
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def is_available() -> tuple[bool, str]:
|
|
11
|
+
"""(available, reason)."""
|
|
12
|
+
if sys.platform != "darwin" or platform.machine() != "arm64":
|
|
13
|
+
return False, "Metal needs macOS on Apple Silicon"
|
|
14
|
+
if importlib.util.find_spec("mlx") is None:
|
|
15
|
+
return False, "mlx not installed (pip install kernelport[metal])"
|
|
16
|
+
if importlib.util.find_spec("torch") is None:
|
|
17
|
+
return False, "torch not installed (needed to build inputs and run references)"
|
|
18
|
+
try:
|
|
19
|
+
import mlx.core as mx
|
|
20
|
+
|
|
21
|
+
if not mx.metal.is_available():
|
|
22
|
+
return False, "MLX reports no Metal device"
|
|
23
|
+
except Exception as exc:
|
|
24
|
+
return False, f"mlx failed to import: {exc}"
|
|
25
|
+
return True, "MLX custom Metal kernels"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def hardware_name() -> str:
|
|
29
|
+
"""Apple chip name, e.g. 'Apple M2 Pro (Metal)'."""
|
|
30
|
+
from kernelport.hardware import _cpu_model
|
|
31
|
+
|
|
32
|
+
return f"{_cpu_model() or 'Apple Silicon'} (Metal)"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""metal (MLX custom Metal kernels) backend configuration."""
|
|
2
|
+
|
|
3
|
+
from kernelport.backends.base import BackendConfig
|
|
4
|
+
|
|
5
|
+
PORT_TEMPLATE = '''"""Metal (MLX) port of <kernel>: <one line of what it computes>."""
|
|
6
|
+
|
|
7
|
+
# Kernel BODY only: MLX writes the signature. Inputs = input/inout tensors then scalars
|
|
8
|
+
# (scalars are 1-element arrays: n[0]); outputs = output params, inouts as <name>_out.
|
|
9
|
+
SOURCE = """
|
|
10
|
+
uint i = thread_position_in_grid.x; // CUDA: blockIdx.x * blockDim.x + threadIdx.x
|
|
11
|
+
if (i < uint(n[0])) {
|
|
12
|
+
y[i] = x[i];
|
|
13
|
+
}
|
|
14
|
+
"""
|
|
15
|
+
THREADGROUP = (256, 1, 1) # CUDA: block
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def grid(n):
|
|
19
|
+
"""TOTAL threads per dimension (Metal dispatchThreads), not threadgroup counts."""
|
|
20
|
+
return (n, 1, 1)
|
|
21
|
+
'''
|
|
22
|
+
|
|
23
|
+
# No tolerance overrides. Metal's compiler may use fast-math, so ports call
|
|
24
|
+
# metal::precise:: versions of exp/log/sqrt/rsqrt (see skills/kernelport/backends/metal.md).
|
|
25
|
+
CONFIG = BackendConfig(
|
|
26
|
+
name="metal",
|
|
27
|
+
extension=".py",
|
|
28
|
+
description="Metal kernels through MLX custom kernels (mx.fast.metal_kernel)",
|
|
29
|
+
dtypes=("float32", "float16", "bfloat16"),
|
|
30
|
+
tolerances={},
|
|
31
|
+
timing_comparable=False,
|
|
32
|
+
timing_note="wall-clock time including MLX graph evaluation; not comparable to device-event timings",
|
|
33
|
+
hardware_hint="Apple Silicon Mac",
|
|
34
|
+
port_template=PORT_TEMPLATE,
|
|
35
|
+
)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Run a metal port: a Metal kernel body compiled by MLX (``mx.fast.metal_kernel``).
|
|
2
|
+
|
|
3
|
+
A metal port is a Python file defining::
|
|
4
|
+
|
|
5
|
+
SOURCE = '''
|
|
6
|
+
uint i = thread_position_in_grid.x;
|
|
7
|
+
if (i < n[0]) { out[i] = a[i] + b[i]; }
|
|
8
|
+
'''
|
|
9
|
+
THREADGROUP = (256, 1, 1)
|
|
10
|
+
def grid(n): # TOTAL threads per dimension (MLX/Metal dispatchThreads),
|
|
11
|
+
return (n, 1, 1) # not threadgroup counts as in CUDA
|
|
12
|
+
|
|
13
|
+
MLX writes the kernel signature from these names (launch.json order):
|
|
14
|
+
|
|
15
|
+
- inputs: every ``input`` and ``inout`` tensor param, then every scalar param as a
|
|
16
|
+
one-element array (read it as ``n[0]``);
|
|
17
|
+
- outputs: every ``output`` param by name, and every ``inout`` param as ``<name>_out``
|
|
18
|
+
(MLX outputs are fresh buffers: copy or update from the input ``<name>``).
|
|
19
|
+
|
|
20
|
+
``T`` is a template parameter when launch.json uses dtype "T". Outputs are
|
|
21
|
+
pre-filled with ``INIT_VALUE`` (default NaN, so unwritten elements are caught).
|
|
22
|
+
``ATOMIC_OUTPUTS = True`` declares every output as ``device atomic<...>*``.
|
|
23
|
+
``HEADER`` may hold helper functions placed before the kernel.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import math
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
import numpy as np
|
|
32
|
+
import torch
|
|
33
|
+
|
|
34
|
+
from kernelport.backends.base import Prepared, bind_kwargs
|
|
35
|
+
from kernelport.harness.inputs import CaseData
|
|
36
|
+
from kernelport.harness.reference import file_hash, load_module
|
|
37
|
+
from kernelport.harness.spec import LaunchSpec
|
|
38
|
+
|
|
39
|
+
_loaded: dict[tuple[str, str], object] = {}
|
|
40
|
+
_kernels: dict[tuple, object] = {}
|
|
41
|
+
SCALAR_NP = {"int32": np.int32, "int64": np.int64, "uint32": np.uint32, "float32": np.float32,
|
|
42
|
+
"float64": np.float32, "float16": np.float16}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _mx():
|
|
46
|
+
import mlx.core as mx
|
|
47
|
+
|
|
48
|
+
return mx
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def mx_dtype(dtype: str):
|
|
52
|
+
mx = _mx()
|
|
53
|
+
return {"float32": mx.float32, "float16": mx.float16, "bfloat16": mx.bfloat16, "int32": mx.int32,
|
|
54
|
+
"int64": mx.int64, "uint32": mx.uint32, "int8": mx.int8, "uint8": mx.uint8,
|
|
55
|
+
"int16": mx.int16}[dtype]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_port(path: Path):
|
|
59
|
+
path = Path(path).resolve()
|
|
60
|
+
key = (str(path), file_hash(path))
|
|
61
|
+
if key not in _loaded:
|
|
62
|
+
mod = load_module(path, "kp_metal")
|
|
63
|
+
if not isinstance(getattr(mod, "SOURCE", None), str):
|
|
64
|
+
raise AttributeError(f"{path.name} must define SOURCE = '''...''' (the Metal kernel body)")
|
|
65
|
+
if not callable(getattr(mod, "grid", None)):
|
|
66
|
+
raise AttributeError(f"{path.name} must define grid(...) returning total threads per dimension")
|
|
67
|
+
_loaded[key] = mod
|
|
68
|
+
return _loaded[key]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def names(spec: LaunchSpec) -> tuple[list[str], list[str]]:
|
|
72
|
+
"""(input_names, output_names) as the kernel sees them."""
|
|
73
|
+
ins = [p.name for p in spec.params if p.kind in ("input", "inout")]
|
|
74
|
+
ins += [p.name for p in spec.params if p.kind == "scalar"]
|
|
75
|
+
outs = [p.name if p.kind == "output" else f"{p.name}_out" for p in spec.params if p.is_output]
|
|
76
|
+
return ins, outs
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _to_mx(t: torch.Tensor):
|
|
80
|
+
mx = _mx()
|
|
81
|
+
if t.dtype == torch.bfloat16: # NumPy has no bf16: go through float32 (exact both ways)
|
|
82
|
+
return mx.array(t.float().numpy()).astype(mx.bfloat16)
|
|
83
|
+
return mx.array(t.contiguous().numpy())
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _to_torch(a, dtype: str) -> torch.Tensor:
|
|
87
|
+
mx = _mx()
|
|
88
|
+
if dtype == "bfloat16":
|
|
89
|
+
return torch.from_numpy(np.array(a.astype(mx.float32))).to(torch.bfloat16)
|
|
90
|
+
return torch.from_numpy(np.array(a).copy())
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _triple(v, what: str) -> tuple[int, int, int]:
|
|
94
|
+
v = (v,) if isinstance(v, int) else tuple(int(x) for x in v)
|
|
95
|
+
if not 1 <= len(v) <= 3 or any(x < 0 for x in v):
|
|
96
|
+
raise ValueError(f"{what} must be 1 to 3 non-negative ints, got {v}")
|
|
97
|
+
return v + (1,) * (3 - len(v))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def prepare(path: Path, spec: LaunchSpec, data: CaseData) -> Prepared:
|
|
101
|
+
"""Build the MLX kernel (cached) and bind it to one case."""
|
|
102
|
+
mx = _mx()
|
|
103
|
+
mod = load_port(path)
|
|
104
|
+
in_names, out_names = names(spec)
|
|
105
|
+
atomic = bool(getattr(mod, "ATOMIC_OUTPUTS", False))
|
|
106
|
+
header = getattr(mod, "HEADER", "") or ""
|
|
107
|
+
key = (id(mx), mod.SOURCE, header, tuple(in_names), tuple(out_names), atomic)
|
|
108
|
+
if key not in _kernels:
|
|
109
|
+
_kernels[key] = mx.fast.metal_kernel(
|
|
110
|
+
name=f"kp_{spec.name}", input_names=in_names, output_names=out_names,
|
|
111
|
+
source=mod.SOURCE, header=header, ensure_row_contiguous=True, atomic_outputs=atomic)
|
|
112
|
+
kernel = _kernels[key]
|
|
113
|
+
|
|
114
|
+
inputs = [_to_mx(data.tensors[p.name]) for p in spec.params if p.kind in ("input", "inout")]
|
|
115
|
+
for p in spec.params:
|
|
116
|
+
if p.kind == "scalar":
|
|
117
|
+
inputs.append(mx.array(np.array([data.scalars[p.name]], dtype=SCALAR_NP[p.dtype])))
|
|
118
|
+
outs = [p for p in spec.params if p.is_output]
|
|
119
|
+
out_dtypes = [p.resolved_dtype(data.dtype) for p in outs]
|
|
120
|
+
out_shapes = [tuple(data.tensors[p.name].shape) for p in outs]
|
|
121
|
+
template = [("T", mx_dtype(data.dtype))] if any(p.dtype == "T" for p in spec.params) else []
|
|
122
|
+
|
|
123
|
+
arrays = {p.name: data.tensors[p.name].numpy() if data.tensors[p.name].dtype != torch.bfloat16
|
|
124
|
+
else data.tensors[p.name] for p in spec.params if p.is_tensor}
|
|
125
|
+
g = _triple(mod.grid(**bind_kwargs(mod.grid, spec, arrays, data.scalars)), "grid()")
|
|
126
|
+
tg = _triple(getattr(mod, "THREADGROUP", (256, 1, 1)), "THREADGROUP")
|
|
127
|
+
init = getattr(mod, "INIT_VALUE", math.nan)
|
|
128
|
+
state: dict = {}
|
|
129
|
+
|
|
130
|
+
def launch() -> None:
|
|
131
|
+
state["outs"] = kernel(inputs=inputs, template=template, grid=g, threadgroup=tg,
|
|
132
|
+
output_shapes=out_shapes, output_dtypes=[mx_dtype(d) for d in out_dtypes],
|
|
133
|
+
init_value=init, verbose=False)
|
|
134
|
+
|
|
135
|
+
def sync() -> None:
|
|
136
|
+
if "outs" in state:
|
|
137
|
+
mx.eval(*state["outs"])
|
|
138
|
+
|
|
139
|
+
def fetch() -> dict:
|
|
140
|
+
return {p.name: _to_torch(a, d) for p, a, d in zip(outs, state["outs"], out_dtypes)}
|
|
141
|
+
|
|
142
|
+
return Prepared(launch=launch, sync=sync, fetch=fetch, timer="wall_clock")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Triton backend (NVIDIA CUDA and AMD ROCm)."""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Can Triton ports run here? Imports nothing heavy at module level."""
|
|
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("torch") is None:
|
|
11
|
+
return False, "torch not installed (pip install kernelport[triton])"
|
|
12
|
+
if importlib.util.find_spec("triton") is None:
|
|
13
|
+
return False, "triton not installed (pip install kernelport[triton]; triton-windows on Windows)"
|
|
14
|
+
import torch
|
|
15
|
+
|
|
16
|
+
if not torch.cuda.is_available():
|
|
17
|
+
return False, "no CUDA or ROCm GPU visible to torch"
|
|
18
|
+
try:
|
|
19
|
+
import triton # noqa: F401
|
|
20
|
+
except Exception as exc:
|
|
21
|
+
return False, f"triton failed to import: {exc}"
|
|
22
|
+
return True, "Triton on " + ("ROCm" if getattr(torch.version, "hip", None) else "CUDA")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def hardware_name() -> str:
|
|
26
|
+
"""GPU name with arch, e.g. 'NVIDIA GeForce RTX 5060 Laptop GPU (sm_120)'."""
|
|
27
|
+
from kernelport.hardware import gpu_name
|
|
28
|
+
|
|
29
|
+
return gpu_name() or "no GPU"
|