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.
@@ -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.
@@ -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)