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/__init__.py +23 -0
- trainkernel/codegen.py +870 -0
- trainkernel/frontend.py +320 -0
- trainkernel/gemv.py +56 -0
- trainkernel/ir.py +63 -0
- trainkernel/passes.py +66 -0
- trainkernel/py.typed +1 -0
- trainkernel/reference.py +53 -0
- trainkernel/runtime.py +142 -0
- trainkernel/wmma.py +130 -0
- trainkernel-0.1.0.dist-info/METADATA +169 -0
- trainkernel-0.1.0.dist-info/RECORD +16 -0
- trainkernel-0.1.0.dist-info/WHEEL +5 -0
- trainkernel-0.1.0.dist-info/licenses/LICENSE +201 -0
- trainkernel-0.1.0.dist-info/licenses/NOTICE +6 -0
- trainkernel-0.1.0.dist-info/top_level.txt +1 -0
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
|
+
'''
|