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/frontend.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Static FX capture for pure tensor functions, including functional training."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable, Sequence
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Generic, TypeVar
|
|
7
|
+
|
|
8
|
+
import torch
|
|
9
|
+
from torch.utils._pytree import TreeSpec, tree_flatten, tree_unflatten
|
|
10
|
+
|
|
11
|
+
from .codegen import contiguous_strides
|
|
12
|
+
from .ir import Operation, Program, TensorSpec
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .runtime import CompiledProgram
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
OutputT = TypeVar("OutputT")
|
|
19
|
+
|
|
20
|
+
_DTYPES = {torch.float32: "float32", torch.float16: "float16", torch.bfloat16: "bfloat16"}
|
|
21
|
+
_VIEWS = {"aten.alias.default", "aten.detach.default", "aten.t.default", "aten.transpose.int", "aten.permute.default", "aten.view.default", "aten._unsafe_view.default", "aten.reshape.default", "aten.expand.default", "aten.squeeze.default", "aten.squeeze.dim", "aten.unsqueeze.default"}
|
|
22
|
+
_BINARY = {"add", "sub", "mul", "div", "pow"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class CapturedProgram:
|
|
27
|
+
"""A graph bound to fixed input buffers. Outputs are reused on each replay."""
|
|
28
|
+
|
|
29
|
+
graph: Any
|
|
30
|
+
inputs: tuple[Any, ...]
|
|
31
|
+
outputs: Any
|
|
32
|
+
input_tensors: tuple[torch.Tensor, ...]
|
|
33
|
+
|
|
34
|
+
def replay(self) -> Any:
|
|
35
|
+
self.graph.replay()
|
|
36
|
+
return self.outputs
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class CompiledFunction(Generic[OutputT]):
|
|
40
|
+
"""A callable, inspectable specialization of a static PyTorch function."""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
program: Program,
|
|
45
|
+
runner: CompiledProgram,
|
|
46
|
+
input_spec: TreeSpec,
|
|
47
|
+
output_spec: TreeSpec,
|
|
48
|
+
) -> None:
|
|
49
|
+
self.program = program
|
|
50
|
+
self.runner = runner
|
|
51
|
+
self.input_spec = input_spec
|
|
52
|
+
self.output_spec = output_spec
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def source(self) -> str:
|
|
56
|
+
"""Generated CUDA source for inspection and reproducibility."""
|
|
57
|
+
return self.runner.source
|
|
58
|
+
|
|
59
|
+
def _flatten_inputs(self, args: tuple[Any, ...]) -> tuple[torch.Tensor, ...]:
|
|
60
|
+
flat, actual_spec = tree_flatten(args)
|
|
61
|
+
if actual_spec != self.input_spec:
|
|
62
|
+
raise ValueError("trainkernel: runtime input pytree differs from the compiled signature")
|
|
63
|
+
return tuple(flat)
|
|
64
|
+
|
|
65
|
+
def __call__(self, *args: Any) -> OutputT:
|
|
66
|
+
flat = self._flatten_inputs(args)
|
|
67
|
+
return tree_unflatten(list(self.runner(*flat)), self.output_spec)
|
|
68
|
+
|
|
69
|
+
def capture(self, *args: Any, warmup: int = 3) -> CapturedProgram:
|
|
70
|
+
"""Capture this specialization as a reusable, state-safe CUDA Graph."""
|
|
71
|
+
if type(warmup) is not int or warmup < 1:
|
|
72
|
+
raise ValueError("trainkernel: graph warmup must be positive")
|
|
73
|
+
flat = self._flatten_inputs(args)
|
|
74
|
+
self.runner._validate_inputs(flat)
|
|
75
|
+
with torch.cuda.device(self.runner.device):
|
|
76
|
+
saved = {i: flat[i].clone() for i in self.runner.inplace_outputs.values()}
|
|
77
|
+
try:
|
|
78
|
+
for _ in range(warmup):
|
|
79
|
+
self(*args)
|
|
80
|
+
torch.cuda.synchronize(self.runner.device)
|
|
81
|
+
finally:
|
|
82
|
+
for i, original in saved.items():
|
|
83
|
+
flat[i].copy_(original)
|
|
84
|
+
torch.cuda.synchronize(self.runner.device)
|
|
85
|
+
graph = torch.cuda.CUDAGraph()
|
|
86
|
+
try:
|
|
87
|
+
with torch.cuda.graph(graph):
|
|
88
|
+
outputs = self(*args)
|
|
89
|
+
finally:
|
|
90
|
+
for i, original in saved.items():
|
|
91
|
+
flat[i].copy_(original)
|
|
92
|
+
torch.cuda.synchronize(self.runner.device)
|
|
93
|
+
# Keep immutable leaf references even if callers later edit a dict/list.
|
|
94
|
+
return CapturedProgram(graph, args, outputs, flat)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class CompiledTrainingStep(CompiledFunction[OutputT]):
|
|
98
|
+
"""A compiled `(state, *data) -> (loss, gradients, updated_state)` step."""
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _metadata(node: torch.fx.Node) -> tuple[tuple[int, ...], str]:
|
|
102
|
+
value = node.meta.get("val")
|
|
103
|
+
if not isinstance(value, torch.Tensor):
|
|
104
|
+
raise ValueError(f"trainkernel: missing tensor metadata for {node.name}")
|
|
105
|
+
if value.dtype not in _DTYPES:
|
|
106
|
+
raise ValueError(f"trainkernel: unsupported dtype {value.dtype} at {node.name}")
|
|
107
|
+
if any(not isinstance(dim, int) for dim in value.shape):
|
|
108
|
+
raise ValueError("trainkernel: dynamic shapes are unsupported")
|
|
109
|
+
return tuple(value.shape), _DTYPES[value.dtype]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _view(node: torch.fx.Node, root: TensorSpec, shape: tuple[int, ...], dtype: str) -> TensorSpec:
|
|
113
|
+
target = str(node.target)
|
|
114
|
+
strides = root.strides
|
|
115
|
+
if target in {"aten.t.default", "aten.transpose.int", "aten.permute.default"}:
|
|
116
|
+
axes = list(range(len(root.shape)))
|
|
117
|
+
if target == "aten.t.default":
|
|
118
|
+
axes = axes[::-1]
|
|
119
|
+
elif target == "aten.transpose.int":
|
|
120
|
+
a, b = int(node.args[1]), int(node.args[2])
|
|
121
|
+
axes[a], axes[b] = axes[b], axes[a]
|
|
122
|
+
else:
|
|
123
|
+
axes = [int(a) for a in node.args[1]]
|
|
124
|
+
strides = tuple(root.strides[a] for a in axes)
|
|
125
|
+
elif target == "aten.expand.default":
|
|
126
|
+
padded_shape = (1,) * (len(shape) - len(root.shape)) + root.shape
|
|
127
|
+
padded_stride = (0,) * (len(shape) - len(root.shape)) + root.strides
|
|
128
|
+
strides = tuple(0 if old == 1 and new != 1 else stride for old, new, stride in zip(padded_shape, shape, padded_stride))
|
|
129
|
+
elif target not in {"aten.alias.default", "aten.detach.default"}:
|
|
130
|
+
# Derive the view from our chosen physical layout, which can differ from
|
|
131
|
+
# FX metadata after we materialize an operation into contiguous storage.
|
|
132
|
+
try:
|
|
133
|
+
meta = torch.empty_strided(root.shape, root.strides, device="meta")
|
|
134
|
+
strides = tuple(meta.view(shape).stride())
|
|
135
|
+
except RuntimeError as exc:
|
|
136
|
+
raise ValueError(f"trainkernel: {target} requires a contiguous copy; use .contiguous() before reshaping") from exc
|
|
137
|
+
return TensorSpec(node.name, shape, dtype, strides, root.buffer, root.offset)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def capture(fn: Callable[..., Any], example_inputs: Sequence[Any]) -> tuple[Program, TreeSpec]:
|
|
141
|
+
"""Trace a pure function. Inputs may contain nested tensor pytrees.
|
|
142
|
+
|
|
143
|
+
Scalars should be captured Python constants. Tensor constants, data-dependent
|
|
144
|
+
control flow, input mutations, and unsupported ATen operations are rejected.
|
|
145
|
+
Fake capture avoids running training updates against the user's real tensors.
|
|
146
|
+
"""
|
|
147
|
+
flat_inputs, input_spec = tree_flatten(tuple(example_inputs))
|
|
148
|
+
if not flat_inputs or any(not isinstance(x, torch.Tensor) for x in flat_inputs):
|
|
149
|
+
raise TypeError("trainkernel: example_inputs must contain tensors only and cannot be empty")
|
|
150
|
+
if any(x.dtype not in _DTYPES for x in flat_inputs):
|
|
151
|
+
raise ValueError("trainkernel: unsupported dtype; expected float32, float16, or bfloat16")
|
|
152
|
+
if any(x.layout != torch.strided for x in flat_inputs):
|
|
153
|
+
raise ValueError("trainkernel: only strided tensors are supported")
|
|
154
|
+
if any(x.device != flat_inputs[0].device for x in flat_inputs):
|
|
155
|
+
raise ValueError("trainkernel: inputs must use one device")
|
|
156
|
+
if any(x.is_neg() or x.is_conj() for x in flat_inputs):
|
|
157
|
+
raise ValueError("trainkernel: resolve lazy negative or conjugate input views before capture")
|
|
158
|
+
from torch.func import functionalize
|
|
159
|
+
from torch.fx.experimental.proxy_tensor import make_fx
|
|
160
|
+
|
|
161
|
+
captured_output_spec: TreeSpec | None = None
|
|
162
|
+
|
|
163
|
+
def flattened(*args: torch.Tensor) -> Any:
|
|
164
|
+
nonlocal captured_output_spec
|
|
165
|
+
result = fn(*tree_unflatten(list(args), input_spec))
|
|
166
|
+
leaves, captured_output_spec = tree_flatten(result)
|
|
167
|
+
# FX freezes dict/list constants into its own immutable container types.
|
|
168
|
+
# Trace flat leaves and keep the user's original container specification.
|
|
169
|
+
return tuple(leaves)
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
gm = make_fx(functionalize(flattened), tracing_mode="fake", _allow_non_fake_inputs=True)(*flat_inputs)
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
raise RuntimeError(f"trainkernel: FX capture failed: {exc}") from exc
|
|
175
|
+
|
|
176
|
+
specs: dict[torch.fx.Node, TensorSpec] = {}
|
|
177
|
+
inputs: list[TensorSpec] = []
|
|
178
|
+
operations: list[Operation] = []
|
|
179
|
+
for node in gm.graph.nodes:
|
|
180
|
+
if node.op == "output":
|
|
181
|
+
continue
|
|
182
|
+
if node.op == "placeholder":
|
|
183
|
+
shape, dtype = _metadata(node)
|
|
184
|
+
tensor = flat_inputs[len(inputs)]
|
|
185
|
+
spec = TensorSpec(node.name, shape, dtype, tuple(tensor.stride()), node.name)
|
|
186
|
+
specs[node] = spec
|
|
187
|
+
inputs.append(spec)
|
|
188
|
+
continue
|
|
189
|
+
if node.op != "call_function":
|
|
190
|
+
raise ValueError(f"trainkernel: unsupported FX node {node.op} ({node.target}); pass tensor constants as explicit inputs")
|
|
191
|
+
target = str(node.target)
|
|
192
|
+
if target.startswith("aten.copy_"):
|
|
193
|
+
raise ValueError("trainkernel: input mutation is unsupported; return updated tensors instead")
|
|
194
|
+
shape, dtype = _metadata(node)
|
|
195
|
+
if node.meta["val"].device != flat_inputs[0].device:
|
|
196
|
+
raise ValueError("trainkernel: cross-device operations are unsupported")
|
|
197
|
+
tensor_args = [specs[arg] for arg in node.args if isinstance(arg, torch.fx.Node)]
|
|
198
|
+
if target in _VIEWS:
|
|
199
|
+
specs[node] = _view(node, tensor_args[0], shape, dtype)
|
|
200
|
+
continue
|
|
201
|
+
short = target.split(".")[1] if target.startswith("aten.") else target
|
|
202
|
+
attrs: dict[str, object] = {}
|
|
203
|
+
operands: tuple[TensorSpec | int | float, ...]
|
|
204
|
+
|
|
205
|
+
def value(arg: Any) -> TensorSpec | int | float:
|
|
206
|
+
if isinstance(arg, torch.fx.Node):
|
|
207
|
+
return specs[arg]
|
|
208
|
+
if isinstance(arg, (int, float)):
|
|
209
|
+
return arg
|
|
210
|
+
raise ValueError(f"trainkernel: unsupported argument in {target}: {type(arg).__name__}")
|
|
211
|
+
|
|
212
|
+
if short in _BINARY:
|
|
213
|
+
if short == "div" and (len(node.args) > 2 or node.kwargs.get("rounding_mode") is not None):
|
|
214
|
+
raise ValueError("trainkernel: rounded division is unsupported")
|
|
215
|
+
operands = tuple(value(arg) for arg in node.args[:2])
|
|
216
|
+
if short == "pow" and not isinstance(operands[1], (int, float)):
|
|
217
|
+
raise ValueError("trainkernel: pow supports a scalar exponent only")
|
|
218
|
+
if short in {"add", "sub"}:
|
|
219
|
+
attrs["alpha"] = node.kwargs.get("alpha", node.args[2] if len(node.args) > 2 else 1)
|
|
220
|
+
op = short
|
|
221
|
+
elif short == "addmm":
|
|
222
|
+
operands = tuple(tensor_args)
|
|
223
|
+
attrs.update(alpha=node.kwargs.get("alpha", 1), beta=node.kwargs.get("beta", 1))
|
|
224
|
+
op = short
|
|
225
|
+
elif short in {"neg", "relu", "mm"}:
|
|
226
|
+
operands = tuple(tensor_args)
|
|
227
|
+
op = short
|
|
228
|
+
elif short in {"sum", "mean"}:
|
|
229
|
+
root = tensor_args[0]
|
|
230
|
+
dims = node.args[1] if len(node.args) > 1 else node.kwargs.get("dim", None)
|
|
231
|
+
if dims is None or dims == [] or dims == ():
|
|
232
|
+
dims = tuple(range(len(root.shape)))
|
|
233
|
+
elif isinstance(dims, int):
|
|
234
|
+
dims = (dims,)
|
|
235
|
+
dims = tuple(sorted({int(d) % len(root.shape) for d in dims})) if root.shape else ()
|
|
236
|
+
attrs.update(dims=dims, keepdim=bool(node.args[2] if len(node.args) > 2 else node.kwargs.get("keepdim", False)))
|
|
237
|
+
operands = (root,)
|
|
238
|
+
op = short
|
|
239
|
+
elif short == "threshold_backward":
|
|
240
|
+
operands = tuple(tensor_args[:2])
|
|
241
|
+
attrs["threshold"] = node.args[2]
|
|
242
|
+
op = short
|
|
243
|
+
elif short in {"ones_like", "zeros_like", "full_like"}:
|
|
244
|
+
fill = 1 if short == "ones_like" else 0 if short == "zeros_like" else node.args[1]
|
|
245
|
+
operands = (value(fill),)
|
|
246
|
+
attrs["value"] = fill
|
|
247
|
+
op = "full"
|
|
248
|
+
elif short in {"_to_copy", "clone"}:
|
|
249
|
+
operands = (tensor_args[0],)
|
|
250
|
+
op = "cast"
|
|
251
|
+
else:
|
|
252
|
+
raise ValueError(f"trainkernel: unsupported ATen op {target} at {node.name}")
|
|
253
|
+
output = TensorSpec(node.name, shape, dtype, contiguous_strides(shape), node.name)
|
|
254
|
+
specs[node] = output
|
|
255
|
+
operations.append(Operation(node.name, op, operands, output, attrs))
|
|
256
|
+
out_node = next(node for node in gm.graph.nodes if node.op == "output")
|
|
257
|
+
flat_outputs, _ = tree_flatten(out_node.args[0])
|
|
258
|
+
if captured_output_spec is None:
|
|
259
|
+
raise RuntimeError("trainkernel: capture did not produce an output structure")
|
|
260
|
+
if any(not isinstance(node, torch.fx.Node) or node not in specs for node in flat_outputs):
|
|
261
|
+
raise ValueError("trainkernel: outputs must contain tensors only")
|
|
262
|
+
return Program(tuple(inputs), tuple(operations), tuple(specs[node] for node in flat_outputs)), captured_output_spec
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def compile(fn: Callable[..., Any], example_inputs: Sequence[Any], **options: Any) -> CompiledFunction[Any]:
|
|
266
|
+
"""Compile a static pure function; explicit training graphs return gradients."""
|
|
267
|
+
program, output_spec = capture(fn, example_inputs)
|
|
268
|
+
return _compile_captured(program, output_spec, example_inputs, options)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _compile_captured(
|
|
272
|
+
program: Program,
|
|
273
|
+
output_spec: TreeSpec,
|
|
274
|
+
example_inputs: Sequence[Any],
|
|
275
|
+
options: dict[str, Any],
|
|
276
|
+
compiled_type: type[CompiledFunction[Any]] = CompiledFunction,
|
|
277
|
+
) -> CompiledFunction[Any]:
|
|
278
|
+
_, input_spec = tree_flatten(tuple(example_inputs))
|
|
279
|
+
from .runtime import compile_program
|
|
280
|
+
runner = compile_program(program, example_inputs, **options)
|
|
281
|
+
return compiled_type(program, runner, input_spec, output_spec)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def compile_training(fn: Callable[..., Any], example_inputs: Sequence[Any], **options: Any) -> CompiledTrainingStep[Any]:
|
|
285
|
+
"""Compile `(state, *data) -> (scalar_loss, gradients, updated_state)`.
|
|
286
|
+
|
|
287
|
+
State and gradient pytrees must match. Updated state is committed in place
|
|
288
|
+
only after the compiler proves that each original input has had its last
|
|
289
|
+
read; other updates use a staged final commit. State must
|
|
290
|
+
be detached from autograd; the function contains its own differentiation.
|
|
291
|
+
"""
|
|
292
|
+
if "inplace_outputs" in options:
|
|
293
|
+
raise ValueError("trainkernel: compile_training derives its own state mapping")
|
|
294
|
+
program, output_spec = capture(fn, example_inputs)
|
|
295
|
+
_, input_spec = tree_flatten(tuple(example_inputs))
|
|
296
|
+
# PyTorch 2.11 introduced accessors while 2.8 exposes children_specs.
|
|
297
|
+
def children(spec: TreeSpec) -> Sequence[TreeSpec]:
|
|
298
|
+
accessor = getattr(spec, "children", None)
|
|
299
|
+
return accessor() if callable(accessor) else spec.children_specs
|
|
300
|
+
state_spec = children(input_spec)[0]
|
|
301
|
+
output_children = children(output_spec)
|
|
302
|
+
if output_spec.type not in (tuple, list) or len(output_children) != 3:
|
|
303
|
+
raise ValueError("trainkernel: training function must return loss, gradients, updated state")
|
|
304
|
+
loss_spec, gradient_spec, update_spec = output_children
|
|
305
|
+
if not loss_spec.is_leaf() or gradient_spec != state_spec or update_spec != state_spec:
|
|
306
|
+
raise ValueError("trainkernel: gradients and updated state must match the state pytree")
|
|
307
|
+
n = state_spec.num_leaves
|
|
308
|
+
if not n or program.outputs[0].shape:
|
|
309
|
+
raise ValueError("trainkernel: training requires nonempty state and a scalar loss")
|
|
310
|
+
for gradient, parameter in zip(program.outputs[1:1+n], program.inputs[:n]):
|
|
311
|
+
if gradient.shape != parameter.shape or gradient.dtype != parameter.dtype:
|
|
312
|
+
raise ValueError("trainkernel: gradients must match parameter shapes and dtypes")
|
|
313
|
+
options["inplace_outputs"] = {1 + n + i: i for i in range(n)}
|
|
314
|
+
return _compile_captured(
|
|
315
|
+
program,
|
|
316
|
+
output_spec,
|
|
317
|
+
example_inputs,
|
|
318
|
+
options,
|
|
319
|
+
compiled_type=CompiledTrainingStep,
|
|
320
|
+
)
|
trainkernel/gemv.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Optional cooperative GEMV source emitter."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
Read = Callable[[str, str], str]
|
|
7
|
+
Store = Callable[[str, str], str]
|
|
8
|
+
Epilogue = Callable[[str, str], str]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True)
|
|
12
|
+
class GEMVEmission:
|
|
13
|
+
source: str
|
|
14
|
+
tile_n: int = 32
|
|
15
|
+
warps: int = 4
|
|
16
|
+
threads: int = 128
|
|
17
|
+
shared_c_elements: int = 128
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def emit_gemv(
|
|
21
|
+
m: int, k: int, n: int, read_a: Read, read_b: Read, store: Store,
|
|
22
|
+
epilogue: Epilogue | None = None,
|
|
23
|
+
*, threads: int = 128,
|
|
24
|
+
) -> GEMVEmission:
|
|
25
|
+
"""Emit a 128-thread, four-warp GEMV with a 32-column output tile."""
|
|
26
|
+
if min(m, k, n) < 0:
|
|
27
|
+
raise ValueError("GEMV dimensions must be non-negative")
|
|
28
|
+
if threads not in {128, 256, 512, 1024}:
|
|
29
|
+
raise ValueError("unsupported GEMV block size")
|
|
30
|
+
warps = threads // 32
|
|
31
|
+
ep = epilogue or (lambda _index, value: value)
|
|
32
|
+
lines = [
|
|
33
|
+
"{",
|
|
34
|
+
" for (int64_t tile = blockIdx.x; tile < "
|
|
35
|
+
f"{m * ((n + 31) // 32)}LL; tile += gridDim.x) {{",
|
|
36
|
+
f" const int64_t row = tile / {max(1, (n + 31) // 32)}LL;",
|
|
37
|
+
f" const int64_t col0 = (tile % {max(1, (n + 31) // 32)}LL) * 32LL;",
|
|
38
|
+
" const int warp = threadIdx.x >> 5, lane = threadIdx.x & 31;",
|
|
39
|
+
" const int64_t col = col0 + lane;",
|
|
40
|
+
" float acc = 0.0f;",
|
|
41
|
+
f" for (int64_t kk = warp; kk < {k}LL; kk += {warps}) {{",
|
|
42
|
+
f" if (col < {n}LL) acc = fmaf({read_a('row', 'kk')}, {read_b('kk', 'col')}, acc);",
|
|
43
|
+
" }",
|
|
44
|
+
" shared_c[warp * 32 + lane] = acc;",
|
|
45
|
+
" __syncthreads();",
|
|
46
|
+
" if (warp == 0 && col < " + f"{n}LL) {{",
|
|
47
|
+
f" float sum = 0.0f; for (int w = 0; w < {warps}; ++w) sum += shared_c[w * 32 + lane];",
|
|
48
|
+
" const int64_t oi = row * " + f"{n}LL + col;",
|
|
49
|
+
f" {store('oi', ep('oi', 'sum'))}",
|
|
50
|
+
" }",
|
|
51
|
+
" __syncthreads();",
|
|
52
|
+
" }",
|
|
53
|
+
"}",
|
|
54
|
+
]
|
|
55
|
+
return GEMVEmission("\n".join(lines), warps=warps, threads=threads, shared_c_elements=threads)
|
|
56
|
+
|
trainkernel/ir.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Small, serialisable intermediate representation for megakernel programs."""
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class TensorSpec:
|
|
7
|
+
name: str
|
|
8
|
+
shape: tuple[int, ...]
|
|
9
|
+
dtype: str
|
|
10
|
+
strides: tuple[int, ...]
|
|
11
|
+
buffer: str
|
|
12
|
+
offset: int = 0
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Operation:
|
|
16
|
+
name: str
|
|
17
|
+
op: str
|
|
18
|
+
inputs: tuple[TensorSpec | int | float, ...]
|
|
19
|
+
output: TensorSpec
|
|
20
|
+
attrs: dict[str, object] = field(default_factory=dict)
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Program:
|
|
24
|
+
inputs: tuple[TensorSpec, ...]
|
|
25
|
+
operations: tuple[Operation, ...]
|
|
26
|
+
outputs: tuple[TensorSpec, ...]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def validate(program: Program) -> None:
|
|
30
|
+
"""Check SSA ordering and view bounds before generating memory accesses."""
|
|
31
|
+
roots: dict[str, tuple[TensorSpec, int]] = {}
|
|
32
|
+
|
|
33
|
+
def metadata(spec: TensorSpec) -> None:
|
|
34
|
+
if len(spec.shape) != len(spec.strides) or any(type(d) is not int or d < 0 for d in spec.shape) or any(type(s) is not int or s < 0 for s in spec.strides) or type(spec.offset) is not int or spec.offset < 0:
|
|
35
|
+
raise ValueError("trainkernel: invalid tensor shape, stride, or offset")
|
|
36
|
+
|
|
37
|
+
def read(spec: TensorSpec) -> None:
|
|
38
|
+
metadata(spec)
|
|
39
|
+
if spec.buffer not in roots:
|
|
40
|
+
raise ValueError(f"trainkernel: undefined or future buffer {spec.buffer}")
|
|
41
|
+
root, extent = roots[spec.buffer]
|
|
42
|
+
if root.dtype != spec.dtype:
|
|
43
|
+
raise ValueError("trainkernel: views must preserve the root dtype")
|
|
44
|
+
if math.prod(spec.shape) and spec.offset + sum((d - 1) * s for d, s in zip(spec.shape, spec.strides)) >= extent:
|
|
45
|
+
raise ValueError("trainkernel: tensor view exceeds its root allocation")
|
|
46
|
+
|
|
47
|
+
for spec in program.inputs:
|
|
48
|
+
metadata(spec)
|
|
49
|
+
if spec.buffer in roots or spec.offset:
|
|
50
|
+
raise ValueError("trainkernel: input roots must be distinct and have zero offset")
|
|
51
|
+
extent = 0 if not math.prod(spec.shape) else 1 + sum((d - 1) * s for d, s in zip(spec.shape, spec.strides))
|
|
52
|
+
roots[spec.buffer] = (spec, extent)
|
|
53
|
+
for op in program.operations:
|
|
54
|
+
for operand in op.inputs:
|
|
55
|
+
if isinstance(operand, TensorSpec):
|
|
56
|
+
read(operand)
|
|
57
|
+
spec = op.output
|
|
58
|
+
metadata(spec)
|
|
59
|
+
if spec.buffer in roots or spec.offset:
|
|
60
|
+
raise ValueError("trainkernel: operation outputs must define new roots at offset zero")
|
|
61
|
+
roots[spec.buffer] = (spec, math.prod(spec.shape))
|
|
62
|
+
for spec in program.outputs:
|
|
63
|
+
read(spec)
|
trainkernel/passes.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Pure IR rewrites that preserve the public tensor output contract."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from dataclasses import replace
|
|
4
|
+
from .ir import Operation, Program, TensorSpec
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def canonicalize_transposed_mm(program: Program) -> Program:
|
|
8
|
+
"""Compute `(A @ B).T` as `B.T @ A.T` when every use is that view.
|
|
9
|
+
|
|
10
|
+
PyTorch backward graphs often return transposed weight gradients. Choosing
|
|
11
|
+
their final physical orientation avoids a separate transpose/cast pass.
|
|
12
|
+
Observable untransposed results and other views keep their original layout.
|
|
13
|
+
"""
|
|
14
|
+
changes: dict[str, tuple[int, int]] = {}
|
|
15
|
+
for op in program.operations:
|
|
16
|
+
if op.op != 'mm' or len(op.output.shape) != 2:
|
|
17
|
+
continue
|
|
18
|
+
a,b=op.inputs[:2]
|
|
19
|
+
if not isinstance(a,TensorSpec) or not isinstance(b,TensorSpec) or len(a.shape)!=2 or len(b.shape)!=2:
|
|
20
|
+
continue
|
|
21
|
+
m,n=op.output.shape
|
|
22
|
+
if min(m,n)<=1 or a.shape!=(m,b.shape[0]) or b.shape[1]!=n:
|
|
23
|
+
continue
|
|
24
|
+
uses=[v for user in program.operations for v in user.inputs if isinstance(v,TensorSpec) and v.buffer==op.output.buffer]
|
|
25
|
+
uses.extend(v for v in program.outputs if v.buffer==op.output.buffer)
|
|
26
|
+
if uses and all(v.shape==(n,m) and v.strides==(1,n) and v.offset==0 for v in uses):
|
|
27
|
+
changes[op.output.buffer]=(m,n)
|
|
28
|
+
if not changes:
|
|
29
|
+
return program
|
|
30
|
+
# Flipping a consumer also flips its operand view. Select downstream
|
|
31
|
+
# candidates first, so adjacent rewrites do not undo each other's layout.
|
|
32
|
+
selected: dict[str, tuple[int, int]] = {}
|
|
33
|
+
for op in reversed(program.operations):
|
|
34
|
+
buffer = op.output.buffer
|
|
35
|
+
if buffer not in changes:
|
|
36
|
+
continue
|
|
37
|
+
if any(user.output.buffer in selected for user in program.operations for value in user.inputs if isinstance(value, TensorSpec) and value.buffer == buffer):
|
|
38
|
+
continue
|
|
39
|
+
selected[buffer] = changes[buffer]
|
|
40
|
+
changes = selected
|
|
41
|
+
|
|
42
|
+
def view(value: TensorSpec | int | float) -> TensorSpec | int | float:
|
|
43
|
+
if not isinstance(value,TensorSpec) or value.buffer not in changes:
|
|
44
|
+
return value
|
|
45
|
+
m,n=changes[value.buffer]
|
|
46
|
+
if value.shape==(n,m) and value.strides==(1,n) and value.offset==0:
|
|
47
|
+
return replace(value,strides=(m,1))
|
|
48
|
+
# Transposing an operand of another rewritten GEMM creates this view.
|
|
49
|
+
if value.shape==(m,n) and value.strides==(n,1) and value.offset==0:
|
|
50
|
+
return replace(value,strides=(1,m))
|
|
51
|
+
raise ValueError('trainkernel: unexpected view in transposed GEMM rewrite')
|
|
52
|
+
|
|
53
|
+
operations=[]
|
|
54
|
+
for op in program.operations:
|
|
55
|
+
if op.output.buffer in changes:
|
|
56
|
+
a,b=op.inputs[:2]
|
|
57
|
+
assert isinstance(a,TensorSpec) and isinstance(b,TensorSpec)
|
|
58
|
+
operands=(replace(b,shape=b.shape[::-1],strides=b.strides[::-1]),replace(a,shape=a.shape[::-1],strides=a.strides[::-1]))
|
|
59
|
+
m,n=changes[op.output.buffer]
|
|
60
|
+
output=replace(op.output,shape=(n,m),strides=(m,1))
|
|
61
|
+
operations.append(replace(op,inputs=tuple(view(x) for x in operands),output=output))
|
|
62
|
+
else:
|
|
63
|
+
operations.append(replace(op,inputs=tuple(view(x) for x in op.inputs)))
|
|
64
|
+
outputs=tuple(view(x) for x in program.outputs)
|
|
65
|
+
assert all(isinstance(x,TensorSpec) for x in outputs)
|
|
66
|
+
return Program(program.inputs,tuple(operations),outputs)
|
trainkernel/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# PEP 561 marker for TrainKernel's public type annotations.
|
trainkernel/reference.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""CPU/PyTorch IR interpreter for validation; never a runtime fallback."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
import torch
|
|
6
|
+
from .ir import Program, TensorSpec
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run(program: Program, inputs: Sequence[torch.Tensor]) -> tuple[torch.Tensor, ...]:
|
|
10
|
+
if len(inputs) != len(program.inputs):
|
|
11
|
+
raise ValueError("reference: incorrect input count")
|
|
12
|
+
env = {spec.buffer: tensor for spec, tensor in zip(program.inputs, inputs)}
|
|
13
|
+
|
|
14
|
+
def read(spec: TensorSpec) -> torch.Tensor:
|
|
15
|
+
root = env[spec.buffer]
|
|
16
|
+
return torch.as_strided(root, spec.shape, spec.strides, root.storage_offset() + spec.offset)
|
|
17
|
+
|
|
18
|
+
def value(operand: TensorSpec | int | float) -> torch.Tensor | int | float:
|
|
19
|
+
return read(operand) if isinstance(operand, TensorSpec) else operand
|
|
20
|
+
|
|
21
|
+
for op in program.operations:
|
|
22
|
+
args = [value(x) for x in op.inputs]
|
|
23
|
+
kind = op.op
|
|
24
|
+
if kind == "mm":
|
|
25
|
+
result = args[0] @ args[1]
|
|
26
|
+
elif kind == "addmm":
|
|
27
|
+
result = torch.addmm(args[0], args[1], args[2], alpha=op.attrs.get("alpha", 1), beta=op.attrs.get("beta", 1))
|
|
28
|
+
elif kind == "add":
|
|
29
|
+
result = torch.add(args[0], args[1], alpha=op.attrs.get("alpha", 1))
|
|
30
|
+
elif kind == "sub":
|
|
31
|
+
result = torch.sub(args[0], args[1], alpha=op.attrs.get("alpha", 1))
|
|
32
|
+
elif kind == "mul":
|
|
33
|
+
result = args[0] * args[1]
|
|
34
|
+
elif kind == "div":
|
|
35
|
+
result = args[0] / args[1]
|
|
36
|
+
elif kind == "neg":
|
|
37
|
+
result = -args[0]
|
|
38
|
+
elif kind == "relu":
|
|
39
|
+
result = torch.relu(args[0])
|
|
40
|
+
elif kind == "pow":
|
|
41
|
+
result = args[0] ** args[1]
|
|
42
|
+
elif kind in {"sum", "mean"}:
|
|
43
|
+
result = getattr(args[0], kind)(dim=op.attrs["dims"], keepdim=op.attrs["keepdim"], dtype=getattr(torch, op.output.dtype))
|
|
44
|
+
elif kind == "threshold_backward":
|
|
45
|
+
result = torch.where(args[1] <= op.attrs["threshold"], 0, args[0])
|
|
46
|
+
elif kind == "full":
|
|
47
|
+
result = torch.full(op.output.shape, op.attrs["value"], dtype=getattr(torch, op.output.dtype), device=inputs[0].device)
|
|
48
|
+
elif kind == "cast":
|
|
49
|
+
result = args[0].to(dtype=getattr(torch, op.output.dtype))
|
|
50
|
+
else:
|
|
51
|
+
raise ValueError(f"reference: unsupported operation {kind}")
|
|
52
|
+
env[op.output.buffer] = result.to(dtype=getattr(torch, op.output.dtype)).contiguous()
|
|
53
|
+
return tuple(read(spec) for spec in program.outputs)
|