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,273 @@
1
+ """Device-only view of a real-world CUDA file, for NVRTC.
2
+
3
+ Real .cu files mix kernels with host code: CPU reference functions, main(),
4
+ cudaMalloc, <<<...>>> launches, cuBLAS handles, <stdio.h>. NVRTC compiles device
5
+ code only and has no host headers, so compiling such a file as-is fails.
6
+
7
+ ``device_source`` walks the file's top level and keeps what device code can need:
8
+
9
+ - ``__global__`` / ``__device__`` functions (also via macros such as
10
+ ``#define DEVICE __device__ __forceinline__``)
11
+ - types (struct/class/union/enum), typedefs, ``using``, templates of those
12
+ - ``__device__`` / ``__constant__`` / const / constexpr variables
13
+ - preprocessor directives, except host-only ``#include <...>`` headers
14
+ - local ``#include "x.h"`` files, inlined (once each) with ``#line`` markers
15
+
16
+ and blanks out host functions, host prototypes and host globals. Blanked text is
17
+ replaced by spaces with every newline kept, so NVRTC's line numbers still point at
18
+ the user's file. The original file is what gets hashed and frozen; this view is
19
+ only what NVRTC compiles.
20
+
21
+ This is lexical, not a C++ front end: kernels generated by macros, or a top-level
22
+ macro call with no trailing ``;`` followed by a kernel, can confuse it.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import re
28
+ from pathlib import Path
29
+
30
+ from kernelport.analyzer.cuda_parser import strip_comments_and_strings
31
+
32
+ # Headers NVRTC (or hipRTC) can provide; every other <...> include is host-only.
33
+ DEVICE_HEADER_PREFIXES = ("cuda_fp16", "cuda_bf16", "cuda_fp8", "cuda_fp4", "cuda_fp6", "cooperative_groups",
34
+ "mma.h", "cuda/", "cub/", "curand_kernel", "curand_philox", "cuda_pipeline",
35
+ "cuda_awbarrier", "sm_", "crt/", "hip/", "vector_types.h", "vector_functions.h")
36
+
37
+ _CUDA_HOST_HEADERS = ("cuda_runtime", "cuda.h", "cublas", "cudnn", "cusparse", "curand.h", "cufft", "nccl",
38
+ "cusolver", "nvToolsExt", "nvtx")
39
+
40
+ # Constants host headers would have supplied (math.h, float.h, limits.h).
41
+ SHIM = """#ifndef M_PI
42
+ #define M_PI 3.14159265358979323846
43
+ #endif
44
+ #ifndef M_SQRT2
45
+ #define M_SQRT2 1.41421356237309504880
46
+ #endif
47
+ #ifndef M_SQRT1_2
48
+ #define M_SQRT1_2 0.70710678118654752440
49
+ #endif
50
+ #ifndef M_E
51
+ #define M_E 2.7182818284590452354
52
+ #endif
53
+ #ifndef M_LN2
54
+ #define M_LN2 0.69314718055994530942
55
+ #endif
56
+ #ifndef FLT_MAX
57
+ #define FLT_MAX 3.402823466e+38F
58
+ #endif
59
+ #ifndef FLT_MIN
60
+ #define FLT_MIN 1.175494351e-38F
61
+ #endif
62
+ #ifndef FLT_EPSILON
63
+ #define FLT_EPSILON 1.192092896e-07F
64
+ #endif
65
+ #ifndef DBL_MAX
66
+ #define DBL_MAX 1.7976931348623158e+308
67
+ #endif
68
+ #ifndef INT_MAX
69
+ #define INT_MAX 2147483647
70
+ #endif
71
+ #ifndef INT_MIN
72
+ #define INT_MIN (-2147483647 - 1)
73
+ #endif
74
+ #ifndef UINT_MAX
75
+ #define UINT_MAX 0xffffffffU
76
+ #endif
77
+ """
78
+
79
+ _DEVICE_WORDS = {"__global__", "__device__"}
80
+ _KEEP_DECL_WORDS = {"__device__", "__constant__", "__shared__", "__managed__", "constexpr", "static_assert",
81
+ "typedef", "using", "const", "struct", "class", "union", "enum", "template"}
82
+ _TYPE_WORDS = {"struct", "class", "union", "enum"}
83
+ _INCLUDE = re.compile(r'#\s*include\s*([<"])([^>"]+)[>"]')
84
+ _OBJECT_MACRO = re.compile(r"^[ \t]*#[ \t]*define[ \t]+(\w+)[ \t]+(.*)$", re.M)
85
+
86
+
87
+ def _strip_templates(s: str) -> str:
88
+ """Remove <...> template argument lists (innermost first)."""
89
+ prev = None
90
+ while prev != s:
91
+ prev, s = s, re.sub(r"<[^<>;{}()]*>", "", s)
92
+ return s
93
+
94
+
95
+ def _flatten(header: str) -> str:
96
+ """Header with operator names, comparisons, template args and (...)/[...] groups removed."""
97
+ s = re.sub(r"\boperator\s*[^\w\s(]+", "operator", header)
98
+ s = re.sub(r"[<>!=]=", "", s)
99
+ s = _strip_templates(s)
100
+ prev = None
101
+ while prev != s:
102
+ prev, s = s, re.sub(r"\([^()]*\)|\[[^\[\]]*\]", "", s)
103
+ return s
104
+
105
+
106
+ class SliceError(ValueError):
107
+ """The file could not be sliced (e.g. an include escapes the repo)."""
108
+
109
+
110
+ def _blank(chars: list[str], start: int, end: int) -> None:
111
+ for k in range(start, end):
112
+ if chars[k] != "\n":
113
+ chars[k] = " "
114
+
115
+
116
+ def _device_markers(clean: str) -> set[str]:
117
+ """Words that mark device code: __global__/__device__ and object-like macros expanding to them."""
118
+ marks = set(_DEVICE_WORDS)
119
+ for name, body in _OBJECT_MACRO.findall(clean):
120
+ if set(re.findall(r"\w+", body)) & marks:
121
+ marks.add(name)
122
+ return marks
123
+
124
+
125
+ def _match(clean: str, i: int, open_ch: str, close_ch: str) -> int:
126
+ depth = 0
127
+ for k in range(i, len(clean)):
128
+ if clean[k] == open_ch:
129
+ depth += 1
130
+ elif clean[k] == close_ch:
131
+ depth -= 1
132
+ if depth == 0:
133
+ return k + 1
134
+ return len(clean)
135
+
136
+
137
+ def _directive_end(clean: str, i: int) -> int:
138
+ """End (exclusive, before the newline) of the preprocessor line starting at i, with continuations."""
139
+ while True:
140
+ j = clean.find("\n", i)
141
+ if j < 0:
142
+ return len(clean)
143
+ if clean[:j].rstrip(" \t\r").endswith("\\"):
144
+ i = j + 1
145
+ continue
146
+ return j
147
+
148
+
149
+ def _at_line_start(clean: str, i: int) -> bool:
150
+ k = clean.rfind("\n", 0, i) + 1
151
+ return clean[k:i].strip() == ""
152
+
153
+
154
+ class _Slicer:
155
+ def __init__(self, root: Path | None):
156
+ self.root = root.resolve() if root else None
157
+ self.seen: set[Path] = set()
158
+ self.dropped_cuda_host = False
159
+
160
+ def file(self, path: Path) -> str:
161
+ path = path.resolve()
162
+ self.seen.add(path)
163
+ text = path.read_text(encoding="utf-8", errors="replace")
164
+ clean = strip_comments_and_strings(text)
165
+ chars = list(text)
166
+ self.marks = getattr(self, "marks", set()) | _device_markers(clean) # includes add theirs
167
+ inserts: dict[int, str] = {}
168
+ self._region(text, clean, chars, 0, len(text), path, inserts)
169
+ out, last = [], 0
170
+ for pos in sorted(inserts):
171
+ out.append("".join(chars[last:pos]))
172
+ out.append(inserts[pos])
173
+ last = pos
174
+ out.append("".join(chars[last:]))
175
+ return "".join(out)
176
+
177
+ def _region(self, text, clean, chars, i, end, path, inserts) -> None:
178
+ while i < end:
179
+ while i < end and clean[i] in " \t\r\n":
180
+ i += 1
181
+ if i >= end:
182
+ return
183
+ if clean[i] == "#" and _at_line_start(clean, i):
184
+ j = _directive_end(clean, i)
185
+ self._directive(text, clean, chars, i, j, path, inserts)
186
+ i = j
187
+ continue
188
+ if clean[i] == ";": # stray semicolon
189
+ i += 1
190
+ continue
191
+ j, body = self._item_end(clean, i, end)
192
+ header = clean[i:body[0]] if body else clean[i:j]
193
+ words = set(re.findall(r"\w+", header))
194
+ if body and (header.split()[:1] == ["namespace"] or re.fullmatch(r'\s*extern\s*"\s*"\s*', header)):
195
+ self._region(text, clean, chars, body[0] + 1, body[1] - 1, path, inserts)
196
+ elif body and not self._is_type_or_init(header):
197
+ if not words & self.marks: # host function definition
198
+ _blank(chars, i, j)
199
+ elif not body and not self._keep_declaration(header, words):
200
+ _blank(chars, i, j)
201
+ i = j
202
+
203
+ def _item_end(self, clean, i, end) -> tuple[int, tuple[int, int] | None]:
204
+ """(end of the item, (body_start, body_end) if it has a top-level { } body)."""
205
+ k = i
206
+ while k < end:
207
+ c = clean[k]
208
+ if c in "([":
209
+ k = _match(clean, k, c, ")" if c == "(" else "]")
210
+ continue
211
+ if c == ";":
212
+ return k + 1, None
213
+ if c == "{":
214
+ b_end = _match(clean, k, "{", "}")
215
+ header = clean[i:k]
216
+ if self._is_type_or_init(header): # struct S {...}; / int a[] = {...};
217
+ semi = clean.find(";", b_end, end)
218
+ return (end if semi < 0 else semi + 1), (k, b_end)
219
+ return b_end, (k, b_end)
220
+ k += 1
221
+ return end, None
222
+
223
+ @staticmethod
224
+ def _is_type_or_init(header: str) -> bool:
225
+ """True for `struct S {` / `enum class E : int {` / `T a[] = {` headers; False for functions."""
226
+ h = _flatten(header)
227
+ if "=" in h:
228
+ return True
229
+ return bool(set(re.findall(r"\w+", h)) & _TYPE_WORDS) and not header.rstrip().endswith(")")
230
+
231
+ def _keep_declaration(self, header: str, words: set[str]) -> bool:
232
+ if words & self.marks or words & {"__constant__", "__shared__", "__managed__"}:
233
+ return True
234
+ if words & {"typedef", "using", "static_assert"}:
235
+ return True
236
+ if "(" in re.sub(r"=.*", "", _strip_templates(header), flags=re.S):
237
+ return False # host function prototype, or a host object built by a constructor
238
+ return bool(words & _KEEP_DECL_WORDS)
239
+
240
+ def _directive(self, text, clean, chars, i, j, path, inserts) -> None:
241
+ m = _INCLUDE.match(text[i:j].strip())
242
+ if not m:
243
+ return
244
+ kind, name = m.groups()
245
+ if kind == "<":
246
+ if not name.startswith(DEVICE_HEADER_PREFIXES):
247
+ _blank(chars, i, j)
248
+ if name.startswith(_CUDA_HOST_HEADERS):
249
+ self.dropped_cuda_host = True
250
+ return
251
+ inc = (path.parent / name).resolve()
252
+ if not inc.is_file():
253
+ return # leave it to the compiler's -I search (and its error message)
254
+ if self.root and self.root not in inc.parents:
255
+ raise SliceError(f"{path.name} includes {name}, which is outside the repo")
256
+ _blank(chars, i, j)
257
+ if inc in self.seen:
258
+ return
259
+ line = text.count("\n", 0, j) + 2
260
+ body = self.file(inc)
261
+ at = j + 1 if j < len(text) else j
262
+ inserts[at] = (("" if at > j else "\n") +
263
+ f'#line 1 "{inc.name}"\n{body}\n#line {line} "{path.name}"\n')
264
+
265
+
266
+ def device_source(path: Path, root: Path | None = None) -> str:
267
+ """The device-only view of ``path`` (see module docstring), with a small constants shim."""
268
+ slicer = _Slicer(root)
269
+ sliced = slicer.file(Path(path))
270
+ # Under nvcc, cuda_runtime.h / cublas headers pull in the half and bfloat16 types; code
271
+ # that relied on that still needs them.
272
+ fp = "#include <cuda_fp16.h>\n#include <cuda_bf16.h>\n" if slicer.dropped_cuda_host else ""
273
+ return f'{SHIM}{fp}#line 1 "{Path(path).name}"\n{sliced}'
@@ -0,0 +1,80 @@
1
+ """Tiny, safe integer expression evaluator for launch specs.
2
+
3
+ Shapes, grid sizes and scalar values in launch.json are written as strings
4
+ like ``"cdiv(n, 256)"`` or ``"rows * cols"``. They are evaluated against the
5
+ case's dims. Only arithmetic on names and numbers plus a few helper functions
6
+ is allowed; anything else raises ValueError.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import ast
12
+ import math
13
+ import operator
14
+ from typing import Any
15
+
16
+ _BINOPS = {
17
+ ast.Add: operator.add,
18
+ ast.Sub: operator.sub,
19
+ ast.Mult: operator.mul,
20
+ ast.Div: operator.truediv,
21
+ ast.FloorDiv: operator.floordiv,
22
+ ast.Mod: operator.mod,
23
+ ast.Pow: operator.pow,
24
+ ast.LShift: operator.lshift,
25
+ ast.RShift: operator.rshift,
26
+ }
27
+
28
+
29
+ def _next_pow2(x: int) -> int:
30
+ return 1 if x <= 1 else 1 << (int(x) - 1).bit_length()
31
+
32
+
33
+ _FUNCS = {
34
+ "cdiv": lambda a, b: -(-a // b),
35
+ "min": min,
36
+ "max": max,
37
+ "next_pow2": _next_pow2,
38
+ "ceil": math.ceil,
39
+ "floor": math.floor,
40
+ "int": int,
41
+ }
42
+
43
+
44
+ def evaluate(expr: Any, dims: dict[str, Any]) -> Any:
45
+ """Evaluate ``expr`` (a number or expression string) with ``dims`` as variables."""
46
+ if isinstance(expr, (int, float)):
47
+ return expr
48
+ if not isinstance(expr, str):
49
+ raise ValueError(f"expression must be a number or string, got {expr!r}")
50
+ tree = ast.parse(expr, mode="eval")
51
+ return _eval(tree.body, dims)
52
+
53
+
54
+ def _eval(node: ast.AST, dims: dict[str, Any]) -> Any:
55
+ if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
56
+ return node.value
57
+ if isinstance(node, ast.Name):
58
+ if node.id not in dims:
59
+ raise ValueError(f"unknown dimension {node.id!r}")
60
+ return dims[node.id]
61
+ if isinstance(node, ast.BinOp) and type(node.op) in _BINOPS:
62
+ return _BINOPS[type(node.op)](_eval(node.left, dims), _eval(node.right, dims))
63
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.USub, ast.UAdd)):
64
+ v = _eval(node.operand, dims)
65
+ return -v if isinstance(node.op, ast.USub) else v
66
+ if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id in _FUNCS:
67
+ if node.keywords:
68
+ raise ValueError("keyword arguments are not allowed")
69
+ return _FUNCS[node.func.id](*[_eval(a, dims) for a in node.args])
70
+ raise ValueError(f"unsupported expression element: {ast.dump(node)}")
71
+
72
+
73
+ def evaluate_int(expr: Any, dims: dict[str, Any]) -> int:
74
+ """Evaluate and require an integer result."""
75
+ v = evaluate(expr, dims)
76
+ if isinstance(v, float):
77
+ if not v.is_integer():
78
+ raise ValueError(f"{expr!r} evaluated to non-integer {v}")
79
+ v = int(v)
80
+ return int(v)
@@ -0,0 +1,194 @@
1
+ """Deterministic input generation from a launch spec.
2
+
3
+ Values come from NumPy's PCG64 ``random_raw`` stream, whose bit output is
4
+ guaranteed stable across NumPy versions and platforms. Uniform and normal
5
+ values are derived from those raw bits here (not via NumPy's distribution
6
+ methods, which are allowed to change), so any machine regenerates
7
+ bit-identical inputs from the same spec.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import hashlib
13
+ import json
14
+ import math
15
+ import zlib
16
+ from dataclasses import dataclass
17
+ from functools import lru_cache
18
+ from typing import Any
19
+
20
+ import numpy as np
21
+ import torch
22
+
23
+ from kernelport.harness.expr import evaluate
24
+ from kernelport.harness.spec import FLOAT_DTYPES, CaseSpec, LaunchSpec, ParamSpec
25
+
26
+ TORCH_DTYPES: dict[str, torch.dtype] = {
27
+ "float32": torch.float32,
28
+ "float16": torch.float16,
29
+ "bfloat16": torch.bfloat16,
30
+ "float64": torch.float64,
31
+ "int8": torch.int8,
32
+ "uint8": torch.uint8,
33
+ "int16": torch.int16,
34
+ "int32": torch.int32,
35
+ "int64": torch.int64,
36
+ }
37
+
38
+ INT_SENTINEL = -0x5A5A5A5 # fills int outputs so unwritten elements are visible
39
+ DEFAULT_EDGE_BIG = 1000.0
40
+ NUMERIC_INIT_KEYS = ("low", "high", "mean", "std", "edge_big", "step")
41
+
42
+
43
+ @dataclass
44
+ class CaseData:
45
+ """Concrete tensors and scalars for one (case, dtype)."""
46
+
47
+ case: str
48
+ dtype: str
49
+ dims: dict[str, Any]
50
+ tensors: dict[str, torch.Tensor] # CPU tensors, inputs and pre-filled outputs
51
+ scalars: dict[str, Any]
52
+ input_hash: str
53
+
54
+ @property
55
+ def label(self) -> str:
56
+ """Short name such as ``odd_1000/float16``."""
57
+ return f"{self.case}/{self.dtype}"
58
+
59
+
60
+ def _uniform01(gen: np.random.Generator, n: int) -> np.ndarray:
61
+ """n doubles in [0, 1): 52 raw bits placed in the mantissa of [1, 2), minus 1. Exact."""
62
+ raw = gen.bit_generator.random_raw(n)
63
+ raw >>= np.uint64(12)
64
+ raw |= np.uint64(0x3FF0000000000000)
65
+ out = raw.view(np.float64)
66
+ out -= 1.0
67
+ return out
68
+
69
+
70
+ def _normal(gen: np.random.Generator, n: int) -> np.ndarray:
71
+ """n values with mean 0 and std 1, bell-shaped (Irwin-Hall: sum of 4 uniforms).
72
+
73
+ Only uses IEEE add/multiply, which are exactly rounded on every platform.
74
+ Box-Muller would need log/sin/cos, whose last bit differs between libms.
75
+ Values are bounded to about (-3.46, 3.46), which is fine for test inputs.
76
+ """
77
+ s = _uniform01(gen, n)
78
+ for _ in range(3): # elementwise adds in a fixed order
79
+ s += _uniform01(gen, n)
80
+ s -= 2.0
81
+ s *= 1.7320508075688772 # sqrt(3): var of the sum of 4 uniforms is 1/3
82
+ return s
83
+
84
+
85
+ def _seed_for(spec: LaunchSpec, case: CaseSpec, param: str) -> int:
86
+ # Same values for every dtype of a case, so dtypes are comparable.
87
+ return (spec.seed * 1_000_003 + zlib.crc32(f"{case.name}|{param}".encode())) & 0xFFFFFFFF
88
+
89
+
90
+ def _edge_values(big: float) -> list[float]:
91
+ return [0.0, -0.0, 1.0, -1.0, big, -big, 1.0 / big, -1.0 / big]
92
+
93
+
94
+ def _generate(init: dict, shape: tuple[int, ...], dtype: str, seed: int) -> torch.Tensor:
95
+ is_float = dtype in FLOAT_DTYPES
96
+ vals = _base_values(json.dumps(init, sort_keys=True), shape, is_float, seed)
97
+ return torch.from_numpy(vals.copy()).to(TORCH_DTYPES[dtype])
98
+
99
+
100
+ @lru_cache(maxsize=4) # dtypes of one case share values; keep the last few
101
+ def _base_values(init_json: str, shape: tuple[int, ...], is_float: bool, seed: int) -> np.ndarray:
102
+ """float64 values before the dtype cast."""
103
+ init = json.loads(init_json)
104
+ n = int(np.prod(shape)) if shape else 1
105
+ gen = np.random.Generator(np.random.PCG64(seed))
106
+ dist = init.get("dist", "normal" if is_float else "randint")
107
+ if dist == "normal":
108
+ vals = init.get("mean", 0.0) + init.get("std", 1.0) * _normal(gen, n)
109
+ elif dist == "uniform":
110
+ lo, hi = init.get("low", -1.0), init.get("high", 1.0)
111
+ vals = lo + (hi - lo) * _uniform01(gen, n)
112
+ elif dist == "randint":
113
+ lo, hi = int(init.get("low", 0)), int(init.get("high", 100))
114
+ vals = np.floor(lo + (hi - lo) * _uniform01(gen, n))
115
+ elif dist == "zeros":
116
+ vals = np.zeros(n)
117
+ elif dist == "ones":
118
+ vals = np.ones(n)
119
+ elif dist == "arange":
120
+ vals = np.arange(n, dtype=np.float64)
121
+ else:
122
+ raise ValueError(f"unknown dist {dist!r}")
123
+ if init.get("edges") and is_float:
124
+ edges = _edge_values(float(init.get("edge_big", DEFAULT_EDGE_BIG)))[:n]
125
+ vals[: len(edges)] = edges
126
+ step = init.get("step")
127
+ if step and is_float:
128
+ # Snap to multiples of a power-of-two step. Sums of such values are exact
129
+ # in float32 (while partial sums stay below 2**24 * step), so reductions
130
+ # give the same answer in any order and strict tolerances still apply.
131
+ if not math.log2(step).is_integer():
132
+ raise ValueError(f"step must be a power of two, got {step}")
133
+ vals = np.round(vals / step) * step
134
+ vals = np.ascontiguousarray(vals.reshape(shape))
135
+ vals.flags.writeable = False
136
+ return vals
137
+
138
+
139
+ def _sentinel(shape: tuple[int, ...], dtype: str) -> torch.Tensor:
140
+ if dtype in FLOAT_DTYPES:
141
+ return torch.full(shape, float("nan"), dtype=TORCH_DTYPES[dtype])
142
+ return torch.full(shape, INT_SENTINEL, dtype=torch.int64).to(TORCH_DTYPES[dtype])
143
+
144
+
145
+ def tensor_bytes(t: torch.Tensor) -> bytes:
146
+ """Raw little-endian bytes of a CPU tensor, for hashing."""
147
+ t = t.detach().cpu().contiguous()
148
+ if t.dim() == 0:
149
+ t = t.reshape(1)
150
+ return t.view(torch.uint8).numpy().tobytes()
151
+
152
+
153
+ def _as_scalar_dtype(value, dtype: str):
154
+ """The value the kernel actually receives: 0.999 as a float32 param is 0.99900001...
155
+
156
+ References, originals and ports all get this rounded value, so a reference never
157
+ disagrees with the kernel just because it used the unrounded Python float.
158
+ """
159
+ if dtype in ("float32", "float16"):
160
+ return float(np.dtype(dtype).type(value))
161
+ if dtype in ("int32", "int64", "uint32", "int8", "uint8", "int16"):
162
+ return int(value)
163
+ return value
164
+
165
+
166
+ def make_case(spec: LaunchSpec, case: CaseSpec, dtype: str) -> CaseData:
167
+ """Build CPU tensors for one case/dtype. Outputs are pre-filled with a sentinel."""
168
+ tensors: dict[str, torch.Tensor] = {}
169
+ scalars: dict[str, Any] = {}
170
+ h = hashlib.sha256()
171
+ for p in spec.params:
172
+ if not p.is_tensor:
173
+ scalars[p.name] = _as_scalar_dtype(p.resolved_value(case.dims), p.dtype)
174
+ continue
175
+ shape = p.resolved_shape(case.dims)
176
+ pdtype = p.resolved_dtype(dtype)
177
+ if p.kind == "output":
178
+ t = _sentinel(shape, pdtype)
179
+ else:
180
+ init = {**spec.init.get(p.name, {}), **case.init.get(p.name, {})}
181
+ # Numeric fields may be expressions over dims, e.g. "high": "max_pos".
182
+ init = {k: (evaluate(v, case.dims) if k in NUMERIC_INIT_KEYS and isinstance(v, str) else v)
183
+ for k, v in init.items()}
184
+ t = _generate(init, shape, pdtype, _seed_for(spec, case, p.name))
185
+ tensors[p.name] = t
186
+ h.update(f"{p.name}:{pdtype}:{shape}".encode())
187
+ h.update(tensor_bytes(t))
188
+ h.update(repr(sorted(scalars.items())).encode())
189
+ return CaseData(case.name, dtype, dict(case.dims), tensors, scalars, h.hexdigest())
190
+
191
+
192
+ def output_params(spec: LaunchSpec) -> list[ParamSpec]:
193
+ """Params the kernel writes."""
194
+ return [p for p in spec.params if p.is_output]
@@ -0,0 +1,76 @@
1
+ """Load and run a plain-PyTorch reference.
2
+
3
+ A reference file defines::
4
+
5
+ def reference(a, b, n): # inputs/inouts and scalars, by name
6
+ return a + b # one output, or a dict {name: tensor}
7
+
8
+ Floating-point inputs are passed as float64 CPU tensors so the reference is
9
+ as close to the exact answer as possible. Integer inputs keep their dtype.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import importlib.util
16
+ import inspect
17
+ import sys
18
+ from pathlib import Path
19
+ from types import ModuleType
20
+
21
+ import torch
22
+
23
+ from kernelport.harness.inputs import CaseData
24
+ from kernelport.harness.spec import LaunchSpec
25
+ from kernelport.hashing import sha256_file
26
+
27
+
28
+ def file_hash(path: Path) -> str:
29
+ """sha256 of a file's contents (line-ending insensitive)."""
30
+ return sha256_file(path)
31
+
32
+
33
+ def load_module(path: Path, prefix: str = "kp_mod") -> ModuleType:
34
+ """Import a Python file under a unique module name."""
35
+ path = Path(path).resolve()
36
+ name = f"{prefix}_{hashlib.sha1(str(path).encode()).hexdigest()[:12]}_{file_hash(path)[:8]}"
37
+ spec = importlib.util.spec_from_file_location(name, path)
38
+ if spec is None or spec.loader is None:
39
+ raise ImportError(f"cannot import {path}")
40
+ mod = importlib.util.module_from_spec(spec)
41
+ sys.modules[name] = mod # some libraries (triton) need to find the module again
42
+ spec.loader.exec_module(mod)
43
+ return mod
44
+
45
+
46
+ def run_reference(path: Path, spec: LaunchSpec, data: CaseData) -> dict[str, torch.Tensor]:
47
+ """Run the reference for one case and return {output_name: float64/int CPU tensor}."""
48
+ fn = getattr(load_module(path, "kp_ref"), "reference", None)
49
+ if fn is None:
50
+ raise AttributeError(f"{path} does not define reference(...)")
51
+ kwargs = {}
52
+ for p in spec.params:
53
+ if p.kind in ("input", "inout"):
54
+ t = data.tensors[p.name].clone()
55
+ kwargs[p.name] = t.double() if t.is_floating_point() else t
56
+ elif p.kind == "scalar":
57
+ kwargs[p.name] = data.scalars[p.name]
58
+ accepted = inspect.signature(fn).parameters
59
+ if not any(v.kind == v.VAR_KEYWORD for v in accepted.values()):
60
+ missing = [k for k in accepted if k not in kwargs]
61
+ if missing:
62
+ raise TypeError(f"reference() asks for unknown params {missing}; available: {list(kwargs)}")
63
+ kwargs = {k: v for k, v in kwargs.items() if k in accepted}
64
+ with torch.no_grad():
65
+ out = fn(**kwargs)
66
+ outs = [p for p in spec.params if p.is_output]
67
+ if isinstance(out, torch.Tensor):
68
+ if len(outs) != 1:
69
+ raise TypeError(f"reference returned one tensor but kernel has outputs {[p.name for p in outs]}")
70
+ out = {outs[0].name: out}
71
+ if not isinstance(out, dict):
72
+ raise TypeError("reference must return a tensor or a dict of tensors")
73
+ missing = [p.name for p in outs if p.name not in out]
74
+ if missing:
75
+ raise TypeError(f"reference did not return outputs {missing}")
76
+ return {k: v.detach().cpu() for k, v in out.items()}