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,480 @@
|
|
|
1
|
+
"""Find and parse CUDA ``__global__`` kernels without a compiler.
|
|
2
|
+
|
|
3
|
+
This is a careful lexical parser, not a C++ front end. It handles comments,
|
|
4
|
+
string literals, templates, ``__launch_bounds__``, ``extern "C"``, multi-line
|
|
5
|
+
signatures, pointer aliases (``T* row = out + r * n``), and ``<<<...>>>``
|
|
6
|
+
launch sites. Kernels embedded as strings in Python files (CuPy RawKernel,
|
|
7
|
+
torch load_inline) are found too.
|
|
8
|
+
|
|
9
|
+
Everything here is pure Python with no torch import, so it runs anywhere.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import ast
|
|
15
|
+
import re
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from kernelport.analyzer.models import KernelInfo, LaunchConfig, Param
|
|
19
|
+
|
|
20
|
+
CUDA_SUFFIXES = {".cu", ".cuh"}
|
|
21
|
+
MAYBE_CUDA_SUFFIXES = {".h", ".hpp", ".hh", ".cpp", ".cc", ".cxx", ".inl"}
|
|
22
|
+
SKIP_DIRS = {".git", ".hg", ".svn", ".venv", "venv", "env", "node_modules", "__pycache__",
|
|
23
|
+
".kernelport", "build", "dist", ".tox", ".mypy_cache", ".pytest_cache", "site-packages"}
|
|
24
|
+
MAX_FILE_BYTES = 4_000_000
|
|
25
|
+
|
|
26
|
+
DTYPE_MAP = {
|
|
27
|
+
"float": "float32", "double": "float64",
|
|
28
|
+
"__half": "float16", "half": "float16", "at::Half": "float16", "c10::Half": "float16",
|
|
29
|
+
"__nv_bfloat16": "bfloat16", "nv_bfloat16": "bfloat16", "at::BFloat16": "bfloat16",
|
|
30
|
+
"c10::BFloat16": "bfloat16",
|
|
31
|
+
"int": "int32", "int32_t": "int32", "unsigned": "int32", "unsigned int": "int32", "uint32_t": "int32",
|
|
32
|
+
"long long": "int64", "long long int": "int64", "int64_t": "int64", "long": "int64",
|
|
33
|
+
"size_t": "int64", "unsigned long long": "int64", "uint64_t": "int64",
|
|
34
|
+
"short": "int16", "int16_t": "int16", "char": "int8", "signed char": "int8", "int8_t": "int8",
|
|
35
|
+
"unsigned char": "uint8", "uint8_t": "uint8", "bool": "uint8",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
FEATURE_PATTERNS: dict[str, str] = {
|
|
39
|
+
"shared_memory": r"__shared__",
|
|
40
|
+
"dynamic_shared_memory": r"extern\s+__shared__",
|
|
41
|
+
"syncthreads": r"__syncthreads\s*\(|__syncwarp\s*\(",
|
|
42
|
+
"warp_shuffle": r"__shfl\w*\s*\(",
|
|
43
|
+
"warp_vote": r"__ballot_sync|__any_sync|__all_sync|__activemask",
|
|
44
|
+
"atomics": r"\batomic[A-Z]\w*\s*\(",
|
|
45
|
+
"fp16": r"\b__half2?\b|\bhalf2\b|__float2half|__half2float|__hadd|__hmul",
|
|
46
|
+
"bf16": r"__nv_bfloat16|__bfloat162float|__float2bfloat16",
|
|
47
|
+
"vector_loads": r"\b(?:float|int|uint|half|double)[234]\b|__ldg\s*\(|reinterpret_cast\s*<\s*(?:const\s+)?\w+[234]\s*\*",
|
|
48
|
+
"grid_stride_loop": r"blockDim\.\w\s*\*\s*gridDim\.\w|gridDim\.\w\s*\*\s*blockDim\.\w",
|
|
49
|
+
"multi_dim_indexing": r"(?:blockIdx|threadIdx)\.[yz]\b",
|
|
50
|
+
"cooperative_groups": r"cooperative_groups|\bcg::",
|
|
51
|
+
"tensor_cores": r"\bwmma::|mma\.sync|__hmma",
|
|
52
|
+
"inline_ptx": r"\basm\s*(?:volatile\s*)?\(",
|
|
53
|
+
"fast_math_intrinsics": r"__expf|__logf|__sinf|__cosf|__fdividef|__powf|__frsqrt_rn",
|
|
54
|
+
"printf": r"\bprintf\s*\(",
|
|
55
|
+
"textures": r"\btex[123]D\w*\s*\(|cudaTextureObject_t",
|
|
56
|
+
"dynamic_parallelism": r"<<<",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
_ASSIGN_OPS = ("<<=", ">>=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=")
|
|
60
|
+
_QUALIFIERS = {"const", "volatile", "__restrict__", "__restrict", "restrict", "struct", "__grid_constant__"}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ----------------------------------------------------------------- lexing
|
|
64
|
+
|
|
65
|
+
def strip_comments_and_strings(text: str) -> str:
|
|
66
|
+
"""Blank out comments and the contents of string/char literals.
|
|
67
|
+
|
|
68
|
+
The result has the same length and line structure as ``text``, so offsets
|
|
69
|
+
and line numbers stay valid.
|
|
70
|
+
"""
|
|
71
|
+
out = list(text)
|
|
72
|
+
i, n = 0, len(text)
|
|
73
|
+
while i < n:
|
|
74
|
+
c = text[i]
|
|
75
|
+
nxt = text[i + 1] if i + 1 < n else ""
|
|
76
|
+
if c == "/" and nxt == "/":
|
|
77
|
+
j = text.find("\n", i)
|
|
78
|
+
j = n if j < 0 else j
|
|
79
|
+
for k in range(i, j):
|
|
80
|
+
out[k] = " "
|
|
81
|
+
i = j
|
|
82
|
+
elif c == "/" and nxt == "*":
|
|
83
|
+
j = text.find("*/", i + 2)
|
|
84
|
+
j = n if j < 0 else j + 2
|
|
85
|
+
for k in range(i, j):
|
|
86
|
+
if out[k] != "\n":
|
|
87
|
+
out[k] = " "
|
|
88
|
+
i = j
|
|
89
|
+
elif c in "\"'":
|
|
90
|
+
# raw strings R"(...)" are rare in kernels; treat like normal strings
|
|
91
|
+
j = i + 1
|
|
92
|
+
while j < n and text[j] != c:
|
|
93
|
+
if text[j] == "\\":
|
|
94
|
+
j += 1
|
|
95
|
+
elif text[j] == "\n":
|
|
96
|
+
break
|
|
97
|
+
j += 1
|
|
98
|
+
for k in range(i + 1, min(j, n)):
|
|
99
|
+
if out[k] != "\n":
|
|
100
|
+
out[k] = " "
|
|
101
|
+
i = j + 1
|
|
102
|
+
else:
|
|
103
|
+
i += 1
|
|
104
|
+
return "".join(out)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _match_close(text: str, start: int, open_ch: str, close_ch: str) -> int:
|
|
108
|
+
"""Index just past the bracket matching ``text[start]`` (which must be open_ch)."""
|
|
109
|
+
depth = 0
|
|
110
|
+
for i in range(start, len(text)):
|
|
111
|
+
if text[i] == open_ch:
|
|
112
|
+
depth += 1
|
|
113
|
+
elif text[i] == close_ch:
|
|
114
|
+
depth -= 1
|
|
115
|
+
if depth == 0:
|
|
116
|
+
return i + 1
|
|
117
|
+
raise ValueError(f"unbalanced {open_ch}{close_ch} starting at offset {start}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def split_top_level(text: str, sep: str = ",") -> list[str]:
|
|
121
|
+
"""Split on ``sep`` outside (), [], {} and template <> nesting."""
|
|
122
|
+
parts: list[str] = []
|
|
123
|
+
depth = angle = 0
|
|
124
|
+
start = 0
|
|
125
|
+
for i, ch in enumerate(text):
|
|
126
|
+
prev = text[i - 1] if i else ""
|
|
127
|
+
nxt = text[i + 1] if i + 1 < len(text) else ""
|
|
128
|
+
if ch in "([{":
|
|
129
|
+
depth += 1
|
|
130
|
+
elif ch in ")]}":
|
|
131
|
+
depth -= 1
|
|
132
|
+
elif ch == "<" and nxt not in "<=" and prev != "<":
|
|
133
|
+
angle += 1
|
|
134
|
+
elif ch == ">" and angle > 0 and prev != "-" and nxt != "=":
|
|
135
|
+
angle -= 1
|
|
136
|
+
elif ch == sep and depth == 0 and angle == 0:
|
|
137
|
+
parts.append(text[start:i])
|
|
138
|
+
start = i + 1
|
|
139
|
+
parts.append(text[start:])
|
|
140
|
+
return [p.strip() for p in parts if p.strip()]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _line_of(text: str, offset: int) -> int:
|
|
144
|
+
return text.count("\n", 0, offset) + 1
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# ---------------------------------------------------------------- params
|
|
148
|
+
|
|
149
|
+
def _base_type(type_text: str) -> str:
|
|
150
|
+
toks = [t for t in re.split(r"[\s*&]+", type_text) if t and t not in _QUALIFIERS]
|
|
151
|
+
return " ".join(toks)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def parse_param(text: str, template_types: list[str]) -> Param | None:
|
|
155
|
+
"""Parse one parameter declaration such as ``const float* __restrict__ x``."""
|
|
156
|
+
text = " ".join(text.split())
|
|
157
|
+
if not text or text == "void":
|
|
158
|
+
return None
|
|
159
|
+
text = re.sub(r"\s*=\s*[^,]+$", "", text) # drop default values
|
|
160
|
+
m = re.search(r"([A-Za-z_]\w*)\s*((?:\[[^\]]*\])*)$", text)
|
|
161
|
+
if not m:
|
|
162
|
+
return None
|
|
163
|
+
name, arrays = m.group(1), m.group(2)
|
|
164
|
+
type_text = text[: m.start()].strip()
|
|
165
|
+
if not type_text: # unnamed parameter: whole text is the type
|
|
166
|
+
type_text, name = text, ""
|
|
167
|
+
is_pointer = "*" in type_text or bool(arrays)
|
|
168
|
+
if is_pointer:
|
|
169
|
+
first_star = type_text.find("*") if "*" in type_text else len(type_text)
|
|
170
|
+
is_const = re.search(r"\bconst\b", type_text[:first_star]) is not None
|
|
171
|
+
else:
|
|
172
|
+
is_const = re.search(r"\bconst\b", type_text) is not None
|
|
173
|
+
base = _base_type(type_text)
|
|
174
|
+
dtype = DTYPE_MAP.get(base)
|
|
175
|
+
if dtype is None and base in template_types:
|
|
176
|
+
dtype = "T" if base == template_types[0] else base
|
|
177
|
+
return Param(name=name, type=type_text, base_type=base, is_pointer=is_pointer,
|
|
178
|
+
is_const=is_const, dtype=dtype)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _template_type_names(template_params: list[str]) -> list[str]:
|
|
182
|
+
names = []
|
|
183
|
+
for tp in template_params:
|
|
184
|
+
m = re.match(r"(?:typename|class)\s+([A-Za-z_]\w*)", tp)
|
|
185
|
+
if m:
|
|
186
|
+
names.append(m.group(1))
|
|
187
|
+
return names
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ------------------------------------------------------ pointer usage
|
|
191
|
+
|
|
192
|
+
# "T* row = ...", "const float *p = ..." : a type name, then *, then the new pointer name.
|
|
193
|
+
_ALIAS_DECL = re.compile(
|
|
194
|
+
r"\b[A-Za-z_][\w:]*\s*(?:<[^;=]*?>)?\s*\*\s*(?:__restrict__\s+|const\s+)*([A-Za-z_]\w*)\s*=(?!=)\s*([^;]*);")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _aliases(body: str, name: str) -> set[str]:
|
|
198
|
+
"""Names of local pointers derived from ``name`` (e.g. ``T* row = name + off``)."""
|
|
199
|
+
names = {name}
|
|
200
|
+
changed = True
|
|
201
|
+
while changed:
|
|
202
|
+
changed = False
|
|
203
|
+
for m in _ALIAS_DECL.finditer(body):
|
|
204
|
+
alias, rhs = m.group(1), m.group(2)
|
|
205
|
+
if alias not in names and any(re.search(rf"\b{re.escape(n)}\b", rhs) for n in names):
|
|
206
|
+
names.add(alias)
|
|
207
|
+
changed = True
|
|
208
|
+
return names
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _usage(body: str, name: str) -> tuple[bool, bool]:
|
|
212
|
+
"""(read, written) for a pointer parameter, following simple aliases."""
|
|
213
|
+
read = written = False
|
|
214
|
+
names = _aliases(body, name)
|
|
215
|
+
alias_defs = {m.start(1) for m in _ALIAS_DECL.finditer(body)}
|
|
216
|
+
for n in names:
|
|
217
|
+
for m in re.finditer(rf"(?<![\w.]){re.escape(n)}\b", body):
|
|
218
|
+
if m.start() in alias_defs:
|
|
219
|
+
continue # the alias declaration itself
|
|
220
|
+
end = m.end()
|
|
221
|
+
rest = body[end:].lstrip()
|
|
222
|
+
if rest.startswith("["): # subscript: skip to after ]
|
|
223
|
+
close = _match_close(body, body.index("[", end), "[", "]")
|
|
224
|
+
after = body[close:].lstrip()
|
|
225
|
+
else:
|
|
226
|
+
after = rest
|
|
227
|
+
before = body[: m.start()].rstrip()
|
|
228
|
+
deref = before.endswith("*") and not re.search(r"[\w)\]]\s*\*$", before)
|
|
229
|
+
if rest.startswith("[") or deref:
|
|
230
|
+
if re.search(r"[(,]\s*&\s*$", before): # &name[i] passed to atomicAdd or any call
|
|
231
|
+
written = read = True
|
|
232
|
+
elif after.startswith("=") and not after.startswith("=="):
|
|
233
|
+
written = True
|
|
234
|
+
elif after.startswith(_ASSIGN_OPS) or after.startswith(("++", "--")):
|
|
235
|
+
written = read = True
|
|
236
|
+
else:
|
|
237
|
+
read = True
|
|
238
|
+
continue
|
|
239
|
+
# bare name: atomics write through it, other calls may do anything
|
|
240
|
+
call = re.search(r"([A-Za-z_]\w*)\s*\(\s*&?\s*$", before)
|
|
241
|
+
if call and call.group(1).startswith("atomic"):
|
|
242
|
+
written = read = True
|
|
243
|
+
elif re.search(r"[(,]\s*&?\s*$", before) and not re.search(r"=\s*$", before):
|
|
244
|
+
written = read = True # passed to a function: assume both
|
|
245
|
+
elif re.search(r"&\s*$", before): # &name[...] handled above; &name alone
|
|
246
|
+
written = read = True
|
|
247
|
+
else:
|
|
248
|
+
# pointer arithmetic in an expression (e.g. rhs of an alias): not a read of data
|
|
249
|
+
pass
|
|
250
|
+
return read, written
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# --------------------------------------------------------------- kernels
|
|
254
|
+
|
|
255
|
+
def _statement_start(clean: str, pos: int) -> int:
|
|
256
|
+
"""Offset where the declaration containing ``pos`` starts."""
|
|
257
|
+
start = max(clean.rfind(ch, 0, pos) for ch in ";{}") + 1
|
|
258
|
+
after_pp = start
|
|
259
|
+
for m in re.finditer(r"^[ \t]*#[^\n]*$", clean[start:pos], flags=re.M):
|
|
260
|
+
after_pp = start + m.end() # skip past preprocessor lines
|
|
261
|
+
return after_pp
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _device_functions(clean: str) -> dict[str, str]:
|
|
265
|
+
"""name -> body for __device__ (non-global) functions in a file."""
|
|
266
|
+
funcs = {}
|
|
267
|
+
for m in re.finditer(r"__device__\b[^;{}()]*?\b([A-Za-z_]\w*)\s*\(", clean):
|
|
268
|
+
try:
|
|
269
|
+
p_end = _match_close(clean, m.end() - 1, "(", ")")
|
|
270
|
+
except ValueError:
|
|
271
|
+
continue
|
|
272
|
+
rest = clean[p_end:]
|
|
273
|
+
mm = re.match(r"\s*(?:const\s*)?\{", rest)
|
|
274
|
+
if not mm:
|
|
275
|
+
continue
|
|
276
|
+
b_start = p_end + mm.end() - 1
|
|
277
|
+
try:
|
|
278
|
+
b_end = _match_close(clean, b_start, "{", "}")
|
|
279
|
+
except ValueError:
|
|
280
|
+
continue
|
|
281
|
+
funcs[m.group(1)] = clean[b_start:b_end]
|
|
282
|
+
return funcs
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _features(body: str, device_funcs: dict[str, str], template_params: list[str]) -> list[str]:
|
|
286
|
+
text = body
|
|
287
|
+
seen = set()
|
|
288
|
+
stack = [body]
|
|
289
|
+
while stack: # include bodies of __device__ helpers the kernel calls (transitively)
|
|
290
|
+
b = stack.pop()
|
|
291
|
+
for fname, fbody in device_funcs.items():
|
|
292
|
+
if fname not in seen and re.search(rf"\b{re.escape(fname)}\s*\(", b):
|
|
293
|
+
seen.add(fname)
|
|
294
|
+
text += "\n" + fbody
|
|
295
|
+
stack.append(fbody)
|
|
296
|
+
feats = [f for f, pat in FEATURE_PATTERNS.items() if re.search(pat, text)]
|
|
297
|
+
if template_params:
|
|
298
|
+
feats.insert(0, "templates")
|
|
299
|
+
return feats
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def parse_source(text: str, rel_path: str, line_offset: int = 0) -> list[KernelInfo]:
|
|
303
|
+
"""Parse every ``__global__`` kernel definition in one source text."""
|
|
304
|
+
clean = strip_comments_and_strings(text)
|
|
305
|
+
device_funcs = _device_functions(clean)
|
|
306
|
+
kernels: list[KernelInfo] = []
|
|
307
|
+
for gm in re.finditer(r"\b__global__\b", clean):
|
|
308
|
+
pos = gm.end()
|
|
309
|
+
launch_bounds = ""
|
|
310
|
+
# walk tokens up to the parameter list
|
|
311
|
+
name = None
|
|
312
|
+
while pos < len(clean):
|
|
313
|
+
ws = re.match(r"\s*", clean[pos:])
|
|
314
|
+
pos += ws.end()
|
|
315
|
+
lb = re.match(r"__launch_bounds__\s*\(", clean[pos:])
|
|
316
|
+
if lb:
|
|
317
|
+
close = _match_close(clean, pos + lb.end() - 1, "(", ")")
|
|
318
|
+
launch_bounds = " ".join(clean[pos + lb.end():close - 1].split())
|
|
319
|
+
pos = close
|
|
320
|
+
continue
|
|
321
|
+
tok = re.match(r"[A-Za-z_][\w:]*", clean[pos:])
|
|
322
|
+
if tok:
|
|
323
|
+
name = tok.group(0)
|
|
324
|
+
pos += tok.end()
|
|
325
|
+
continue
|
|
326
|
+
break
|
|
327
|
+
if not name or pos >= len(clean) or clean[pos] != "(":
|
|
328
|
+
continue
|
|
329
|
+
try:
|
|
330
|
+
p_end = _match_close(clean, pos, "(", ")")
|
|
331
|
+
except ValueError:
|
|
332
|
+
continue
|
|
333
|
+
after = re.match(r"\s*", clean[p_end:])
|
|
334
|
+
b_start = p_end + after.end()
|
|
335
|
+
if b_start >= len(clean) or clean[b_start] != "{":
|
|
336
|
+
continue # declaration only
|
|
337
|
+
b_end = _match_close(clean, b_start, "{", "}")
|
|
338
|
+
|
|
339
|
+
stmt_start = _statement_start(clean, gm.start())
|
|
340
|
+
prefix = clean[stmt_start:gm.start()]
|
|
341
|
+
template_params: list[str] = []
|
|
342
|
+
tm = re.search(r"template\s*<", prefix)
|
|
343
|
+
if tm:
|
|
344
|
+
t_open = stmt_start + tm.end() - 1
|
|
345
|
+
t_close = _match_close(clean, t_open, "<", ">")
|
|
346
|
+
template_params = split_top_level(clean[t_open + 1:t_close - 1])
|
|
347
|
+
extern_c = re.search(r'extern\s*"C"', text[stmt_start:gm.start()]) is not None
|
|
348
|
+
src_start = stmt_start + (len(prefix) - len(prefix.lstrip()))
|
|
349
|
+
|
|
350
|
+
ttypes = _template_type_names(template_params)
|
|
351
|
+
params = []
|
|
352
|
+
for raw in split_top_level(clean[pos + 1:p_end - 1]):
|
|
353
|
+
p = parse_param(raw, ttypes)
|
|
354
|
+
if p is not None:
|
|
355
|
+
params.append(p)
|
|
356
|
+
body = clean[b_start:b_end]
|
|
357
|
+
for p in params:
|
|
358
|
+
if p.is_pointer and p.name:
|
|
359
|
+
p.read, p.written = _usage(body, p.name)
|
|
360
|
+
|
|
361
|
+
kernels.append(KernelInfo(
|
|
362
|
+
id=f"{rel_path}::{name}",
|
|
363
|
+
name=name,
|
|
364
|
+
file=rel_path,
|
|
365
|
+
line=_line_of(text, gm.start()) + line_offset,
|
|
366
|
+
end_line=_line_of(text, b_end - 1) + line_offset,
|
|
367
|
+
params=params,
|
|
368
|
+
template_params=template_params,
|
|
369
|
+
launch_bounds=launch_bounds,
|
|
370
|
+
extern_c=extern_c,
|
|
371
|
+
features=_features(clean[pos:p_end] + "\n" + body, device_funcs, template_params),
|
|
372
|
+
source=text[src_start:b_end],
|
|
373
|
+
))
|
|
374
|
+
# disambiguate repeated names in one file
|
|
375
|
+
counts: dict[str, int] = {}
|
|
376
|
+
for k in kernels:
|
|
377
|
+
counts[k.name] = counts.get(k.name, 0) + 1
|
|
378
|
+
for k in kernels:
|
|
379
|
+
if counts[k.name] > 1:
|
|
380
|
+
k.id = f"{k.id}@{k.line}"
|
|
381
|
+
return kernels
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
_LAUNCH_RE = re.compile(
|
|
385
|
+
r"\b([A-Za-z_][\w:]*)\s*(?:<((?:[^<>;]|<[^<>;]*>)*)>)?\s*<<<"
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def find_launches(text: str, rel_path: str, line_offset: int = 0) -> list[tuple[str, LaunchConfig]]:
|
|
390
|
+
"""All ``name<<<...>>>(...)`` launch sites as (kernel name, LaunchConfig)."""
|
|
391
|
+
clean = strip_comments_and_strings(text)
|
|
392
|
+
out = []
|
|
393
|
+
for m in _LAUNCH_RE.finditer(clean):
|
|
394
|
+
cfg_start = m.end()
|
|
395
|
+
cfg_end = clean.find(">>>", cfg_start)
|
|
396
|
+
if cfg_end < 0:
|
|
397
|
+
continue
|
|
398
|
+
cfg = split_top_level(clean[cfg_start:cfg_end])
|
|
399
|
+
rest = re.match(r"\s*\(", clean[cfg_end + 3:])
|
|
400
|
+
args: list[str] = []
|
|
401
|
+
if rest:
|
|
402
|
+
a_open = cfg_end + 3 + rest.end() - 1
|
|
403
|
+
try:
|
|
404
|
+
a_close = _match_close(clean, a_open, "(", ")")
|
|
405
|
+
args = [" ".join(a.split()) for a in split_top_level(text[a_open + 1:a_close - 1])]
|
|
406
|
+
except ValueError:
|
|
407
|
+
pass
|
|
408
|
+
cfg_src = split_top_level(text[cfg_start:cfg_end])
|
|
409
|
+
if len(cfg_src) != len(cfg):
|
|
410
|
+
cfg_src = cfg
|
|
411
|
+
cfg_src = [" ".join(c.split()) for c in cfg_src]
|
|
412
|
+
name = m.group(1).split("::")[-1]
|
|
413
|
+
out.append((name, LaunchConfig(
|
|
414
|
+
file=rel_path, line=_line_of(text, m.start()) + line_offset,
|
|
415
|
+
grid=cfg_src[0] if len(cfg_src) > 0 else "",
|
|
416
|
+
block=cfg_src[1] if len(cfg_src) > 1 else "",
|
|
417
|
+
shared_mem=cfg_src[2] if len(cfg_src) > 2 else "0",
|
|
418
|
+
stream=cfg_src[3] if len(cfg_src) > 3 else "",
|
|
419
|
+
template_args=" ".join((m.group(2) or "").split()),
|
|
420
|
+
args=args,
|
|
421
|
+
)))
|
|
422
|
+
return out
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _python_cuda_strings(text: str) -> list[tuple[str, int]]:
|
|
426
|
+
"""(source, line offset) for string literals in Python code that contain __global__."""
|
|
427
|
+
try:
|
|
428
|
+
tree = ast.parse(text)
|
|
429
|
+
except SyntaxError:
|
|
430
|
+
return []
|
|
431
|
+
found = []
|
|
432
|
+
for node in ast.walk(tree):
|
|
433
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, str) and "__global__" in node.value:
|
|
434
|
+
# string starts on node.lineno; content usually begins on that line or the next
|
|
435
|
+
found.append((node.value, node.lineno - 1))
|
|
436
|
+
return found
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# ------------------------------------------------------------------ scan
|
|
440
|
+
|
|
441
|
+
def _iter_files(root: Path):
|
|
442
|
+
if root.is_file():
|
|
443
|
+
yield root
|
|
444
|
+
return
|
|
445
|
+
for p in sorted(root.rglob("*")):
|
|
446
|
+
if any(part in SKIP_DIRS for part in p.relative_to(root).parts[:-1]):
|
|
447
|
+
continue
|
|
448
|
+
if p.is_file() and (p.suffix in CUDA_SUFFIXES or p.suffix in MAYBE_CUDA_SUFFIXES or p.suffix == ".py"):
|
|
449
|
+
yield p
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def scan(path: Path) -> list[KernelInfo]:
|
|
453
|
+
"""Find and parse every kernel under ``path`` (a file or directory)."""
|
|
454
|
+
root = Path(path).resolve()
|
|
455
|
+
base = root.parent if root.is_file() else root
|
|
456
|
+
kernels: list[KernelInfo] = []
|
|
457
|
+
launches: list[tuple[str, LaunchConfig]] = []
|
|
458
|
+
for f in _iter_files(root):
|
|
459
|
+
try:
|
|
460
|
+
if f.stat().st_size > MAX_FILE_BYTES:
|
|
461
|
+
continue
|
|
462
|
+
text = f.read_text(encoding="utf-8", errors="replace")
|
|
463
|
+
except OSError:
|
|
464
|
+
continue
|
|
465
|
+
if "__global__" not in text and "<<<" not in text:
|
|
466
|
+
continue
|
|
467
|
+
rel = f.relative_to(base).as_posix()
|
|
468
|
+
if f.suffix == ".py":
|
|
469
|
+
for src, off in _python_cuda_strings(text):
|
|
470
|
+
kernels.extend(parse_source(src, rel, off))
|
|
471
|
+
continue
|
|
472
|
+
kernels.extend(parse_source(text, rel))
|
|
473
|
+
launches.extend(find_launches(text, rel))
|
|
474
|
+
by_name: dict[str, list[KernelInfo]] = {}
|
|
475
|
+
for k in kernels:
|
|
476
|
+
by_name.setdefault(k.name, []).append(k)
|
|
477
|
+
for name, cfg in launches:
|
|
478
|
+
for k in by_name.get(name, []):
|
|
479
|
+
k.launches.append(cfg)
|
|
480
|
+
return kernels
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Data models produced by the CUDA analyzer. No torch, no GPU imports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class Param:
|
|
10
|
+
"""One kernel parameter as written in the source."""
|
|
11
|
+
|
|
12
|
+
name: str
|
|
13
|
+
type: str # full type text, e.g. "const T* __restrict__"
|
|
14
|
+
base_type: str # type without qualifiers/pointer, e.g. "T"
|
|
15
|
+
is_pointer: bool
|
|
16
|
+
is_const: bool # pointer to const (read-only data) or const scalar
|
|
17
|
+
dtype: str | None = None # kernelport dtype guess ("float32", "T", ...) or None if unknown
|
|
18
|
+
written: bool = False # pointer param is assigned through in the body
|
|
19
|
+
read: bool = False # pointer param is read in the body
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def kind(self) -> str:
|
|
23
|
+
"""Suggested launch-spec kind: input, output, inout, or scalar."""
|
|
24
|
+
if not self.is_pointer:
|
|
25
|
+
return "scalar"
|
|
26
|
+
if self.is_const or not self.written:
|
|
27
|
+
return "input"
|
|
28
|
+
return "inout" if self.read else "output"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class LaunchConfig:
|
|
33
|
+
"""A launch site ``kernel<<<grid, block, shmem, stream>>>(args)`` found in the repo."""
|
|
34
|
+
|
|
35
|
+
file: str
|
|
36
|
+
line: int
|
|
37
|
+
grid: str
|
|
38
|
+
block: str
|
|
39
|
+
shared_mem: str = "0"
|
|
40
|
+
stream: str = ""
|
|
41
|
+
template_args: str = ""
|
|
42
|
+
args: list[str] = field(default_factory=list)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class KernelInfo:
|
|
47
|
+
"""A ``__global__`` kernel definition."""
|
|
48
|
+
|
|
49
|
+
id: str # "<relative path>::<name>", with "@<line>" appended if the name repeats in a file
|
|
50
|
+
name: str
|
|
51
|
+
file: str # path relative to the scan root, forward slashes
|
|
52
|
+
line: int # 1-based line of the __global__ keyword
|
|
53
|
+
end_line: int
|
|
54
|
+
params: list[Param]
|
|
55
|
+
template_params: list[str] = field(default_factory=list) # e.g. ["typename T", "int BLOCK = 256"]
|
|
56
|
+
launch_bounds: str = ""
|
|
57
|
+
extern_c: bool = False
|
|
58
|
+
features: list[str] = field(default_factory=list) # e.g. ["shared_memory", "atomics"]
|
|
59
|
+
source: str = "" # full kernel text including template line
|
|
60
|
+
launches: list[LaunchConfig] = field(default_factory=list)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def is_template(self) -> bool:
|
|
64
|
+
return bool(self.template_params)
|
|
65
|
+
|
|
66
|
+
def to_dict(self, include_source: bool = True) -> dict:
|
|
67
|
+
"""JSON-serialisable dict (optionally without the source text)."""
|
|
68
|
+
d = asdict(self)
|
|
69
|
+
for p, pd in zip(self.params, d["params"]):
|
|
70
|
+
pd["kind"] = p.kind
|
|
71
|
+
if not include_source:
|
|
72
|
+
d.pop("source")
|
|
73
|
+
return d
|
|
74
|
+
|
|
75
|
+
def summary(self) -> str:
|
|
76
|
+
"""One line: id, signature, features."""
|
|
77
|
+
sig = ", ".join(f"{p.type} {p.name}".strip() for p in self.params)
|
|
78
|
+
tpl = f"template<{', '.join(self.template_params)}> " if self.template_params else ""
|
|
79
|
+
feats = f" [{', '.join(self.features)}]" if self.features else ""
|
|
80
|
+
return f"{self.id} (line {self.line}): {tpl}{self.name}({sig}){feats}"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Draft a launch.json for a parsed kernel.
|
|
2
|
+
|
|
3
|
+
The draft is a starting point for the agent: kinds and dtypes come from the
|
|
4
|
+
signature and body, but tensor shapes, case sizes and the grid usually need a
|
|
5
|
+
human or agent to fill in. Unknowns are marked with "TODO" so validation fails
|
|
6
|
+
loudly until they are resolved.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import keyword
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
from kernelport.analyzer.models import KernelInfo
|
|
15
|
+
|
|
16
|
+
TODO = "TODO"
|
|
17
|
+
STANDARD_1D_SIZES = [("tiny_1", 1), ("tiny_7", 7), ("odd_1000", 1000), ("odd_1023", 1023),
|
|
18
|
+
("odd_4097", 4097), ("pow2_65536", 65536), ("large_16M", 16_777_216)]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def safe_name(name: str) -> str:
|
|
22
|
+
"""Python-safe parameter name (``in`` -> ``in_``)."""
|
|
23
|
+
name = re.sub(r"\W", "_", name) or "arg"
|
|
24
|
+
return name + "_" if keyword.iskeyword(name) or name[0].isdigit() else name
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _dtypes_for(kernel: KernelInfo) -> list[str]:
|
|
28
|
+
if any(p.dtype == "T" for p in kernel.params):
|
|
29
|
+
return ["float32", "float16"]
|
|
30
|
+
return ["float32"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _entry_for(kernel: KernelInfo) -> str:
|
|
34
|
+
if not kernel.template_params:
|
|
35
|
+
return kernel.name
|
|
36
|
+
# Only the first type parameter maps to {T}; other template args need filling in.
|
|
37
|
+
args = []
|
|
38
|
+
first_type_done = False
|
|
39
|
+
for tp in kernel.template_params:
|
|
40
|
+
if re.match(r"(?:typename|class)\s", tp) and not first_type_done:
|
|
41
|
+
args.append("{T}")
|
|
42
|
+
first_type_done = True
|
|
43
|
+
else:
|
|
44
|
+
default = tp.split("=", 1)[1].strip() if "=" in tp else TODO
|
|
45
|
+
args.append(default)
|
|
46
|
+
return f"{kernel.name}<{', '.join(args)}>"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def suggest_spec(kernel: KernelInfo) -> dict:
|
|
50
|
+
"""Return a draft launch.json dict for ``kernel``."""
|
|
51
|
+
scalar_names = [safe_name(p.name) for p in kernel.params if not p.is_pointer]
|
|
52
|
+
one_dim = len(scalar_names) >= 1 and sum(p.is_pointer for p in kernel.params) >= 1
|
|
53
|
+
size_dim = next((n for n in scalar_names if n.lower() in ("n", "size", "numel", "num", "len", "length")), None)
|
|
54
|
+
|
|
55
|
+
params = []
|
|
56
|
+
for p in kernel.params:
|
|
57
|
+
name = safe_name(p.name)
|
|
58
|
+
entry: dict = {"name": name, "kind": p.kind, "dtype": p.dtype or TODO}
|
|
59
|
+
if p.is_pointer:
|
|
60
|
+
entry["shape"] = [size_dim] if size_dim else [TODO]
|
|
61
|
+
else:
|
|
62
|
+
entry["value"] = name # scalars come from case dims of the same name
|
|
63
|
+
if name != p.name:
|
|
64
|
+
entry["cuda_name"] = p.name
|
|
65
|
+
params.append(entry)
|
|
66
|
+
|
|
67
|
+
launch = {"grid": [TODO], "block": [TODO], "shared_mem": 0}
|
|
68
|
+
notes = []
|
|
69
|
+
if kernel.launches:
|
|
70
|
+
l0 = kernel.launches[0]
|
|
71
|
+
notes.append(f"launch site {l0.file}:{l0.line}: <<<{l0.grid}, {l0.block}"
|
|
72
|
+
+ (f", {l0.shared_mem}" if l0.shared_mem not in ("", "0") else "") + ">>>"
|
|
73
|
+
+ (f" args ({', '.join(l0.args)})" if l0.args else ""))
|
|
74
|
+
if re.fullmatch(r"\d+", l0.block or ""):
|
|
75
|
+
launch["block"] = [int(l0.block)]
|
|
76
|
+
if "dynamic_shared_memory" in kernel.features:
|
|
77
|
+
launch["shared_mem"] = TODO
|
|
78
|
+
notes.append("kernel uses extern __shared__: set shared_mem bytes")
|
|
79
|
+
|
|
80
|
+
if size_dim and one_dim:
|
|
81
|
+
cases = [{"name": n, "dims": {size_dim: v}} for n, v in STANDARD_1D_SIZES]
|
|
82
|
+
else:
|
|
83
|
+
cases = [{"name": "tiny", "dims": {d: TODO for d in scalar_names}},
|
|
84
|
+
{"name": "odd", "dims": {d: TODO for d in scalar_names}},
|
|
85
|
+
{"name": "pow2", "dims": {d: TODO for d in scalar_names}},
|
|
86
|
+
{"name": "large", "dims": {d: TODO for d in scalar_names}}]
|
|
87
|
+
init = {safe_name(p.name): {"dist": "normal", "edges": True}
|
|
88
|
+
for p in kernel.params if p.is_pointer and p.kind != "output"
|
|
89
|
+
and (p.dtype in ("T", "float32", "float16", "bfloat16", "float64"))}
|
|
90
|
+
for p in kernel.params:
|
|
91
|
+
if p.is_pointer and p.kind != "output" and p.dtype and p.dtype.startswith("int"):
|
|
92
|
+
init[safe_name(p.name)] = {"dist": "randint", "low": 0, "high": TODO}
|
|
93
|
+
notes.append(f"{p.name} looks like an index array: set its valid range")
|
|
94
|
+
if any(f in kernel.features for f in ("atomics",)) or any(
|
|
95
|
+
k in kernel.name.lower() for k in ("sum", "reduce", "matmul", "gemm", "scan", "cumsum", "dot")):
|
|
96
|
+
notes.append("accumulating kernel: consider \"step\": 0.0625 in init so float sums are exact in any order")
|
|
97
|
+
if any(p.dtype == "T" for p in kernel.params):
|
|
98
|
+
notes.append("templated on T: add \"bfloat16\" to dtypes if the source includes cuda_bf16.h "
|
|
99
|
+
"and T may be __nv_bfloat16")
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
"name": kernel.name,
|
|
103
|
+
"source": kernel.file,
|
|
104
|
+
"entry": _entry_for(kernel),
|
|
105
|
+
"dtypes": _dtypes_for(kernel),
|
|
106
|
+
"params": params,
|
|
107
|
+
"launch": launch,
|
|
108
|
+
"cases": cases,
|
|
109
|
+
"init": init,
|
|
110
|
+
"_notes": notes,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def has_todo(obj) -> bool:
|
|
115
|
+
"""True if any value in a (nested) spec dict is still TODO."""
|
|
116
|
+
if isinstance(obj, dict):
|
|
117
|
+
return any(has_todo(v) for v in obj.values())
|
|
118
|
+
if isinstance(obj, list):
|
|
119
|
+
return any(has_todo(v) for v in obj)
|
|
120
|
+
return obj == TODO
|