trainkernel 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.
trainkernel/codegen.py ADDED
@@ -0,0 +1,870 @@
1
+ """Static CUDA code generation for a cooperative persistent training kernel.
2
+
3
+ The initial scheduler is deliberately conservative: operators execute in topological
4
+ order with a grid barrier. Single-use pointwise values are evaluated inline.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from collections import Counter
9
+ from dataclasses import dataclass
10
+ import math
11
+ import re
12
+
13
+ from .ir import Operation, Program, TensorSpec, validate
14
+
15
+ _DTYPES = {"float32": "float", "float16": "__half", "bfloat16": "__nv_bfloat16"}
16
+ _POINTWISE = {"add", "sub", "mul", "div", "neg", "relu", "pow", "threshold_backward", "full", "cast"}
17
+
18
+
19
+ def numel(shape: tuple[int, ...]) -> int:
20
+ return math.prod(shape)
21
+
22
+
23
+ def contiguous_strides(shape: tuple[int, ...]) -> tuple[int, ...]:
24
+ return tuple(math.prod(shape[i + 1:]) for i in range(len(shape)))
25
+
26
+
27
+ def _literal(value: int | float) -> str:
28
+ value = float(value)
29
+ if not math.isfinite(value):
30
+ raise ValueError("trainkernel: non-finite scalar constants are unsupported")
31
+ return f"{value:.17e}f"
32
+
33
+
34
+ def _round(expr: str, dtype: str) -> str:
35
+ if dtype == "float32":
36
+ return f"({expr})"
37
+ if dtype == "bfloat16":
38
+ return f"__bfloat162float(__float2bfloat16_rn({expr}))"
39
+ if dtype == "float16":
40
+ return f"__half2float(__float2half_rn({expr}))"
41
+ raise ValueError(f"trainkernel: unsupported CUDA dtype {dtype}")
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class GeneratedKernel:
46
+ source: str
47
+ workspace_elements: int
48
+ materialized_operations: int
49
+ inlined_operations: int
50
+ tensor_core_operations: int
51
+ blocks_per_sm: int
52
+ max_work_blocks: int
53
+ grid_barriers: int
54
+ direct_outputs: int
55
+ direct_state_commits: int = 0
56
+
57
+
58
+ class _Generator:
59
+ def __init__(self, program: Program, *, blocks_per_sm: int, fuse_pointwise: bool,
60
+ max_blocks: int | None = None, fuse_epilogues: bool = True,
61
+ gemm_k_tile: int = 32, gemm_tile_n: int = 32, shared_skew: int = 0, split_k_gemv: bool = False, threads_per_block: int = 128, inline_input_casts: bool = True, inplace_outputs: dict[int, int] | None = None):
62
+ self.program = program
63
+ self.blocks_per_sm = blocks_per_sm
64
+ self.max_blocks = max_blocks
65
+ self.gemm_k_tile = gemm_k_tile
66
+ self.gemm_tile_n = gemm_tile_n
67
+ self.shared_skew = shared_skew
68
+ self.split_k_gemv = split_k_gemv
69
+ self.threads_per_block = threads_per_block
70
+ self.inplace_outputs = dict(inplace_outputs or {})
71
+ aliased_buffers = {program.outputs[i].buffer for i in self.inplace_outputs}
72
+ self.producers = {op.output.buffer: op for op in program.operations}
73
+ self.input_indices = {spec.buffer: i for i, spec in enumerate(program.inputs)}
74
+ counts: Counter[str] = Counter()
75
+ for op in program.operations:
76
+ counts.update(x.buffer for x in op.inputs if isinstance(x, TensorSpec))
77
+ output_buffers = {x.buffer for x in program.outputs}
78
+ self.epilogues: dict[str, tuple[tuple[Operation, ...], str, Operation]] = {}
79
+ self.side_epilogues: dict[str, tuple[Operation, ...]] = {}
80
+ side_outputs: dict[str, TensorSpec] = {}
81
+ replacements: dict[str, Operation] = {}
82
+ fused_skip: set[str] = set()
83
+ if fuse_epilogues:
84
+ users: dict[str, list[Operation]] = {}
85
+ for candidate in program.operations:
86
+ for value in candidate.inputs:
87
+ if isinstance(value, TensorSpec):
88
+ users.setdefault(value.buffer, []).append(candidate)
89
+ op_index = {op.output.buffer: i for i, op in enumerate(program.operations)}
90
+ for mm_index, mm in enumerate(program.operations):
91
+ if mm.op not in {"mm", "addmm"} or mm.output.buffer in output_buffers:
92
+ continue
93
+ chain: list[Operation] = []
94
+ current = mm.output.buffer
95
+ while len(users.get(current, [])) == 1:
96
+ nxt = users[current][0]
97
+ if nxt.op not in _POINTWISE or nxt.output.buffer in output_buffers:
98
+ break
99
+ # The substituted GEMM root must be consumed with the
100
+ # same logical layout. A square transpose/view can have
101
+ # the same shape, so shape-only checks are insufficient.
102
+ current_spec = self.producers.get(current, None)
103
+ if current_spec is None:
104
+ break
105
+ matching = [value for value in nxt.inputs
106
+ if isinstance(value, TensorSpec) and value.buffer == current]
107
+ if not matching or any(value.shape != current_spec.output.shape
108
+ or value.strides != contiguous_strides(value.shape)
109
+ or value.offset != 0 for value in matching):
110
+ break
111
+ chain.append(nxt)
112
+ fused_skip.add(current)
113
+ current = nxt.output.buffer
114
+ if len(users.get(current, [])) != 1:
115
+ break
116
+ if chain:
117
+ final = chain[-1]
118
+ # Fusion is safe only for direct, contiguous tensor roots
119
+ # and shape-preserving pointwise chains. External values
120
+ # must already exist when the replacement executes.
121
+ safe = all(op.output.shape == mm.output.shape and op.output.offset == 0
122
+ and op.output.strides == contiguous_strides(op.output.shape)
123
+ for op in chain)
124
+ external: list[TensorSpec] = []
125
+ chain_buffers = {mm.output.buffer} | {op.output.buffer for op in chain}
126
+ for cop in chain:
127
+ for value in cop.inputs:
128
+ if isinstance(value, TensorSpec) and value.buffer not in chain_buffers:
129
+ if value.offset != 0 or value.strides != contiguous_strides(value.shape):
130
+ safe = False
131
+ producer_index = op_index.get(value.buffer)
132
+ if producer_index is not None and producer_index >= mm_index:
133
+ safe = False
134
+ if value.buffer not in {x.buffer for x in external}:
135
+ external.append(value)
136
+ if not safe:
137
+ fused_skip.difference_update({mm.output.buffer, *[op.output.buffer for op in chain]})
138
+ continue
139
+ fused_inputs = tuple(mm.inputs) + tuple(external)
140
+ fused = Operation(mm.name + "_epilogue", mm.op, fused_inputs, final.output, mm.attrs)
141
+ self.epilogues[fused.output.buffer] = (tuple(chain), mm.output.buffer, mm)
142
+ replacements[mm.output.buffer] = fused
143
+ fused_skip.add(final.output.buffer)
144
+ self.producers[fused.output.buffer] = fused
145
+ # A second, conservative case keeps the GEMM root and writes
146
+ # returned pointwise descendants from the same output tile. Only
147
+ # externally visible descendants have storage outside this tile.
148
+ for mm_index, mm in enumerate(program.operations):
149
+ if mm.op not in {"mm", "addmm"} or mm.output.buffer in replacements or mm.output.buffer in fused_skip:
150
+ continue
151
+ known = {mm.output.buffer}
152
+ chain = []
153
+ external: dict[str, TensorSpec] = {}
154
+ for nxt in program.operations[mm_index + 1:]:
155
+ tensors = [x for x in nxt.inputs if isinstance(x, TensorSpec)]
156
+ if nxt.op not in _POINTWISE or not any(x.buffer in known for x in tensors):
157
+ continue
158
+ if nxt.output.shape != mm.output.shape or nxt.output.buffer in fused_skip or nxt.output.buffer in aliased_buffers or nxt.output.offset or nxt.output.strides != contiguous_strides(nxt.output.shape):
159
+ continue
160
+ if any(x.buffer in known and (x.shape != mm.output.shape or x.strides != contiguous_strides(x.shape) or x.offset) for x in tensors):
161
+ continue
162
+ others = [x for x in tensors if x.buffer not in known]
163
+ if any(op_index.get(x.buffer, -1) >= mm_index for x in others):
164
+ continue
165
+ chain.append(nxt)
166
+ known.add(nxt.output.buffer)
167
+ external.update((x.buffer, x) for x in others)
168
+ derived = known - {mm.output.buffer}
169
+ returned = derived & output_buffers
170
+ # Discard branches that do not lead to a stored return value.
171
+ needed = set(returned)
172
+ for candidate in reversed(chain):
173
+ if candidate.output.buffer in needed:
174
+ needed.update(x.buffer for x in candidate.inputs if isinstance(x, TensorSpec) and x.buffer in derived)
175
+ chain = [op for op in chain if op.output.buffer in needed]
176
+ derived &= needed
177
+ if not returned:
178
+ continue
179
+ if any(user.output.buffer not in derived for buf in derived - returned for user in users.get(buf, [])):
180
+ continue
181
+ if any(out.offset or out.shape != self.producers[out.buffer].output.shape or out.strides != contiguous_strides(out.shape) for out in program.outputs if out.buffer in returned):
182
+ continue
183
+ replacement = Operation(mm.name, mm.op, mm.inputs + tuple(external.values()), mm.output, mm.attrs)
184
+ replacements[mm.output.buffer] = replacement
185
+ self.producers[mm.output.buffer] = replacement
186
+ self.side_epilogues[mm.output.buffer] = tuple(chain)
187
+ fused_skip.update(derived)
188
+ side_outputs.update((op.output.buffer, op.output) for op in chain if op.output.buffer in returned)
189
+ self.fused_replacements = replacements
190
+ # Do not duplicate a pointwise expression across multiple consumers or outputs.
191
+ self.inline = {
192
+ op.output.buffer for op in program.operations
193
+ if fuse_pointwise and op.op in _POINTWISE
194
+ and (counts[op.output.buffer] == 1 or (inline_input_casts and op.op == "cast" and isinstance(op.inputs[0], TensorSpec) and op.inputs[0].buffer in self.input_indices)) and op.output.buffer not in output_buffers
195
+ and op.output.buffer not in self.epilogues
196
+ }
197
+ # Direct state writes are allowed only for a final elementwise use of
198
+ # an input. Other mappings retain the staged transactional commit.
199
+ schedule = []
200
+ for candidate in program.operations:
201
+ if candidate.output.buffer in replacements:
202
+ schedule.append(replacements[candidate.output.buffer])
203
+ elif candidate.output.buffer not in fused_skip and candidate.output.buffer not in self.inline:
204
+ schedule.append(candidate)
205
+ def raw_inputs(spec: TensorSpec) -> set[str]:
206
+ if spec.buffer in self.input_indices:
207
+ return {spec.buffer}
208
+ if spec.buffer in self.inline:
209
+ return set().union(*(raw_inputs(v) for v in self.producers[spec.buffer].inputs if isinstance(v, TensorSpec)))
210
+ return set()
211
+ def identity_access(spec: TensorSpec, target: str, shape: tuple[int, ...]) -> bool:
212
+ if target not in raw_inputs(spec):
213
+ return True
214
+ if spec.shape != shape or spec.offset or spec.strides != contiguous_strides(shape):
215
+ return False
216
+ if spec.buffer == target:
217
+ return True
218
+ return all(identity_access(v, target, shape) for v in self.producers[spec.buffer].inputs if isinstance(v, TensorSpec))
219
+ self.direct_aliases: dict[int, int] = {}
220
+ output_counts = Counter(x.buffer for x in program.outputs)
221
+ for slot, input_index in self.inplace_outputs.items():
222
+ output = program.outputs[slot]
223
+ target = program.inputs[input_index].buffer
224
+ owner = next((i for i, op in enumerate(schedule) if op.output.buffer == output.buffer), None)
225
+ if owner is None or schedule[owner].op not in _POINTWISE or output_counts[output.buffer] != 1:
226
+ continue
227
+ if output.offset or output.strides != contiguous_strides(output.shape) or any(out.buffer == target for out in program.outputs):
228
+ continue
229
+ if any(target in raw_inputs(v) for op in schedule[owner+1:] for v in op.inputs if isinstance(v, TensorSpec)):
230
+ continue
231
+ if not all(identity_access(v, target, output.shape) for v in schedule[owner].inputs if isinstance(v, TensorSpec)):
232
+ continue
233
+ self.direct_aliases[slot] = input_index
234
+ self.input_regions: dict[str, tuple[int, int]] = {}
235
+ cursor = -1
236
+ for spec in program.inputs:
237
+ size = max(1, numel(spec.shape))
238
+ self.input_regions[spec.buffer] = (cursor - size, cursor)
239
+ cursor -= size + 1
240
+ self.alias_output_regions = {program.outputs[slot].buffer: self.input_regions[program.inputs[index].buffer] for slot, index in self.direct_aliases.items()}
241
+ self.output_bindings: dict[str, tuple[int, str]] = {}
242
+ for index, spec in enumerate(program.outputs):
243
+ if index in self.inplace_outputs and index not in self.direct_aliases:
244
+ continue
245
+ producer = self.producers.get(spec.buffer)
246
+ if producer is not None and producer.output.dtype == spec.dtype and spec.offset == 0 and numel(spec.shape) == numel(producer.output.shape) and all(dim == 1 or a == b for dim, a, b in zip(spec.shape, spec.strides, contiguous_strides(spec.shape))):
247
+ self.output_bindings.setdefault(spec.buffer, (index, spec.dtype))
248
+ if not side_outputs.keys() <= self.output_bindings.keys():
249
+ raise ValueError("trainkernel: side epilogues require direct output bindings")
250
+ self.native_regions: dict[str, tuple[int, int]] = {}
251
+ for buffer in self.output_bindings:
252
+ if buffer in self.alias_output_regions:
253
+ self.native_regions[buffer] = self.alias_output_regions[buffer]
254
+ else:
255
+ size = max(1, numel(self.producers[buffer].output.shape))
256
+ self.native_regions[buffer] = (cursor - size, cursor)
257
+ cursor -= size + 1
258
+ self.discarded_mm_roots = {
259
+ root for root, chain in self.side_epilogues.items()
260
+ if root not in output_buffers and all(user.output.buffer in {op.output.buffer for op in chain}
261
+ for user in program.operations if any(isinstance(x, TensorSpec) and x.buffer == root for x in user.inputs))
262
+ }
263
+ self.discarded_regions: dict[str, tuple[int, int]] = {}
264
+ for root in sorted(self.discarded_mm_roots):
265
+ size = max(1, numel(self.producers[root].output.shape))
266
+ self.discarded_regions[root] = (cursor - size, cursor)
267
+ cursor -= size + 1
268
+ self.materialized = []
269
+ for op in program.operations:
270
+ if op.output.buffer in self.fused_replacements:
271
+ self.materialized.append(self.fused_replacements[op.output.buffer])
272
+ elif op.output.buffer in fused_skip:
273
+ continue
274
+ elif op.output.buffer not in self.inline:
275
+ self.materialized.append(op)
276
+ self.offsets, self.workspace_elements = self._plan_workspace()
277
+ # Native allocations use disjoint virtual regions for dependencies and
278
+ # never consume arena space. Declared aliases share their input region.
279
+ for buffer, spec in side_outputs.items():
280
+ self.offsets[buffer] = self.native_regions[buffer][0]
281
+ self.reduction_scratch = self.workspace_elements
282
+ if any(op.op in {"sum", "mean"} and isinstance(op.inputs[0], TensorSpec) and numel(op.inputs[0].shape) > 4096 and numel(op.output.shape) <= 4 for op in self.materialized):
283
+ self.workspace_elements += 1024
284
+ self.internal_barriers = 0
285
+ self.tensor_core_count = 0
286
+
287
+ def _plan_workspace(self) -> tuple[dict[str, int], int]:
288
+ # A small best-fit arena reuses storage after the last materialized reader.
289
+ def roots(spec: TensorSpec) -> set[str]:
290
+ if spec.buffer not in self.inline:
291
+ return {spec.buffer}
292
+ return set().union(*(roots(x) for x in self.producers[spec.buffer].inputs if isinstance(x, TensorSpec)))
293
+ last: dict[str, int] = {}
294
+ for i, op in enumerate(self.materialized):
295
+ for x in op.inputs:
296
+ if isinstance(x, TensorSpec):
297
+ for root in roots(x):
298
+ last[root] = i
299
+ for spec in self.program.outputs:
300
+ for root in roots(spec):
301
+ last[root] = len(self.materialized)
302
+ offsets: dict[str, int] = {}
303
+ active: list[tuple[str, int, int]] = []
304
+ free: list[tuple[int, int]] = []
305
+ total = 0
306
+ for i, op in enumerate(self.materialized):
307
+ if op.output.buffer in self.native_regions:
308
+ offsets[op.output.buffer] = self.native_regions[op.output.buffer][0]
309
+ continue
310
+ if op.output.buffer in self.discarded_regions:
311
+ offsets[op.output.buffer] = self.discarded_regions[op.output.buffer][0]
312
+ continue
313
+ remaining = []
314
+ for root, start, size in active:
315
+ if last.get(root, -1) < i:
316
+ free.append((start, size))
317
+ else:
318
+ remaining.append((root, start, size))
319
+ active = remaining
320
+ size = max(64, ((numel(op.output.shape) + 63) // 64) * 64)
321
+ candidates = [(n, j, start) for j, (start, n) in enumerate(free) if n >= size]
322
+ if candidates:
323
+ n, j, start = min(candidates)
324
+ free.pop(j)
325
+ if n > size:
326
+ free.append((start + size, n - size))
327
+ else:
328
+ start = total
329
+ total += size
330
+ offsets[op.output.buffer] = start
331
+ active.append((op.output.buffer, start, size))
332
+ return offsets, max(total, 1)
333
+
334
+ @staticmethod
335
+ def coords(shape: tuple[int, ...], index: str) -> tuple[str, ...]:
336
+ return tuple(f"((({index}) / {max(1, math.prod(shape[i + 1:]))}LL) % {max(1, dim)}LL)" for i, dim in enumerate(shape))
337
+
338
+ def read(self, spec: TensorSpec, coords: tuple[str, ...]) -> str:
339
+ index = " + ".join([f"{spec.offset}LL"] + [f"(({c}) * {s}LL)" for c, s in zip(coords, spec.strides) if s])
340
+ if spec.buffer in self.inline:
341
+ return _round(self.point_expr(self.producers[spec.buffer], index), spec.dtype)
342
+ if spec.buffer in self.input_indices:
343
+ i = self.input_indices[spec.buffer]
344
+ raw = f"static_cast<const {_DTYPES[spec.dtype]}*>(args.inputs[{i}])[{index}]"
345
+ if spec.dtype == "bfloat16":
346
+ return f"__bfloat162float({raw})"
347
+ if spec.dtype == "float16":
348
+ return f"__half2float({raw})"
349
+ return raw
350
+ if spec.buffer in self.output_bindings:
351
+ slot, dtype = self.output_bindings[spec.buffer]
352
+ raw = f"static_cast<const {_DTYPES[dtype]}*>(args.outputs[{slot}])[{index}]"
353
+ return f"__bfloat162float({raw})" if dtype == "bfloat16" else f"__half2float({raw})" if dtype == "float16" else raw
354
+ return f"workspace[{self.offsets[spec.buffer]}LL + ({index})]"
355
+
356
+ def store(self, spec: TensorSpec, index: str, value: str) -> str:
357
+ if spec.buffer in self.output_bindings:
358
+ slot, dtype = self.output_bindings[spec.buffer]
359
+ cast = f"__float2bfloat16_rn({value})" if dtype == "bfloat16" else f"__float2half_rn({value})" if dtype == "float16" else value
360
+ return f"static_cast<{_DTYPES[dtype]}*>(args.outputs[{slot}])[{index}] = {cast};"
361
+ return f"workspace[{self.offsets[spec.buffer]}LL + ({index})] = {_round(value, spec.dtype)};"
362
+
363
+ def operand(self, operand: TensorSpec | int | float, output: TensorSpec, index: str) -> str:
364
+ if not isinstance(operand, TensorSpec):
365
+ return _literal(operand)
366
+ if operand.buffer in getattr(self, "_tile_values", {}):
367
+ return self._tile_values[operand.buffer]
368
+ if hasattr(self, "_epilogue_operand") and self._epilogue_operand is not None and operand.buffer == self._epilogue_operand[0]:
369
+ return self._epilogue_operand[1]
370
+ coords = self.coords(output.shape, index)
371
+ if len(operand.shape) > len(coords):
372
+ raise ValueError("trainkernel: invalid broadcast rank")
373
+ trailing = coords[len(coords) - len(operand.shape):] if operand.shape else ()
374
+ if any(a != 1 and a != b for a, b in zip(operand.shape, output.shape[-len(operand.shape):] if operand.shape else ())):
375
+ raise ValueError("trainkernel: incompatible broadcast dimensions")
376
+ return self.read(operand, tuple("0" if dim == 1 else c for dim, c in zip(operand.shape, trailing)))
377
+
378
+ def point_expr(self, op: Operation, index: str) -> str:
379
+ values = [self.operand(x, op.output, index) for x in op.inputs]
380
+ if op.op in {"add", "sub"}:
381
+ sign = "+" if op.op == "add" else "-"
382
+ return f"({values[0]} {sign} ({_literal(op.attrs.get('alpha', 1))} * {values[1]}))"
383
+ if op.op in {"mul", "div"}:
384
+ sign = "*" if op.op == "mul" else "/"
385
+ return f"({values[0]} {sign} {values[1]})"
386
+ if op.op == "neg":
387
+ return f"(-({values[0]}))"
388
+ if op.op == "relu":
389
+ return f"mk_relu({values[0]})"
390
+ if op.op == "pow":
391
+ exponent = op.inputs[1] if len(op.inputs) > 1 else op.attrs.get("exponent", 2)
392
+ if not isinstance(exponent, (float, int)):
393
+ raise ValueError("trainkernel: pow exponent must be a scalar")
394
+ if exponent == 2:
395
+ return f"mk_square({values[0]})"
396
+ return f"powf({values[0]}, {_literal(exponent)})"
397
+ if op.op == "threshold_backward":
398
+ return f"(({values[1]} <= {_literal(op.attrs.get('threshold', 0))}) ? 0.0f : {values[0]})"
399
+ if op.op == "full":
400
+ return _literal(op.attrs.get("value", op.inputs[0] if op.inputs else 0))
401
+ if op.op == "cast":
402
+ return values[0]
403
+ raise ValueError(f"trainkernel: unsupported pointwise operation {op.op}")
404
+
405
+ def epilogue_expr(self, buffer: str, value: str, index: str) -> str:
406
+ record = self.epilogues.get(buffer)
407
+ if record is None:
408
+ return value
409
+ chain, buffer, original = record
410
+ value = _round(value, original.output.dtype)
411
+ for op in chain:
412
+ self._epilogue_operand = (buffer, value)
413
+ value = _round(self.point_expr(op, index), op.output.dtype)
414
+ self._epilogue_operand = None
415
+ buffer = op.output.buffer
416
+ return value
417
+
418
+ def emit_pointwise(self, op: Operation) -> str:
419
+ expr = self.point_expr(op, "i")
420
+ return f"""for (int64_t i = tid; i < {numel(op.output.shape)}LL; i += nt) {{
421
+ {self.store(op.output, 'i', expr)}
422
+ }}"""
423
+
424
+ def store_mm(self, op: Operation, index: str, value: str) -> str:
425
+ value = self.epilogue_expr(op.output.buffer, value, index)
426
+ chain = self.side_epilogues.get(op.output.buffer, ())
427
+ if not chain:
428
+ return self.store(op.output, index, value)
429
+ lines = ["{", f"float tile_root = {_round(value, op.output.dtype)};"]
430
+ if op.output.buffer not in self.discarded_mm_roots:
431
+ lines.append(self.store(op.output, index, "tile_root"))
432
+ self._tile_values = {op.output.buffer: "tile_root"}
433
+ for i, descendant in enumerate(chain):
434
+ name = f"tile_value_{i}"
435
+ lines.append(f"float {name} = {_round(self.point_expr(descendant, index), descendant.output.dtype)};")
436
+ self._tile_values[descendant.output.buffer] = name
437
+ if descendant.output.buffer in self.output_bindings:
438
+ lines.append(self.store(descendant.output, index, name))
439
+ self._tile_values = {}
440
+ return "\n".join([*lines, "}"])
441
+
442
+ def emit_reduction(self, op: Operation) -> str:
443
+ source = op.inputs[0]
444
+ if not isinstance(source, TensorSpec):
445
+ raise ValueError("trainkernel: reduction requires a tensor")
446
+ dims = tuple(int(d) % len(source.shape) for d in op.attrs.get("dims", tuple(range(len(source.shape))))) if source.shape else ()
447
+ dims = tuple(sorted(set(dims)))
448
+ kept = [d for d in range(len(source.shape)) if d not in dims]
449
+ kept_shape = tuple(source.shape[d] for d in kept)
450
+ reduced_shape = tuple(source.shape[d] for d in dims)
451
+ out_coords = self.coords(kept_shape, "row")
452
+ red_coords = self.coords(reduced_shape, "r")
453
+ coordinates = ["0"] * len(source.shape)
454
+ for d, coord in zip(kept, out_coords):
455
+ coordinates[d] = coord
456
+ for d, coord in zip(dims, red_coords):
457
+ coordinates[d] = coord
458
+ value = self.read(source, tuple(coordinates))
459
+ size = numel(reduced_shape)
460
+ if numel(op.output.shape) != numel(kept_shape):
461
+ raise ValueError("trainkernel: reduction output shape does not match its axes")
462
+ if size > 4096 and numel(op.output.shape) <= 4:
463
+ parts = min(256, (size + 511) // 512)
464
+ rows = numel(op.output.shape)
465
+ finish = "acc" if op.op == "sum" else f"(acc / {size}.0f)"
466
+ self.internal_barriers += 2
467
+ # The fixed scratch region is reused only after both grid barriers.
468
+ # Each partial has its own slot, independent of cooperative grid size.
469
+ return f"""{{
470
+ for (int64_t part = blockIdx.x; part < {rows * parts}LL; part += gridDim.x) {{
471
+ const int64_t row = part / {parts};
472
+ const int64_t begin = (part % {parts}) * 512;
473
+ float acc = 0.0f;
474
+ for (int64_t r = begin + threadIdx.x; r < min(begin + 512LL, {size}LL); r += blockDim.x) acc += {value};
475
+ acc = mk_block_sum(acc, shared_reduce);
476
+ if (threadIdx.x == 0) workspace[{self.reduction_scratch}LL + part] = acc;
477
+ __syncthreads();
478
+ }}
479
+ mk_grid_sync(grid);
480
+ for (int64_t row = blockIdx.x; row < {rows}LL; row += gridDim.x) {{
481
+ float acc = 0.0f;
482
+ for (int r = threadIdx.x; r < {parts}; r += blockDim.x) acc += workspace[{self.reduction_scratch}LL + row * {parts} + r];
483
+ acc = mk_block_sum(acc, shared_reduce);
484
+ if (threadIdx.x == 0) {self.store(op.output, 'row', finish)}
485
+ __syncthreads();
486
+ }}
487
+ mk_grid_sync(grid);
488
+ }}"""
489
+ if size <= 128 and numel(op.output.shape) >= 8:
490
+ finish = "acc" if op.op == "sum" else f"(acc / {size}.0f)" if size else "NAN"
491
+ return f"""for (int64_t row = tid; row < {numel(op.output.shape)}LL; row += nt) {{
492
+ float acc = 0.0f;
493
+ for (int64_t r = 0; r < {size}LL; ++r) acc += {value};
494
+ {self.store(op.output, 'row', finish)}
495
+ }}"""
496
+ finish = "acc" if op.op == "sum" else f"(acc / {size}.0f)" if size else "NAN"
497
+ return f"""for (int64_t row = blockIdx.x; row < {numel(op.output.shape)}LL; row += gridDim.x) {{
498
+ float acc = 0.0f;
499
+ for (int64_t r = threadIdx.x; r < {size}LL; r += blockDim.x) acc += {value};
500
+ acc = mk_block_sum(acc, shared_reduce);
501
+ if (threadIdx.x == 0) {self.store(op.output, 'row', finish)}
502
+ __syncthreads();
503
+ }}"""
504
+
505
+ def emit_mm(self, op: Operation) -> str:
506
+ a, b = op.inputs[1:3] if op.op == "addmm" else op.inputs[:2]
507
+ if not isinstance(a, TensorSpec) or not isinstance(b, TensorSpec) or len(a.shape) != 2 or len(b.shape) != 2 or a.shape[1] != b.shape[0]:
508
+ raise ValueError("trainkernel: mm requires compatible rank-two tensors")
509
+ m, k = a.shape
510
+ _, n = b.shape
511
+ if op.output.shape != (m, n):
512
+ raise ValueError("trainkernel: matrix output shape does not match its inputs")
513
+ def epilogue(acc: str, index: str) -> str:
514
+ if op.op != "addmm":
515
+ return acc
516
+ alpha, beta = op.attrs.get("alpha", 1), op.attrs.get("beta", 1)
517
+ if alpha == 0 and beta == 0:
518
+ return "0.0f"
519
+ if beta == 0:
520
+ return f"({_literal(alpha)} * ({acc}))"
521
+ bias = self.operand(op.inputs[0], op.output, index)
522
+ if alpha == 0:
523
+ return f"({_literal(beta)} * ({bias}))"
524
+ return f"({_literal(alpha)} * ({acc}) + {_literal(beta)} * ({bias}))"
525
+ if self.split_k_gemv and m == 1 and k >= 64:
526
+ from .gemv import emit_gemv
527
+ return emit_gemv(m, k, n, lambda r, q: self.read(a, (r, q)), lambda q, c: self.read(b, (q, c)),
528
+ lambda index, value: self.store_mm(op, index, value), lambda index, value: epilogue(value, index), threads=self.threads_per_block).source
529
+ if k <= 8 or m <= 4:
530
+ # For online and micro-batch gradients, WMMA's 32-wide K staging
531
+ # wastes work and synchronization. Each thread owns one output
532
+ # element and performs the exact FP32 accumulation directly.
533
+ value = "acc"
534
+ index = f"rr * {n}LL + cc"
535
+ result = epilogue(value, index)
536
+ return f"""for (int64_t i = tid; i < {m * n}LL; i += nt) {{
537
+ const int64_t rr = i / {n}LL, cc = i % {n}LL;
538
+ float acc = 0.0f;
539
+ #pragma unroll 4
540
+ for (int kk = 0; kk < {k}; ++kk) acc = fmaf({self.read(a, ('rr', 'kk'))}, {self.read(b, ('kk', 'cc'))}, acc);
541
+ {self.store_mm(op, 'i', result)}
542
+ }}"""
543
+ if a.dtype == b.dtype and a.dtype in {"bfloat16", "float16"}:
544
+ self.tensor_core_count += 1
545
+ cast = "__float2bfloat16_rn" if a.dtype == "bfloat16" else "__float2half_rn"
546
+ typ = _DTYPES[a.dtype]
547
+ if self.gemm_tile_n == 64 and m >= 16 and n >= 64:
548
+ from .wmma import emit_wmma
549
+ return emit_wmma(m, k, n, lambda r, q: self.read(a, (r, q)), lambda q, c: self.read(b, (q, c)),
550
+ lambda index, value: self.store_mm(op, index, value),
551
+ lambda index, value: epilogue(value, index), k_tile=self.gemm_k_tile, value_type=typ, cast=cast).source
552
+ tile_size = 16 if m <= 16 and n <= 16 else 32
553
+ a_pitch = self.gemm_k_tile + self.shared_skew
554
+ b_pitch = tile_size + self.shared_skew
555
+ c_pitch = tile_size + self.shared_skew // 2
556
+ warp_columns = tile_size // 16
557
+ active_warps = warp_columns * warp_columns
558
+ transpose_a = a.strides == (1, a.shape[0])
559
+ a_row = f"row + p % {tile_size}" if transpose_a else f"row + p / {self.gemm_k_tile}"
560
+ a_k = f"kk + p / {tile_size}" if transpose_a else f"kk + p % {self.gemm_k_tile}"
561
+ a_store = f"sa[(p % {tile_size}) * {a_pitch} + p / {tile_size}]" if transpose_a else f"sa[(p / {self.gemm_k_tile}) * {a_pitch} + p % {self.gemm_k_tile}]"
562
+ av = self.read(a, ("ar", "ak"))
563
+ # A transposed B view has unit stride in K. Assign adjacent
564
+ # loader lanes to adjacent K elements and scatter into the
565
+ # row-major WMMA tile; this keeps the global read coalesced while
566
+ # preserving the layout expected by load_matrix_sync.
567
+ transpose_b = b.strides == (1, b.shape[0])
568
+ if transpose_b:
569
+ bv = self.read(b, (f"kk + p % {self.gemm_k_tile}", f"col + p / {self.gemm_k_tile}"))
570
+ b_store = f"sb[(p % {self.gemm_k_tile}) * {b_pitch} + p / {self.gemm_k_tile}]"
571
+ b_valid = f"(kk + p % {self.gemm_k_tile} < {k}LL && col + p / {self.gemm_k_tile} < {n}LL)"
572
+ else:
573
+ bv = self.read(b, ("bk", "bc"))
574
+ b_store = f"sb[(p / {tile_size}) * {b_pitch} + p % {tile_size}]"
575
+ b_valid = f"(bk < {k}LL && bc < {n}LL)"
576
+ return f"""{{
577
+ auto* sa = reinterpret_cast<{typ}*>(shared_a);
578
+ auto* sb = reinterpret_cast<{typ}*>(shared_b);
579
+ const int warp = threadIdx.x / 32;
580
+ for (int64_t tile = blockIdx.x; tile < {((m + tile_size - 1)//tile_size)*((n + tile_size - 1)//tile_size)}LL; tile += gridDim.x) {{
581
+ const int64_t row = (tile / {(n+tile_size-1)//tile_size or 1}LL) * {tile_size};
582
+ const int64_t col = (tile % {(n+tile_size-1)//tile_size or 1}LL) * {tile_size};
583
+ nvcuda::wmma::fragment<nvcuda::wmma::accumulator, 16, 16, 16, float> acc;
584
+ nvcuda::wmma::fill_fragment(acc, 0.0f);
585
+ // Stage a 32-wide K tile and consume it with two WMMA
586
+ // instructions before the next barrier. This removes one
587
+ // shared-memory synchronization per K tile on SM80.
588
+ for (int64_t kk = 0; kk < {k}LL; kk += {self.gemm_k_tile}) {{
589
+ for (int p = threadIdx.x; p < {tile_size * self.gemm_k_tile}; p += blockDim.x) {{
590
+ const int64_t ar = {a_row}, ak = {a_k};
591
+ const int64_t bk = kk + p / {tile_size}, bc = col + p % {tile_size};
592
+ {a_store} = {cast}((ar < {m}LL && ak < {k}LL) ? {av} : 0.0f);
593
+ {b_store} = {cast}({b_valid} ? {bv} : 0.0f);
594
+ }}
595
+ __syncthreads();
596
+ if (warp < {active_warps}) {{
597
+ nvcuda::wmma::fragment<nvcuda::wmma::matrix_a, 16, 16, 16, {typ}, nvcuda::wmma::row_major> af;
598
+ nvcuda::wmma::fragment<nvcuda::wmma::matrix_b, 16, 16, 16, {typ}, nvcuda::wmma::row_major> bf;
599
+ nvcuda::wmma::load_matrix_sync(af, sa + (warp / {warp_columns}) * 16 * {a_pitch}, {a_pitch});
600
+ nvcuda::wmma::load_matrix_sync(bf, sb + (warp % {warp_columns}) * 16, {b_pitch});
601
+ nvcuda::wmma::mma_sync(acc, af, bf, acc);
602
+ {'' if self.gemm_k_tile == 16 else f'nvcuda::wmma::load_matrix_sync(af, sa + (warp / {warp_columns}) * 16 * {a_pitch} + 16, {a_pitch});'}
603
+ {'' if self.gemm_k_tile == 16 else f'nvcuda::wmma::load_matrix_sync(bf, sb + (warp % {warp_columns}) * 16 + {b_pitch} * 16, {b_pitch});'}
604
+ {'' if self.gemm_k_tile == 16 else 'nvcuda::wmma::mma_sync(acc, af, bf, acc);'}
605
+ }}
606
+ __syncthreads();
607
+ }}
608
+ if (warp < {active_warps}) nvcuda::wmma::store_matrix_sync(shared_c + (warp / {warp_columns}) * 16 * {c_pitch} + (warp % {warp_columns}) * 16, acc, {c_pitch}, nvcuda::wmma::mem_row_major);
609
+ __syncthreads();
610
+ for (int p = threadIdx.x; p < {tile_size * tile_size}; p += blockDim.x) {{
611
+ int64_t rr = row + p / {tile_size}, cc = col + p % {tile_size};
612
+ if (rr < {m}LL && cc < {n}LL) {self.store_mm(op, f'rr * {n}LL + cc', epilogue(f'shared_c[(p / {tile_size}) * {c_pitch} + p % {tile_size}]', f'rr * {n}LL + cc'))}
613
+ }}
614
+ __syncthreads();
615
+ }}
616
+ }}"""
617
+ # Accurate FP32 fallback; kept separate from low-precision tensor-core paths.
618
+ av = self.read(a, ("ar", "ak"))
619
+ bv = self.read(b, ("bk", "bc"))
620
+ return f"""for (int64_t tile = blockIdx.x; tile < {((m+15)//16)*((n+7)//8)}LL; tile += gridDim.x) {{
621
+ int64_t row = (tile / {(n+7)//8 or 1}LL) * 16;
622
+ int64_t col = (tile % {(n+7)//8 or 1}LL) * 8;
623
+ const int rr = threadIdx.x / 8, cc = threadIdx.x % 8;
624
+ float acc = 0.0f;
625
+ for (int64_t kk = 0; kk < {k}LL; kk += 16) {{
626
+ for (int p = threadIdx.x; p < 256; p += blockDim.x) {{
627
+ int64_t ar = row + p / 16, ak = kk + p % 16;
628
+ shared_float_a[p] = (ar < {m}LL && ak < {k}LL) ? {av} : 0.0f;
629
+ }}
630
+ for (int p = threadIdx.x; p < 128; p += blockDim.x) {{
631
+ int64_t bk = kk + p / 8, bc = col + p % 8;
632
+ shared_float_b[p] = (bk < {k}LL && bc < {n}LL) ? {bv} : 0.0f;
633
+ }}
634
+ __syncthreads();
635
+ for (int j = 0; j < 16; ++j) acc = fmaf(shared_float_a[rr * 16 + j], shared_float_b[j * 8 + cc], acc);
636
+ __syncthreads();
637
+ }}
638
+ if (row + rr < {m}LL && col + cc < {n}LL) {self.store_mm(op, f'(row + rr) * {n}LL + col + cc', epilogue('acc', f'(row + rr) * {n}LL + col + cc'))}
639
+ __syncthreads();
640
+ }}"""
641
+
642
+ def generate(self) -> GeneratedKernel:
643
+ for spec in (*self.program.inputs, *(op.output for op in self.program.operations), *self.program.outputs):
644
+ if spec.dtype not in _DTYPES:
645
+ raise ValueError(f"trainkernel: unsupported CUDA dtype {spec.dtype}")
646
+ if any(d < 0 for d in spec.shape) or len(spec.shape) != len(spec.strides) or any(s < 0 for s in spec.strides):
647
+ raise ValueError("trainkernel: invalid static shape or stride")
648
+ if not re.fullmatch(r"[A-Za-z_][A-Za-z_0-9]*", spec.buffer):
649
+ raise ValueError("trainkernel: invalid buffer identifier")
650
+ statements = []
651
+ stage_reads: list[tuple[int, int]] = []
652
+ stage_writes: list[tuple[int, int]] = []
653
+ barriers = 0
654
+
655
+ def read_regions(spec: TensorSpec) -> list[tuple[int, int]]:
656
+ if spec.buffer in self.inline:
657
+ return [region for value in self.producers[spec.buffer].inputs if isinstance(value, TensorSpec) for region in read_regions(value)]
658
+ if spec.buffer in self.input_indices:
659
+ return [self.input_regions[spec.buffer]] if self.direct_aliases else []
660
+ if spec.buffer in self.native_regions:
661
+ return [self.native_regions[spec.buffer]]
662
+ start = self.offsets[spec.buffer]
663
+ return [(start, start + numel(self.producers[spec.buffer].output.shape))]
664
+
665
+ def overlap(left: tuple[int, int], right: tuple[int, int]) -> bool:
666
+ return left[0] < right[1] and right[0] < left[1]
667
+
668
+ for op in self.materialized:
669
+ if op.output.strides != contiguous_strides(op.output.shape):
670
+ # Size-one dimensions may have arbitrary equivalent strides.
671
+ if any(n > 1 and a != b for n, a, b in zip(op.output.shape, op.output.strides, contiguous_strides(op.output.shape))):
672
+ raise ValueError(f"trainkernel: materialized output must be contiguous: {op.name}")
673
+ if op.op in {"mm", "addmm"}:
674
+ code = self.emit_mm(op)
675
+ elif op.op in {"sum", "mean"}:
676
+ code = self.emit_reduction(op)
677
+ elif op.op in _POINTWISE:
678
+ code = self.emit_pointwise(op)
679
+ else:
680
+ raise ValueError(f"trainkernel: unsupported CUDA operation {op.op}")
681
+ reads = [region for value in op.inputs if isinstance(value, TensorSpec) for region in read_regions(value)]
682
+ start = self.offsets[op.output.buffer]
683
+ write = self.native_regions.get(op.output.buffer, (start, start + numel(op.output.shape)))
684
+ writes = ([] if op.output.buffer in self.discarded_mm_roots else [write]) + [(self.offsets[x.output.buffer], self.offsets[x.output.buffer] + numel(x.output.shape)) for x in self.side_epilogues.get(op.output.buffer, ()) if x.output.buffer in self.output_bindings]
685
+ # Remove barriers only when both dependencies and arena reuse permit
686
+ # overlap. Include write-after-read hazards on recycled allocations.
687
+ hazard = any(overlap(old, new) for old in stage_writes for new in (*reads, *writes)) or any(overlap(old, new) for old in stage_reads for new in writes)
688
+ if hazard:
689
+ statements.append("mk_grid_sync(grid);")
690
+ barriers += 1
691
+ stage_reads.clear()
692
+ stage_writes.clear()
693
+ statements.append(f"// {op.name}: {op.op}\n{{\n{code}\n}}")
694
+ stage_reads.extend(reads)
695
+ stage_writes.extend(writes)
696
+ needs_output_copy = any(self.output_bindings.get(spec.buffer, (-1, ""))[0] != i for i, spec in enumerate(self.program.outputs))
697
+ if stage_writes and needs_output_copy:
698
+ statements.append("mk_grid_sync(grid);")
699
+ barriers += 1
700
+ copied_outputs = False
701
+ for i, spec in enumerate(self.program.outputs):
702
+ if i in self.inplace_outputs:
703
+ continue
704
+ if spec.buffer in self.output_bindings and self.output_bindings[spec.buffer][0] == i:
705
+ continue
706
+ value = self.read(spec, self.coords(spec.shape, "i"))
707
+ stored = f"__float2bfloat16_rn({value})" if spec.dtype == "bfloat16" else f"__float2half_rn({value})" if spec.dtype == "float16" else value
708
+ statements.append(f"for (int64_t i = tid; i < {numel(spec.shape)}LL; i += nt) static_cast<{_DTYPES[spec.dtype]}*>(args.outputs[{i}])[i] = {stored};")
709
+ copied_outputs = True
710
+ if self.inplace_outputs:
711
+ if copied_outputs:
712
+ statements.append("mk_grid_sync(grid);")
713
+ barriers += 1
714
+ statements.append("// Transactional input commit: all input reads have completed.")
715
+ for slot in self.inplace_outputs:
716
+ if slot in self.direct_aliases:
717
+ continue
718
+ spec = self.program.outputs[slot]
719
+ value = self.read(spec, self.coords(spec.shape, "i"))
720
+ stored = f"__float2bfloat16_rn({value})" if spec.dtype == "bfloat16" else f"__float2half_rn({value})" if spec.dtype == "float16" else value
721
+ statements.append(f"for (int64_t i = tid; i < {numel(spec.shape)}LL; i += nt) static_cast<{_DTYPES[spec.dtype]}*>(args.outputs[{slot}])[i] = {stored};")
722
+ work_blocks = [1]
723
+ for op in self.materialized:
724
+ if op.op in {"mm", "addmm"}:
725
+ a, b = op.inputs[1:3] if op.op == "addmm" else op.inputs[:2]
726
+ if self.split_k_gemv and a.shape[0] == 1 and a.shape[1] >= 64:
727
+ work_blocks.append((b.shape[1] + 31) // 32)
728
+ elif a.shape[1] <= 8 or a.shape[0] <= 4:
729
+ work_blocks.append((numel(op.output.shape) + self.threads_per_block - 1) // self.threads_per_block)
730
+ else:
731
+ tm, tn = ((32, 64) if self.gemm_tile_n == 64 and a.shape[0] >= 16 and b.shape[1] >= 64 else (16, 16) if a.shape[0] <= 16 and b.shape[1] <= 16 else (32, 32)) if a.dtype in {"float16", "bfloat16"} else (16, 8)
732
+ work_blocks.append(((a.shape[0] + tm - 1) // tm) * ((b.shape[1] + tn - 1) // tn))
733
+ elif op.op in {"sum", "mean"}:
734
+ source = op.inputs[0]
735
+ dims = tuple(int(d) % len(source.shape) for d in op.attrs.get("dims", tuple(range(len(source.shape))))) if source.shape else ()
736
+ reduction_size = math.prod(source.shape[d] for d in set(dims))
737
+ rows = numel(op.output.shape)
738
+ work_blocks.append(rows * min(256, (reduction_size + 511) // 512) if reduction_size > 4096 and rows <= 4 else (rows + self.threads_per_block - 1) // self.threads_per_block if reduction_size <= 128 and rows >= 8 else rows)
739
+ else:
740
+ work_blocks.append((numel(op.output.shape) + self.threads_per_block - 1) // self.threads_per_block)
741
+ work_blocks.extend((numel(output.shape) + self.threads_per_block - 1) // self.threads_per_block for output in self.program.outputs)
742
+ max_work_blocks = max(work_blocks)
743
+ source = _CUDA_PREAMBLE.replace("@INPUT_COUNT@", str(max(1, len(self.program.inputs)))).replace("@OUTPUT_COUNT@", str(max(1, len(self.program.outputs))))
744
+ source = source.replace("@SHARED_A@", str(max(1024, 32 * (self.gemm_k_tile + self.shared_skew)))).replace("@SHARED_B@", str(max(1024, self.gemm_k_tile * (32 + self.shared_skew), self.gemm_k_tile * 64 if self.gemm_tile_n == 64 else 0))).replace("@SHARED_C@", str(max(1024, 32 * (32 + self.shared_skew // 2), 2048 if self.gemm_tile_n == 64 else 0)))
745
+ source += "\n".join(statements) + "\n}\n" + _CUDA_HOST.replace("@INPUT_COUNT@", str(len(self.program.inputs))).replace("@OUTPUT_COUNT@", str(len(self.program.outputs))).replace("@BLOCKS_PER_SM@", str(self.blocks_per_sm)).replace("@MAX_WORK_BLOCKS@", str(max_work_blocks)).replace("@MAX_BLOCKS@", str(-1 if self.max_blocks is None else self.max_blocks)).replace("@THREADS@", str(self.threads_per_block))
746
+ return GeneratedKernel(source, self.workspace_elements, len(self.materialized), len(self.inline), self.tensor_core_count, self.blocks_per_sm, max_work_blocks, barriers + self.internal_barriers, len(self.output_bindings), len(self.direct_aliases))
747
+
748
+
749
+ def generate(program: Program, *, blocks_per_sm: int = 1, fuse_pointwise: bool = True,
750
+ max_blocks: int | None = None, fuse_epilogues: bool = True,
751
+ gemm_k_tile: int = 32, gemm_tile_n: int = 32, shared_skew: int = 0, split_k_gemv: bool = False, threads_per_block: int = 128, inline_input_casts: bool = True, inplace_outputs: dict[int, int] | None = None) -> GeneratedKernel:
752
+ """Generate CUDA source without requiring a CUDA device or CUDA toolkit."""
753
+ validate(program)
754
+ from .passes import canonicalize_transposed_mm
755
+ program = canonicalize_transposed_mm(program)
756
+ validate(program)
757
+ if type(blocks_per_sm) is not int or blocks_per_sm < 1 or blocks_per_sm > 8:
758
+ raise ValueError("trainkernel: blocks_per_sm must be between 1 and 8")
759
+ if max_blocks is not None and (type(max_blocks) is not int or max_blocks < 1):
760
+ raise ValueError("trainkernel: max_blocks must be positive when provided")
761
+ if type(gemm_k_tile) is not int or gemm_k_tile not in {16, 32}:
762
+ raise ValueError("trainkernel: gemm_k_tile must be 16 or 32")
763
+ if type(gemm_tile_n) is not int or gemm_tile_n not in {32, 64}:
764
+ raise ValueError("trainkernel: gemm_tile_n must be 32 or 64")
765
+ if type(shared_skew) is not int or shared_skew not in {0, 16}:
766
+ raise ValueError("trainkernel: shared_skew must be 0 or 16")
767
+ if type(threads_per_block) is not int or threads_per_block not in {128, 256, 512, 1024}:
768
+ raise ValueError("trainkernel: threads_per_block must be 128, 256, 512, or 1024")
769
+ if threads_per_block != 128:
770
+ for op in program.operations:
771
+ if op.op in {"mm", "addmm"}:
772
+ a = op.inputs[1] if op.op == "addmm" else op.inputs[0]
773
+ if a.shape[0] > 4 and a.shape[1] > 8:
774
+ raise ValueError("trainkernel: larger blocks require direct SIMT/GEMV matrix paths")
775
+ mapping = dict(inplace_outputs or {})
776
+ if any(type(i) is not int or type(j) is not int or not 0 <= i < len(program.outputs) or not 0 <= j < len(program.inputs) for i, j in mapping.items()):
777
+ raise ValueError("trainkernel: invalid inplace output/input index")
778
+ if len(set(mapping.values())) != len(mapping):
779
+ raise ValueError("trainkernel: inplace outputs must target distinct inputs")
780
+ produced = {op.output.buffer for op in program.operations}
781
+ for slot, target in mapping.items():
782
+ output, input_spec = program.outputs[slot], program.inputs[target]
783
+ if output.shape != input_spec.shape or output.dtype != input_spec.dtype or input_spec.strides != contiguous_strides(input_spec.shape):
784
+ raise ValueError("trainkernel: inplace output must match a contiguous input shape and dtype")
785
+ if output.buffer not in produced:
786
+ raise ValueError("trainkernel: inplace outputs must be computed values, not input views")
787
+ return _Generator(program, blocks_per_sm=blocks_per_sm, fuse_pointwise=fuse_pointwise,
788
+ max_blocks=max_blocks, fuse_epilogues=fuse_epilogues,
789
+ gemm_k_tile=gemm_k_tile, gemm_tile_n=gemm_tile_n, shared_skew=shared_skew, split_k_gemv=split_k_gemv, threads_per_block=threads_per_block, inline_input_casts=inline_input_casts, inplace_outputs=mapping).generate()
790
+
791
+
792
+ _CUDA_PREAMBLE = r'''
793
+ #include <torch/extension.h>
794
+ #include <ATen/cuda/CUDAContext.h>
795
+ #include <c10/cuda/CUDAGuard.h>
796
+ #include <c10/cuda/CUDAException.h>
797
+ #include <cuda.h>
798
+ #include <cuda_runtime.h>
799
+ #include <cuda_fp16.h>
800
+ #include <cuda_bf16.h>
801
+ #include <cooperative_groups.h>
802
+ #include <mma.h>
803
+ #include <cmath>
804
+ #include <algorithm>
805
+ #include <cstdint>
806
+ namespace cg = cooperative_groups;
807
+ __device__ __forceinline__ void mk_grid_sync(cg::grid_group& grid) {
808
+ if (gridDim.x == 1) __syncthreads();
809
+ else grid.sync();
810
+ }
811
+ struct Arguments { const void* inputs[@INPUT_COUNT@]; void* outputs[@OUTPUT_COUNT@]; };
812
+ __device__ __forceinline__ float mk_relu(float x) { return x < 0.0f ? 0.0f : x; }
813
+ __device__ __forceinline__ float mk_square(float x) { return x * x; }
814
+ __device__ __forceinline__ float mk_block_sum(float value, float* scratch) {
815
+ const int lane = threadIdx.x % 32;
816
+ const int warp = threadIdx.x / 32;
817
+ for (int offset = 16; offset; offset >>= 1) value += __shfl_down_sync(0xffffffff, value, offset);
818
+ if (lane == 0) scratch[warp] = value;
819
+ __syncthreads();
820
+ value = threadIdx.x < blockDim.x / 32 ? scratch[lane] : 0.0f;
821
+ if (warp == 0) for (int offset = 16; offset; offset >>= 1) value += __shfl_down_sync(0xffffffff, value, offset);
822
+ return value;
823
+ }
824
+ __global__ void megakernel_entry(Arguments args, float* workspace) {
825
+ auto grid = cg::this_grid();
826
+ const int64_t tid = int64_t(blockIdx.x) * blockDim.x + threadIdx.x;
827
+ const int64_t nt = int64_t(gridDim.x) * blockDim.x;
828
+ __shared__ __align__(32) unsigned short shared_a[@SHARED_A@];
829
+ __shared__ __align__(32) unsigned short shared_b[@SHARED_B@];
830
+ __shared__ __align__(32) float shared_c[@SHARED_C@];
831
+ __shared__ float shared_reduce[128];
832
+ __shared__ float shared_float_a[256];
833
+ __shared__ float shared_float_b[128];
834
+ '''
835
+
836
+ _CUDA_HOST = r'''
837
+ void launch(std::vector<torch::Tensor> inputs, std::vector<torch::Tensor> outputs, torch::Tensor workspace) {
838
+ TORCH_CHECK(inputs.size() == @INPUT_COUNT@ && outputs.size() == @OUTPUT_COUNT@, "incorrect tensor count");
839
+ TORCH_CHECK(workspace.is_cuda(), "CUDA workspace required");
840
+ c10::cuda::CUDAGuard guard(workspace.device());
841
+ Arguments args{};
842
+ for (size_t i = 0; i < inputs.size(); ++i) {
843
+ TORCH_CHECK(inputs[i].is_cuda() && inputs[i].device() == workspace.device(), "input device mismatch");
844
+ args.inputs[i] = inputs[i].data_ptr();
845
+ }
846
+ for (size_t i = 0; i < outputs.size(); ++i) {
847
+ TORCH_CHECK(outputs[i].is_cuda() && outputs[i].device() == workspace.device(), "output device mismatch");
848
+ args.outputs[i] = outputs[i].data_ptr();
849
+ }
850
+ cudaDeviceProp prop;
851
+ C10_CUDA_CHECK(cudaGetDeviceProperties(&prop, workspace.get_device()));
852
+ TORCH_CHECK(prop.cooperativeLaunch, "device does not support cooperative launch");
853
+ int active = 0;
854
+ C10_CUDA_CHECK(cudaOccupancyMaxActiveBlocksPerMultiprocessor(&active, megakernel_entry, @THREADS@, 0));
855
+ TORCH_CHECK(active > 0, "kernel has no resident blocks");
856
+ int blocks = std::min(prop.multiProcessorCount * std::min(active, @BLOCKS_PER_SM@),
857
+ @MAX_BLOCKS@ < 0 ? @MAX_WORK_BLOCKS@ : std::min(@MAX_WORK_BLOCKS@, @MAX_BLOCKS@));
858
+ float* arena = workspace.data_ptr<float>();
859
+ void* kernel_args[] = {&args, &arena};
860
+ C10_CUDA_CHECK(cudaLaunchCooperativeKernel(reinterpret_cast<void*>(megakernel_entry), dim3(blocks), dim3(@THREADS@), kernel_args, 0, at::cuda::getCurrentCUDAStream()));
861
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
862
+ }
863
+ std::vector<int64_t> resource_usage() {
864
+ cudaFuncAttributes attributes;
865
+ C10_CUDA_CHECK(cudaFuncGetAttributes(&attributes, megakernel_entry));
866
+ int active = 0;
867
+ C10_CUDA_CHECK(cudaOccupancyMaxActiveBlocksPerMultiprocessor(&active, megakernel_entry, @THREADS@, 0));
868
+ return {attributes.numRegs, static_cast<int64_t>(attributes.localSizeBytes), static_cast<int64_t>(attributes.sharedSizeBytes), active};
869
+ }
870
+ '''