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,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
@@ -0,0 +1,5 @@
1
+ """Backends: each subpackage ports CUDA kernels to one target."""
2
+
3
+ from kernelport.backends.base import Backend, BackendConfig, Prepared, get_backend, registered_backends
4
+
5
+ __all__ = ["Backend", "BackendConfig", "Prepared", "get_backend", "registered_backends"]