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,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"