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/runtime.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""CUDA extension compilation and a checked, static execution boundary."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import hashlib
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import torch
|
|
9
|
+
from torch.utils._pytree import tree_flatten
|
|
10
|
+
|
|
11
|
+
from .codegen import GeneratedKernel, generate
|
|
12
|
+
from .ir import Program
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ExecutionBuffers:
|
|
17
|
+
"""Caller-owned storage. Reuse only when previous work on it is ordered."""
|
|
18
|
+
|
|
19
|
+
outputs: tuple[torch.Tensor, ...]
|
|
20
|
+
workspace: torch.Tensor
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CompiledProgram:
|
|
24
|
+
"""One specialized kernel. Calls allocate independent buffers for stream safety.
|
|
25
|
+
|
|
26
|
+
The result has no autograd registration: compile a functional training step
|
|
27
|
+
containing explicit differentiation to compute gradients and parameter updates.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, program: Program, generated: GeneratedKernel, module: Any, device: torch.device, inplace_outputs: dict[int, int] | None = None):
|
|
31
|
+
self.program = program
|
|
32
|
+
self.generated = generated
|
|
33
|
+
self.source = generated.source
|
|
34
|
+
self.module = module
|
|
35
|
+
self.device = device
|
|
36
|
+
self.inplace_outputs = dict(inplace_outputs or {})
|
|
37
|
+
|
|
38
|
+
def resource_usage(self) -> dict[str, int]:
|
|
39
|
+
"""Compiler resource attributes; these do not require GPU counters."""
|
|
40
|
+
with torch.cuda.device(self.device):
|
|
41
|
+
values = self.module.resource_usage()
|
|
42
|
+
return dict(zip(("registers_per_thread", "local_bytes_per_thread", "shared_bytes_per_block", "resident_blocks_per_sm"), values))
|
|
43
|
+
|
|
44
|
+
def _validate_inputs(self, inputs: tuple[torch.Tensor, ...]) -> None:
|
|
45
|
+
if len(inputs) != len(self.program.inputs):
|
|
46
|
+
raise ValueError(f"trainkernel: expected {len(self.program.inputs)} inputs, got {len(inputs)}")
|
|
47
|
+
for i, (tensor, spec) in enumerate(zip(inputs, self.program.inputs)):
|
|
48
|
+
if not isinstance(tensor, torch.Tensor):
|
|
49
|
+
raise TypeError(f"trainkernel: input {i} must be a tensor")
|
|
50
|
+
if tensor.is_neg() or tensor.is_conj():
|
|
51
|
+
raise ValueError("trainkernel: resolve lazy negative or conjugate input views before execution")
|
|
52
|
+
if tensor.device != self.device or tuple(tensor.shape) != spec.shape or tuple(tensor.stride()) != spec.strides or tensor.dtype != getattr(torch, spec.dtype):
|
|
53
|
+
raise ValueError(f"trainkernel: input {i} does not match compiled device, shape, stride, or dtype")
|
|
54
|
+
if any(inputs[i].requires_grad for i in self.inplace_outputs.values()):
|
|
55
|
+
raise ValueError("trainkernel: inplace targets must be detached from autograd")
|
|
56
|
+
|
|
57
|
+
def allocate(self, *inputs: torch.Tensor) -> ExecutionBuffers:
|
|
58
|
+
if self.inplace_outputs:
|
|
59
|
+
self._validate_inputs(inputs)
|
|
60
|
+
return ExecutionBuffers(
|
|
61
|
+
tuple(inputs[self.inplace_outputs[i]] if i in self.inplace_outputs else torch.empty(spec.shape, dtype=getattr(torch, spec.dtype), device=self.device) for i, spec in enumerate(self.program.outputs)),
|
|
62
|
+
torch.empty(self.generated.workspace_elements, dtype=torch.float32, device=self.device),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def run_into(self, *inputs: torch.Tensor, buffers: ExecutionBuffers) -> tuple[torch.Tensor, ...]:
|
|
66
|
+
self._validate_inputs(inputs)
|
|
67
|
+
if len(buffers.outputs) != len(self.program.outputs):
|
|
68
|
+
raise ValueError("trainkernel: incorrect output buffer count")
|
|
69
|
+
if buffers.workspace.device != self.device or buffers.workspace.dtype != torch.float32 or not buffers.workspace.is_contiguous() or buffers.workspace.numel() < self.generated.workspace_elements:
|
|
70
|
+
raise ValueError("trainkernel: workspace does not match compiled requirements")
|
|
71
|
+
# Caller buffers must be disjoint from each other and the inputs. A
|
|
72
|
+
# conservative storage-level check prevents writes clobbering dependencies.
|
|
73
|
+
output_storages = []
|
|
74
|
+
allowed_storages = set()
|
|
75
|
+
for slot, (out, spec) in enumerate(zip(buffers.outputs, self.program.outputs)):
|
|
76
|
+
if out.device != self.device or out.dtype != getattr(torch, spec.dtype) or tuple(out.shape) != spec.shape or not out.is_contiguous():
|
|
77
|
+
raise ValueError("trainkernel: output buffer does not match compiled requirements")
|
|
78
|
+
if out.numel():
|
|
79
|
+
output_storages.append(out.untyped_storage().data_ptr())
|
|
80
|
+
if slot in self.inplace_outputs:
|
|
81
|
+
target_index = self.inplace_outputs[slot]
|
|
82
|
+
target = inputs[target_index]
|
|
83
|
+
if out.data_ptr() != target.data_ptr() or tuple(out.stride()) != tuple(target.stride()):
|
|
84
|
+
raise ValueError("trainkernel: inplace output must bind its declared input")
|
|
85
|
+
if target.numel():
|
|
86
|
+
storage = target.untyped_storage().data_ptr()
|
|
87
|
+
if any(i != target_index and value.numel() and value.untyped_storage().data_ptr() == storage for i, value in enumerate(inputs)):
|
|
88
|
+
raise ValueError("trainkernel: inplace target must not alias another input")
|
|
89
|
+
allowed_storages.add(storage)
|
|
90
|
+
if buffers.workspace.numel():
|
|
91
|
+
output_storages.append(buffers.workspace.untyped_storage().data_ptr())
|
|
92
|
+
input_storages = {tensor.untyped_storage().data_ptr() for tensor in inputs if tensor.numel()}
|
|
93
|
+
if len(set(output_storages)) != len(output_storages) or input_storages.intersection(output_storages) - allowed_storages:
|
|
94
|
+
raise ValueError("trainkernel: execution buffers must not alias inputs or each other")
|
|
95
|
+
self.module.launch(list(inputs), list(buffers.outputs), buffers.workspace)
|
|
96
|
+
if self.inplace_outputs:
|
|
97
|
+
torch.autograd.graph.increment_version([inputs[i] for i in self.inplace_outputs.values()])
|
|
98
|
+
return buffers.outputs
|
|
99
|
+
|
|
100
|
+
def __call__(self, *inputs: torch.Tensor) -> tuple[torch.Tensor, ...]:
|
|
101
|
+
return self.run_into(*inputs, buffers=self.allocate(*inputs))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def compile_program(program: Program, example_inputs: tuple[Any, ...] | list[Any], *, blocks_per_sm: int = 1, max_blocks: int | None = None, fuse_pointwise: bool = True, fuse_epilogues: bool = True, 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, verbose: bool = False) -> CompiledProgram:
|
|
105
|
+
"""Compile for the current input device, requiring Linux, CUDA and SM80+."""
|
|
106
|
+
flat, _ = tree_flatten(example_inputs)
|
|
107
|
+
if not flat or any(not isinstance(x, torch.Tensor) for x in flat):
|
|
108
|
+
raise TypeError("trainkernel: nonempty tensor inputs required")
|
|
109
|
+
if len(flat) != len(program.inputs):
|
|
110
|
+
raise ValueError("trainkernel: input count differs from the program signature")
|
|
111
|
+
if not torch.cuda.is_available() or any(x.device.type != "cuda" for x in flat):
|
|
112
|
+
raise RuntimeError("trainkernel: CUDA SM80+ GPU required; CPU IR inspection is available through capture()")
|
|
113
|
+
device = flat[0].device
|
|
114
|
+
if any(x.device != device for x in flat):
|
|
115
|
+
raise ValueError("trainkernel: all inputs must use the same CUDA device")
|
|
116
|
+
capability = torch.cuda.get_device_capability(device)
|
|
117
|
+
if capability < (8, 0):
|
|
118
|
+
raise RuntimeError("trainkernel: NVIDIA compute capability 8.0 or newer required")
|
|
119
|
+
generated = generate(program, blocks_per_sm=blocks_per_sm, max_blocks=max_blocks, fuse_pointwise=fuse_pointwise, fuse_epilogues=fuse_epilogues, 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=inplace_outputs)
|
|
120
|
+
if any(flat[i].requires_grad for i in (inplace_outputs or {}).values()):
|
|
121
|
+
raise ValueError("trainkernel: inplace targets must be detached from autograd")
|
|
122
|
+
from torch.utils.cpp_extension import CUDA_HOME, load_inline
|
|
123
|
+
if CUDA_HOME is None:
|
|
124
|
+
raise RuntimeError("trainkernel: CUDA toolkit with nvcc is required to compile generated kernels")
|
|
125
|
+
name = "trainkernel_" + hashlib.sha256((generated.source + str(capability) + torch.__version__).encode()).hexdigest()[:20]
|
|
126
|
+
# Restrict compilation to the actual target rather than relying on a global
|
|
127
|
+
# TORCH_CUDA_ARCH_LIST, which may target a different GPU or unsupported SM.
|
|
128
|
+
arch = f"-gencode=arch=compute_{capability[0]}{capability[1]},code=sm_{capability[0]}{capability[1]}"
|
|
129
|
+
with torch.cuda.device(device):
|
|
130
|
+
module = load_inline(
|
|
131
|
+
name=name,
|
|
132
|
+
cpp_sources="void launch(std::vector<torch::Tensor>, std::vector<torch::Tensor>, torch::Tensor); std::vector<int64_t> resource_usage();",
|
|
133
|
+
cuda_sources=generated.source,
|
|
134
|
+
functions=["launch", "resource_usage"],
|
|
135
|
+
extra_cuda_cflags=["-O3", "-std=c++17", "--fmad=false", arch],
|
|
136
|
+
extra_cflags=["-O3"],
|
|
137
|
+
with_cuda=True,
|
|
138
|
+
verbose=verbose,
|
|
139
|
+
)
|
|
140
|
+
result = CompiledProgram(program, generated, module, device, inplace_outputs)
|
|
141
|
+
result._validate_inputs(tuple(flat))
|
|
142
|
+
return result
|
trainkernel/wmma.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Optional 32x64 WMMA GEMM source emitter.
|
|
2
|
+
|
|
3
|
+
This module deliberately has no dependency on the scheduler. The callbacks
|
|
4
|
+
are the small boundary needed to retain scheduler-specific addressing and
|
|
5
|
+
epilogue fusion while keeping this experimental tile independently testable.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Callable
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
Read = Callable[[str, str], str]
|
|
13
|
+
Store = Callable[[str, str], str]
|
|
14
|
+
Epilogue = Callable[[str, str], str]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class WMMAEmission:
|
|
19
|
+
source: str
|
|
20
|
+
tile_m: int = 32
|
|
21
|
+
tile_n: int = 64
|
|
22
|
+
warps: int = 4
|
|
23
|
+
shared_a_elements: int = 0
|
|
24
|
+
shared_b_elements: int = 0
|
|
25
|
+
shared_c_elements: int = 2048
|
|
26
|
+
|
|
27
|
+
# A/B are FP16/BF16 (2 bytes each); C is FP32 (4 bytes). Keeping these
|
|
28
|
+
# separate avoids treating the mixed-width allocation as one element type.
|
|
29
|
+
@property
|
|
30
|
+
def shared_a_bytes(self) -> int:
|
|
31
|
+
return self.shared_a_elements * 2
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def shared_b_bytes(self) -> int:
|
|
35
|
+
return self.shared_b_elements * 2
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def shared_c_bytes(self) -> int:
|
|
39
|
+
return self.shared_c_elements * 4
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def shared_bytes(self) -> int:
|
|
43
|
+
return self.shared_a_bytes + self.shared_b_bytes + self.shared_c_bytes
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def shared_elements(self) -> int:
|
|
47
|
+
return self.shared_a_elements + self.shared_b_elements + self.shared_c_elements
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def emit_wmma(
|
|
51
|
+
m: int,
|
|
52
|
+
k: int,
|
|
53
|
+
n: int,
|
|
54
|
+
read_a: Read,
|
|
55
|
+
read_b: Read,
|
|
56
|
+
store: Store,
|
|
57
|
+
epilogue: Epilogue | None = None,
|
|
58
|
+
*,
|
|
59
|
+
k_tile: int = 32,
|
|
60
|
+
value_type: str = "__nv_bfloat16",
|
|
61
|
+
cast: str = "__float2bfloat16_rn",
|
|
62
|
+
) -> WMMAEmission:
|
|
63
|
+
"""Emit one 32x64 low-precision GEMM work tile.
|
|
64
|
+
|
|
65
|
+
``read_a(row, k)`` and ``read_b(k, col)`` return device expressions.
|
|
66
|
+
``store(index, value)`` must return a complete CUDA statement. The
|
|
67
|
+
optional epilogue receives the logical output index and accumulator
|
|
68
|
+
expression, and may return a replacement expression.
|
|
69
|
+
"""
|
|
70
|
+
if min(m, k, n) < 0 or k_tile not in (16, 32):
|
|
71
|
+
raise ValueError("WMMA dimensions must be non-negative and k_tile must be 16 or 32")
|
|
72
|
+
ep = epilogue or (lambda _index, value: value)
|
|
73
|
+
a_tile = 32 * k_tile
|
|
74
|
+
b_tile = k_tile * 64
|
|
75
|
+
chunks = (k + k_tile - 1) // k_tile
|
|
76
|
+
# Each warp owns one 16-row A slab and two 16-column output slabs.
|
|
77
|
+
lines = [
|
|
78
|
+
"{",
|
|
79
|
+
f" auto* wmma_a = reinterpret_cast<{value_type}*>(shared_a);",
|
|
80
|
+
f" auto* wmma_b = reinterpret_cast<{value_type}*>(shared_b);",
|
|
81
|
+
" const int wmma_warp = threadIdx.x >> 5;",
|
|
82
|
+
f" for (int64_t wtile = blockIdx.x; wtile < {((m + 31) // 32) * ((n + 63) // 64)}LL; wtile += gridDim.x) {{",
|
|
83
|
+
f" const int64_t wrow = (wtile / {max(1, (n + 63) // 64)}LL) * 32LL;",
|
|
84
|
+
f" const int64_t wcol = (wtile % {max(1, (n + 63) // 64)}LL) * 64LL;",
|
|
85
|
+
" nvcuda::wmma::fragment<nvcuda::wmma::accumulator,16,16,16,float> acc0, acc1;",
|
|
86
|
+
" nvcuda::wmma::fill_fragment(acc0, 0.0f);",
|
|
87
|
+
" nvcuda::wmma::fill_fragment(acc1, 0.0f);",
|
|
88
|
+
f" for (int64_t wkk = 0; wkk < {k}LL; wkk += {k_tile}) {{",
|
|
89
|
+
f" for (int p = threadIdx.x; p < {max(a_tile, b_tile)}; p += blockDim.x) {{",
|
|
90
|
+
f" if (p < {a_tile}) {{ const int64_t ar = wrow + p / {k_tile}, ak = wkk + p % {k_tile}; wmma_a[p] = {cast}((ar < {m}LL && ak < {k}LL) ? {read_a('ar', 'ak')} : 0.0f); }}",
|
|
91
|
+
f" if (p < {b_tile}) {{ const int64_t bk = wkk + p / 64, bc = wcol + p % 64; wmma_b[p] = {cast}((bk < {k}LL && bc < {n}LL) ? {read_b('bk', 'bc')} : 0.0f); }}",
|
|
92
|
+
" }",
|
|
93
|
+
" __syncthreads();",
|
|
94
|
+
" if (wmma_warp < 4) {",
|
|
95
|
+
f" const int arow = (wmma_warp >> 1) * 16;",
|
|
96
|
+
" nvcuda::wmma::fragment<nvcuda::wmma::matrix_a,16,16,16," + value_type + ",nvcuda::wmma::row_major> af;",
|
|
97
|
+
" nvcuda::wmma::fragment<nvcuda::wmma::matrix_b,16,16,16," + value_type + ",nvcuda::wmma::row_major> bf0, bf1;",
|
|
98
|
+
f" nvcuda::wmma::load_matrix_sync(af, wmma_a + arow * {k_tile}, {k_tile});",
|
|
99
|
+
" nvcuda::wmma::load_matrix_sync(bf0, wmma_b + (wmma_warp & 1) * 16, 64);",
|
|
100
|
+
" nvcuda::wmma::load_matrix_sync(bf1, wmma_b + (wmma_warp & 1) * 16 + 32, 64);",
|
|
101
|
+
" nvcuda::wmma::mma_sync(acc0, af, bf0, acc0);",
|
|
102
|
+
" nvcuda::wmma::mma_sync(acc1, af, bf1, acc1);",
|
|
103
|
+
]
|
|
104
|
+
if k_tile == 32:
|
|
105
|
+
lines += [
|
|
106
|
+
" nvcuda::wmma::load_matrix_sync(af, wmma_a + arow * 32 + 16, 32);",
|
|
107
|
+
" nvcuda::wmma::load_matrix_sync(bf0, wmma_b + 16 * 64 + (wmma_warp & 1) * 16, 64);",
|
|
108
|
+
" nvcuda::wmma::load_matrix_sync(bf1, wmma_b + 16 * 64 + (wmma_warp & 1) * 16 + 32, 64);",
|
|
109
|
+
" nvcuda::wmma::mma_sync(acc0, af, bf0, acc0);",
|
|
110
|
+
" nvcuda::wmma::mma_sync(acc1, af, bf1, acc1);",
|
|
111
|
+
]
|
|
112
|
+
lines += [
|
|
113
|
+
" }",
|
|
114
|
+
" __syncthreads();",
|
|
115
|
+
" }",
|
|
116
|
+
" if (wmma_warp < 4) {",
|
|
117
|
+
" nvcuda::wmma::store_matrix_sync(shared_c + (wmma_warp >> 1) * 16 * 64 + (wmma_warp & 1) * 16, acc0, 64, nvcuda::wmma::mem_row_major);",
|
|
118
|
+
" nvcuda::wmma::store_matrix_sync(shared_c + (wmma_warp >> 1) * 16 * 64 + (wmma_warp & 1) * 16 + 32, acc1, 64, nvcuda::wmma::mem_row_major);",
|
|
119
|
+
" }",
|
|
120
|
+
" __syncthreads();",
|
|
121
|
+
" for (int p = threadIdx.x; p < 2048; p += blockDim.x) {",
|
|
122
|
+
" const int64_t rr = wrow + p / 64, cc = wcol + p % 64;",
|
|
123
|
+
f" if (rr < {m}LL && cc < {n}LL) {{ const int64_t oi = rr * {n}LL + cc; {store('oi', ep('oi', 'shared_c[p]'))} }}",
|
|
124
|
+
" }",
|
|
125
|
+
" __syncthreads();",
|
|
126
|
+
" }",
|
|
127
|
+
"}",
|
|
128
|
+
]
|
|
129
|
+
return WMMAEmission("\n".join(lines), shared_a_elements=a_tile, shared_b_elements=b_tile)
|
|
130
|
+
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: trainkernel
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Compile a complete static PyTorch training step into one CUDA kernel
|
|
5
|
+
Author: Arya Somu
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/arpjw/trainkernel
|
|
8
|
+
Project-URL: Documentation, https://github.com/arpjw/trainkernel#readme
|
|
9
|
+
Project-URL: Issues, https://github.com/arpjw/trainkernel/issues
|
|
10
|
+
Project-URL: Source, https://github.com/arpjw/trainkernel
|
|
11
|
+
Keywords: pytorch,cuda,compiler,training,gpu
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: GPU :: NVIDIA CUDA
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
License-File: NOTICE
|
|
26
|
+
Requires-Dist: torch>=2.8
|
|
27
|
+
Requires-Dist: ninja>=1.11
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
31
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# TrainKernel
|
|
35
|
+
|
|
36
|
+
TrainKernel compiles a supported static PyTorch training step into one
|
|
37
|
+
cooperative CUDA megakernel. The generated kernel can include the forward pass,
|
|
38
|
+
loss, gradients, and SGD update, so a captured step has no per-operator Python
|
|
39
|
+
launch overhead.
|
|
40
|
+
|
|
41
|
+
The current backend schedules operators in topological order, uses grid barriers for dependencies, inlines single-consumer pointwise operations, and reuses temporary storage after its last reader. BF16 and FP16 matrix multiplications use WMMA tensor cores. FP32 uses a tiled SIMT implementation for accuracy.
|
|
42
|
+
|
|
43
|
+
This is a scoped v0.1 research release for static functional training on NVIDIA
|
|
44
|
+
SM80 GPUs. It is not a general PyTorch compiler and does not claim universal
|
|
45
|
+
speedups.
|
|
46
|
+
|
|
47
|
+
| A100 workload | TrainKernel | Fastest measured Inductor | TrainKernel / Inductor |
|
|
48
|
+
| --- | ---: | ---: | ---: |
|
|
49
|
+
| tiny online MLP | 15.28 μs | 22.85 μs | **0.669, 1.49x faster** |
|
|
50
|
+
| online MLP | 20.32 μs | 24.24 μs | **0.838, 1.19x faster** |
|
|
51
|
+
| small MLP | 66.04 μs | 22.92 μs | 2.894, slower |
|
|
52
|
+
| medium MLP | 255.32 μs | 42.19 μs | 6.052, slower |
|
|
53
|
+
|
|
54
|
+
These are complete forward, backward, and update steps captured as CUDA Graphs.
|
|
55
|
+
The [held-out report](https://github.com/arpjw/trainkernel/blob/main/results/a100-optimization/README.md) publishes all seeds,
|
|
56
|
+
raw timings, correctness checks, baseline failures, and the two regressions.
|
|
57
|
+
|
|
58
|
+
An independent release run adds a one-hot least-squares classification head.
|
|
59
|
+
Across three seeds, TrainKernel's in-place path measured 13.07 us versus
|
|
60
|
+
18.63 us for the fastest measured Inductor configuration at the median, or
|
|
61
|
+
1.43x faster. The [release evidence](https://github.com/arpjw/trainkernel/blob/main/results/release-validation/README.md)
|
|
62
|
+
retains all raw samples and correctness failures. Its cold-cache setup took
|
|
63
|
+
53.62 seconds and required an estimated 7.82 million updates to amortize, so
|
|
64
|
+
this is a steady-state small-workload result rather than a claim about
|
|
65
|
+
short-lived jobs.
|
|
66
|
+
|
|
67
|
+
## Install
|
|
68
|
+
|
|
69
|
+
Linux, Python 3.10+, PyTorch 2.8+, an NVIDIA SM80+ GPU, a CUDA toolkit with `nvcc`, and a C++ compiler are required for generated kernel execution. CPU capture and IR checks run without CUDA.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
python -m pip install trainkernel
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
For development:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python -m venv --system-site-packages .venv
|
|
79
|
+
source .venv/bin/activate
|
|
80
|
+
python -m pip install -e '.[dev]'
|
|
81
|
+
python -m pytest -q
|
|
82
|
+
python -m examples.online_training
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The backend targets SM80+, including A100 and H100. Release evidence is A100-only; H100 has not been tested and Hopper TMA/WGMMA paths are not implemented.
|
|
86
|
+
|
|
87
|
+
## Compile a training step
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import torch
|
|
91
|
+
import trainkernel
|
|
92
|
+
|
|
93
|
+
def train_step(weights, x, target):
|
|
94
|
+
def loss(w):
|
|
95
|
+
return ((torch.relu(x @ w) - target) ** 2).mean()
|
|
96
|
+
gradients, value = torch.func.grad_and_value(loss)(weights)
|
|
97
|
+
return value, gradients, weights - 1e-3 * gradients
|
|
98
|
+
|
|
99
|
+
compiled = trainkernel.compile_training(train_step, (weights, x, target))
|
|
100
|
+
loss, gradients, weights = compiled(weights, x, target)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`compile_training` returns a typed `CompiledTrainingStep`. It verifies that
|
|
104
|
+
gradients match state structure and derives the update mapping, including nested
|
|
105
|
+
state trees. The supplied state must be detached from autograd because the
|
|
106
|
+
training function computes its own gradients. `compiled.capture(...)` creates a
|
|
107
|
+
CUDA Graph after warmup, restores the supplied state after warmup and capture,
|
|
108
|
+
and returns a reusable `CapturedProgram`; replay outputs borrow captured buffers
|
|
109
|
+
with fixed shapes and addresses. See the [100-update example](https://github.com/arpjw/trainkernel/blob/main/examples/online_training.py).
|
|
110
|
+
|
|
111
|
+
Parameters may be nested dictionaries or tuples. Inputs and operations must stay on one device; resolve lazy negative or conjugate input views before compiling. Updated parameters are returned explicitly. The compiler proves direct last-read state commits where safe and stages all other updates transactionally; input mutation and arbitrary aliasing remain unsupported. Different shapes, strides, devices, or dtypes require another compilation.
|
|
112
|
+
|
|
113
|
+
Inspect the compiler without a GPU:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
program, output_tree = trainkernel.capture(train_step, cpu_examples)
|
|
117
|
+
from trainkernel.codegen import generate
|
|
118
|
+
cuda_source = generate(program).source
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Benchmark
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
bash scripts/run_train_loop_suite.sh results/a100-optimization/heldout
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
For a clean release-candidate check, including the useful classification-head
|
|
128
|
+
workload and package build:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
bash scripts/run_release_validation.sh results/release-validation
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The [training-loop harness](https://github.com/arpjw/trainkernel/blob/main/benchmarks/train_loop.py) uses FP32 master weights, BF16 compute, FP32 loss, and FP32 SGD updates. It checks every loss, gradient, and updated parameter for 100 steps and separately checks a captured 20-step replay. Timed graphs include state feedback over four resident batches. They compare pure and in-place megakernels, captured eager PyTorch, and default and eager-rounding Inductor variants. CUDA event timings exclude compilation, capture, reset, and warmup, and retain raw samples. Numerical failures remain visible for all baselines. The [methodology](https://github.com/arpjw/trainkernel/blob/main/docs/a100-methodology.md) explains the held-out seeds and fixed tolerance policy; the [result](https://github.com/arpjw/trainkernel/blob/main/results/a100-optimization/README.md) includes both wins and regressions. The older [native-BF16 fixed-step harness](https://github.com/arpjw/trainkernel/blob/main/benchmarks/train_mlp.py) remains available for the historical report.
|
|
135
|
+
|
|
136
|
+
## Supported subset
|
|
137
|
+
|
|
138
|
+
- FP32, BF16, and FP16 strided tensors with static shapes.
|
|
139
|
+
- Matrix multiplication and `addmm`, broadcast arithmetic, ReLU and its backward, scalar powers, sums and means, dtype casts, fills, and supported storage views.
|
|
140
|
+
- Pure tensor functions, including explicit differentiation through `torch.func.grad_and_value`.
|
|
141
|
+
|
|
142
|
+
Unsupported operations produce an error. There is no silent eager fallback. Attention, dropout/RNG, convolutions, dynamic shapes, arbitrary input mutation, distributed collectives, and asynchronous task-queue scheduling are not implemented.
|
|
143
|
+
|
|
144
|
+
Temporary values use FP32 workspace storage with explicit rounding at low-precision boundaries. Fusion reduces materialization but does not keep an entire model on-chip. The tested larger GEMMs run faster in Inductor; the report keeps those regressions visible.
|
|
145
|
+
|
|
146
|
+
## Design and evidence
|
|
147
|
+
|
|
148
|
+
See [compiler design](https://github.com/arpjw/trainkernel/blob/main/docs/design.md), [A100 optimization evidence](https://github.com/arpjw/trainkernel/blob/main/results/a100-optimization/README.md), [independent release validation](https://github.com/arpjw/trainkernel/blob/main/results/release-validation/README.md), and [GPU access record](https://github.com/arpjw/trainkernel/blob/main/docs/infrastructure.md). CPU tests validate IR semantics, views, reductions, diagnostics, and arena lifetimes. The independent release suite passed 116 tests that compile and execute generated CUDA, compare training outputs across precisions, and exercise CUDA Graph replay.
|
|
149
|
+
|
|
150
|
+
## Related work and roadmap
|
|
151
|
+
|
|
152
|
+
Most adjacent public megakernel systems currently focus on inference:
|
|
153
|
+
[HazyResearch Megakernels](https://github.com/HazyResearch/Megakernels) targets
|
|
154
|
+
low-latency Llama inference, [MegaBake](https://github.com/megabake/megabake)
|
|
155
|
+
captures PyTorch inference workloads for SM90, [AutoMegaKernel](https://github.com/RightNow-AI/AutoMegaKernel)
|
|
156
|
+
generates Llama-family inference kernels, and [Mirage Persistent Kernel](https://arxiv.org/abs/2512.22219)
|
|
157
|
+
targets multi-GPU model inference. TrainKernel instead explores compiling the
|
|
158
|
+
stateful forward, loss, backward, and optimizer update of a small training step
|
|
159
|
+
on A100 into one kernel. It does not claim priority for megakernel compilation
|
|
160
|
+
in general.
|
|
161
|
+
|
|
162
|
+
The next engineering priorities are a measured shape-aware dispatcher that can
|
|
163
|
+
delegate GEMM-heavy steps to Inductor, an experimental `torch.compile` backend
|
|
164
|
+
after AOTAutograd, AdamW, and additional real online-learning workloads. These
|
|
165
|
+
are roadmap items, not v0.1 capabilities.
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
Apache License 2.0. See [LICENSE](https://github.com/arpjw/trainkernel/blob/main/LICENSE) and [NOTICE](https://github.com/arpjw/trainkernel/blob/main/NOTICE).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
trainkernel/__init__.py,sha256=q3Rn3ug1CeCDHN3nxOiYwFBxSj38VssMqdH-1XGYCwk,407
|
|
2
|
+
trainkernel/codegen.py,sha256=mzIFB9agOqBaZvN4d6yyECPSs7mGnV5ghcQLC-iEGfs,52881
|
|
3
|
+
trainkernel/frontend.py,sha256=zeC06BmpclBf5eAL7kisVbffkUGGejmmVAUzXp5zhEU,15438
|
|
4
|
+
trainkernel/gemv.py,sha256=T466_07Jj3sH9RGj_XKD8ZYr_AEni1QJAh6UEaign3c,2049
|
|
5
|
+
trainkernel/ir.py,sha256=ghIch3x1XXni4C5xo9fKyhBcw3s-FKblKVGXaxMcLg8,2525
|
|
6
|
+
trainkernel/passes.py,sha256=pw3VtNeI3CSxUAMFpgOL1cZy3llVFha4M8yHvDYi9Ks,3382
|
|
7
|
+
trainkernel/py.typed,sha256=1F5juyAFRQIScl2WbVd2VzUvkxbW-SYhkQnmeXncq_4,60
|
|
8
|
+
trainkernel/reference.py,sha256=8qZTyq6PYx_VaCn6wBZz_V3AEy1rpnd0saAUzyG8a3M,2468
|
|
9
|
+
trainkernel/runtime.py,sha256=CDhAW4ar5BGGsO9v9P96IqE1JjCuNX_gVzBc0O3s6qo,9053
|
|
10
|
+
trainkernel/wmma.py,sha256=TVjFn4OMUBEc_1C_7BcRkXvvjPmmpLFUsmnMs-i_PRY,5796
|
|
11
|
+
trainkernel-0.1.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
12
|
+
trainkernel-0.1.0.dist-info/licenses/NOTICE,sha256=K2yZZQoC774cNzyge_5x1Dr2wE132L3Lf_Yegg8cVxg,174
|
|
13
|
+
trainkernel-0.1.0.dist-info/METADATA,sha256=FEuQCQaLUWGDFwHXwppMYAnOzvjM1T33SZ6P6jOf5VI,9638
|
|
14
|
+
trainkernel-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
15
|
+
trainkernel-0.1.0.dist-info/top_level.txt,sha256=-EBMWciAioOLO1Pbf9uC9AUqPPTJCbPsfjY_zRE9nEs,12
|
|
16
|
+
trainkernel-0.1.0.dist-info/RECORD,,
|