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,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()}
|