kernel-fun 0.2.0.dev1__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.
- kernel_fun/__init__.py +67 -0
- kernel_fun/_common/__init__.py +11 -0
- kernel_fun/_common/cache.py +99 -0
- kernel_fun/_common/compat.py +168 -0
- kernel_fun/_common/support.py +267 -0
- kernel_fun/cconv/__init__.py +24 -0
- kernel_fun/cconv/_kernels/__init__.py +6 -0
- kernel_fun/cconv/_kernels/strip.py +450 -0
- kernel_fun/cconv/_provenance.py +11 -0
- kernel_fun/cconv/ops.py +256 -0
- kernel_fun/kda/__init__.py +23 -0
- kernel_fun/kda/_kernels/__init__.py +7 -0
- kernel_fun/kda/_kernels/bwd_dhu.py +994 -0
- kernel_fun/kda/_kernels/bwd_intra.py +1089 -0
- kernel_fun/kda/_kernels/bwd_intra_triton.py +328 -0
- kernel_fun/kda/_kernels/bwd_scan.py +1105 -0
- kernel_fun/kda/_kernels/bwd_wy.py +320 -0
- kernel_fun/kda/_kernels/bwd_wy_t.py +309 -0
- kernel_fun/kda/_kernels/fwd_intra_triton.py +101 -0
- kernel_fun/kda/_kernels/fwd_state.py +1065 -0
- kernel_fun/kda/_provenance.py +18 -0
- kernel_fun/kda/autograd.py +114 -0
- kernel_fun/kda/chain.py +150 -0
- kernel_fun/kda/ops.py +342 -0
- kernel_fun-0.2.0.dev1.dist-info/METADATA +347 -0
- kernel_fun-0.2.0.dev1.dist-info/RECORD +30 -0
- kernel_fun-0.2.0.dev1.dist-info/WHEEL +4 -0
- kernel_fun-0.2.0.dev1.dist-info/licenses/LICENSE +201 -0
- kernel_fun-0.2.0.dev1.dist-info/licenses/NOTICE +60 -0
- kernel_fun-0.2.0.dev1.dist-info/licenses/THIRD_PARTY_NOTICES.md +175 -0
kernel_fun/cconv/ops.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""`causal_conv1d` — a drop-in for `fla.modules.convolution.causal_conv1d`.
|
|
2
|
+
|
|
3
|
+
Same signature, same `(y, final_state)` return, same validation. On a supported call it runs
|
|
4
|
+
the strip kernels in _kernels/strip.py; on anything else it forwards the call to fla
|
|
5
|
+
verbatim, which is why the fallback is bit-identical rather than merely close.
|
|
6
|
+
|
|
7
|
+
The gate is a WHITELIST, like kda's. Every fla parameter is either implemented here or
|
|
8
|
+
forces the fallback, so an fla flag this package has never seen degrades to fla instead of
|
|
9
|
+
being silently dropped. The supported box is exactly what the KDA layer calls in mainline
|
|
10
|
+
pretraining: `activation` silu/swish, no bias, no residual, no state, no `cu_seqlens`,
|
|
11
|
+
`backend="triton"`, W <= 4, bf16/fp16 `x` of shape [B, T, D] (any strides), a [D, W] weight.
|
|
12
|
+
|
|
13
|
+
Two things this family does differently from kda, both on purpose:
|
|
14
|
+
|
|
15
|
+
- No CuTe, no sm100 gate. Both kernels are Triton, so the arch floor is sm90. They have
|
|
16
|
+
only been TIMED on sm100 (B300); on an H100 they compute the same thing at an
|
|
17
|
+
unmeasured speed.
|
|
18
|
+
- `torch.compiler.disable`, but for a different reason than kda's. This family used to
|
|
19
|
+
go undecorated on the theory that fla's `causal_conv1d` is plain Python around an
|
|
20
|
+
`autograd.Function` and so is this one, so Dynamo would treat the two alike and the
|
|
21
|
+
graph-break count of a compiled block would not change. Production falsified that on
|
|
22
|
+
the 30M mainline ladder (2026-09-04): Dynamo took OUR `autograd.Function` down its
|
|
23
|
+
`trace_backward_graph` path — two tensor arguments and nothing else is exactly the
|
|
24
|
+
shape it agrees to speculate, where fla's eleven-argument Function is not — and
|
|
25
|
+
speculating `cconv_bwd` died on `dy.stride(0)` with a symbolic stride
|
|
26
|
+
(`AssertionError: Cannot construct ConstantVariable for value of type torch.SymInt`),
|
|
27
|
+
a Dynamo bug we cannot fix from here. The decorator makes the entry point opaque, so
|
|
28
|
+
the backward runs eagerly under the autograd engine like kda's does. It also REDUCES
|
|
29
|
+
the break count rather than raising it: the undecorated form already broke twice
|
|
30
|
+
inside this function (at `is_supported`, then at `log_once`) and now breaks once at
|
|
31
|
+
the call. Keep every tensor operation inside the Function anyway — a `.float()` in
|
|
32
|
+
the caller would split a compiled block for no reason.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import logging
|
|
38
|
+
|
|
39
|
+
import torch
|
|
40
|
+
|
|
41
|
+
from .._common import support
|
|
42
|
+
|
|
43
|
+
__all__ = ["causal_conv1d", "is_supported", "warmup"]
|
|
44
|
+
|
|
45
|
+
log = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
# Written for W <= 4 taps (every ladder config); narrower W zero-weights the missing taps.
|
|
48
|
+
MAX_W = 4
|
|
49
|
+
|
|
50
|
+
# Arguments this package understands. Anything else present and non-default -> fla.
|
|
51
|
+
_HANDLED = frozenset({"x", "weight", "activation", "backend", "output_final_state"})
|
|
52
|
+
# Present in fla's signature, and each one non-None/non-False forces the fallback.
|
|
53
|
+
_UNSUPPORTED = frozenset({
|
|
54
|
+
"bias", "residual", "initial_state", "cu_seqlens", "cu_seqlens_cpu", "chunk_indices",
|
|
55
|
+
"cp_context",
|
|
56
|
+
})
|
|
57
|
+
_ACTIVATIONS = ("silu", "swish")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _warm_sig(x: torch.Tensor, w: torch.Tensor) -> tuple:
|
|
61
|
+
"""What decides which kernels a call launches: shapes, dtypes, device. Not the stream —
|
|
62
|
+
see `support.capture_unsupported_reason`."""
|
|
63
|
+
return ("cconv", tuple(x.shape), x.dtype, tuple(w.shape), w.dtype, x.device.index)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def is_supported(
|
|
67
|
+
x: torch.Tensor,
|
|
68
|
+
weight: torch.Tensor | None = None,
|
|
69
|
+
*,
|
|
70
|
+
activation: str | None = None,
|
|
71
|
+
backend: str | None = "triton",
|
|
72
|
+
output_final_state: bool | None = False,
|
|
73
|
+
**kwargs,
|
|
74
|
+
) -> tuple[bool, str | None]:
|
|
75
|
+
"""Can this call use our kernels? Returns (ok, reason-if-not).
|
|
76
|
+
|
|
77
|
+
The reason strings are meant to end up in a training log verbatim.
|
|
78
|
+
"""
|
|
79
|
+
reason = support.triton_unsupported_reason(x, "cconv", min_major=9)
|
|
80
|
+
if reason is not None:
|
|
81
|
+
return False, reason
|
|
82
|
+
|
|
83
|
+
for name in _UNSUPPORTED:
|
|
84
|
+
val = kwargs.get(name)
|
|
85
|
+
if val is not None:
|
|
86
|
+
return False, f"{name}={type(val).__name__} is not implemented here"
|
|
87
|
+
for name in kwargs:
|
|
88
|
+
if name not in _HANDLED and name not in _UNSUPPORTED:
|
|
89
|
+
return False, f"unrecognized argument {name!r} (fla may have grown a flag)"
|
|
90
|
+
|
|
91
|
+
if output_final_state:
|
|
92
|
+
return False, "output_final_state=True (the training path keeps no conv state)"
|
|
93
|
+
if activation not in _ACTIVATIONS:
|
|
94
|
+
return False, f"activation={activation!r} (only silu/swish, the KDA layer's contract)"
|
|
95
|
+
if backend != "triton":
|
|
96
|
+
return False, f"backend={backend!r} was asked for explicitly"
|
|
97
|
+
if weight is None:
|
|
98
|
+
return False, "weight=None"
|
|
99
|
+
if x.dim() != 3:
|
|
100
|
+
return False, f"x must be [B, T, D], got {x.dim()} dims"
|
|
101
|
+
D = x.shape[-1]
|
|
102
|
+
if weight.dim() != 2 or weight.shape[0] != D:
|
|
103
|
+
return False, f"weight must be [D={D}, W], got {list(weight.shape)}"
|
|
104
|
+
W = weight.shape[1]
|
|
105
|
+
if W > MAX_W:
|
|
106
|
+
return False, f"W={W} (the register ring is written for W <= {MAX_W})"
|
|
107
|
+
if x.dtype not in (torch.bfloat16, torch.float16):
|
|
108
|
+
return False, f"dtype {x.dtype} (only bf16 and fp16 x)"
|
|
109
|
+
# Last, so it only speaks for a call that would otherwise be ours.
|
|
110
|
+
needs_grad = torch.is_grad_enabled() and (x.requires_grad or weight.requires_grad)
|
|
111
|
+
reason = support.capture_unsupported_reason(_warm_sig(x, weight), needs_grad)
|
|
112
|
+
if reason is not None:
|
|
113
|
+
return False, reason
|
|
114
|
+
return True, None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _make_fn():
|
|
118
|
+
"""Built lazily: the kernel module imports triton, and importing this package must stay
|
|
119
|
+
cheap on a machine that will never call it."""
|
|
120
|
+
from ._kernels import strip
|
|
121
|
+
|
|
122
|
+
class CausalConv1dStrip(torch.autograd.Function):
|
|
123
|
+
"""Residuals are fla's: x and the weight. The backward recomputes the pre-activation
|
|
124
|
+
in registers from the x it needs for dw anyway, so unlike fla it saves nothing
|
|
125
|
+
else and re-runs nothing."""
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def forward(ctx, x, w, warm_sig):
|
|
129
|
+
y = strip.cconv_fwd(x, w)
|
|
130
|
+
ctx.save_for_backward(x, w)
|
|
131
|
+
ctx.warm_sig = warm_sig
|
|
132
|
+
return y
|
|
133
|
+
|
|
134
|
+
@staticmethod
|
|
135
|
+
def backward(ctx, dy):
|
|
136
|
+
x, w = ctx.saved_tensors
|
|
137
|
+
dx, dw = strip.cconv_bwd(x, w, dy)
|
|
138
|
+
support.mark_warm(ctx.warm_sig, "bwd") # capture may use this shape's backward now
|
|
139
|
+
return dx, dw, None
|
|
140
|
+
|
|
141
|
+
return CausalConv1dStrip
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
_FN = None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@torch.compiler.disable
|
|
148
|
+
def causal_conv1d(
|
|
149
|
+
x: torch.Tensor,
|
|
150
|
+
weight: torch.Tensor | None = None,
|
|
151
|
+
bias: torch.Tensor | None = None,
|
|
152
|
+
residual: torch.Tensor | None = None,
|
|
153
|
+
initial_state: torch.Tensor | None = None,
|
|
154
|
+
output_final_state: bool | None = False,
|
|
155
|
+
activation: str | None = None,
|
|
156
|
+
backend: str | None = "triton",
|
|
157
|
+
cu_seqlens: torch.Tensor | None = None,
|
|
158
|
+
cu_seqlens_cpu: torch.LongTensor | None = None,
|
|
159
|
+
chunk_indices: torch.LongTensor | None = None,
|
|
160
|
+
cp_context=None,
|
|
161
|
+
**kwargs,
|
|
162
|
+
):
|
|
163
|
+
"""Drop-in for ``fla.modules.convolution.causal_conv1d``. See that function for the
|
|
164
|
+
full argument docs. Returns ``(y, None)`` on our path, exactly fla's contract when
|
|
165
|
+
``output_final_state`` is False.
|
|
166
|
+
|
|
167
|
+
``@torch.compiler.disable``: Dynamo must not speculate this family's backward — see
|
|
168
|
+
the module docstring for the crash that taught us so. Unlike kda's decorator this one
|
|
169
|
+
ADDS a break relative to fla (whose conv is traceable), while removing the two the
|
|
170
|
+
undecorated form already cost inside this function. Keep every tensor operation
|
|
171
|
+
inside the Function."""
|
|
172
|
+
global _FN
|
|
173
|
+
from fla.modules.convolution import causal_conv1d as fla_causal_conv1d
|
|
174
|
+
|
|
175
|
+
fla_kwargs = dict(
|
|
176
|
+
x=x, weight=weight, bias=bias, residual=residual, initial_state=initial_state,
|
|
177
|
+
output_final_state=output_final_state, activation=activation, backend=backend,
|
|
178
|
+
cu_seqlens=cu_seqlens, cu_seqlens_cpu=cu_seqlens_cpu, chunk_indices=chunk_indices,
|
|
179
|
+
cp_context=cp_context, **kwargs,
|
|
180
|
+
)
|
|
181
|
+
ok, reason = is_supported(
|
|
182
|
+
x, weight, activation=activation, backend=backend,
|
|
183
|
+
output_final_state=output_final_state, bias=bias, residual=residual,
|
|
184
|
+
initial_state=initial_state, cu_seqlens=cu_seqlens, cu_seqlens_cpu=cu_seqlens_cpu,
|
|
185
|
+
chunk_indices=chunk_indices, cp_context=cp_context, **kwargs,
|
|
186
|
+
)
|
|
187
|
+
if not ok:
|
|
188
|
+
support.log_once(
|
|
189
|
+
f"kernel-fun cconv: falling back to fla — {reason}", logging.WARNING
|
|
190
|
+
)
|
|
191
|
+
return fla_causal_conv1d(**fla_kwargs)
|
|
192
|
+
|
|
193
|
+
assert weight is not None
|
|
194
|
+
# fla's input_guard makes every tensor but x contiguous; the squeezed nn.Conv1d weight
|
|
195
|
+
# already is, so this is a no-op in production and a correctness guard elsewhere.
|
|
196
|
+
w = weight if weight.is_contiguous() else weight.contiguous()
|
|
197
|
+
B, T, D = x.shape
|
|
198
|
+
support.log_versions_once()
|
|
199
|
+
support.log_once(
|
|
200
|
+
f"kernel-fun cconv: engaged (B={B} T={T} D={D} W={w.shape[1]} {x.dtype} "
|
|
201
|
+
f"weight={w.dtype})"
|
|
202
|
+
)
|
|
203
|
+
if _FN is None:
|
|
204
|
+
_FN = _make_fn()
|
|
205
|
+
# fla's input_guard also enters the tensor's device; Triton launches on the current one.
|
|
206
|
+
sig = _warm_sig(x, w)
|
|
207
|
+
with torch.cuda.device(x.device):
|
|
208
|
+
y = _FN.apply(x, w, sig)
|
|
209
|
+
support.mark_warm(sig, "fwd") # the backward marks itself, from the Function
|
|
210
|
+
return y, None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def warmup(
|
|
214
|
+
*,
|
|
215
|
+
B: int,
|
|
216
|
+
T: int,
|
|
217
|
+
D: tuple[int, ...] = (2048, 4096),
|
|
218
|
+
W: int = 4,
|
|
219
|
+
dtype: torch.dtype = torch.bfloat16,
|
|
220
|
+
device: str | torch.device = "cuda",
|
|
221
|
+
) -> float:
|
|
222
|
+
"""Autotune both kernels for the training shapes, so step 1 does not.
|
|
223
|
+
|
|
224
|
+
The autotune key is (D, W, B, T): pass the real microbatch and sequence length, and
|
|
225
|
+
every channel count the layer convolves (q/k at n_heads*head_dim, v at expand_v times
|
|
226
|
+
that). Each new key costs ~24 trial launches per kernel — a few seconds — which is
|
|
227
|
+
exactly what a reported "regression" at step 1 looks like. Also runs the fla
|
|
228
|
+
compatibility probe. Returns the elapsed seconds — log it.
|
|
229
|
+
"""
|
|
230
|
+
import time
|
|
231
|
+
|
|
232
|
+
from .._common.compat import check_fla
|
|
233
|
+
from ._kernels import strip
|
|
234
|
+
|
|
235
|
+
check_fla()
|
|
236
|
+
t0 = time.perf_counter()
|
|
237
|
+
torch.manual_seed(0)
|
|
238
|
+
for d in D:
|
|
239
|
+
x = torch.randn(B, T, d, device=device, dtype=dtype, requires_grad=True)
|
|
240
|
+
w = (torch.rand(d, W, device=device, dtype=torch.float32) * 2 - 1) * W ** -0.5
|
|
241
|
+
w.requires_grad_(True)
|
|
242
|
+
y, _ = causal_conv1d(x, w, activation="silu")
|
|
243
|
+
y.float().square().sum().backward()
|
|
244
|
+
torch.cuda.synchronize()
|
|
245
|
+
|
|
246
|
+
# A warmup that autotuned nothing hides the cost it exists to move.
|
|
247
|
+
empty = [
|
|
248
|
+
k.fn.__name__ for k in (strip.cconv_fwd_strip, strip.cconv_bwd_strip)
|
|
249
|
+
if not getattr(k, "cache", None)
|
|
250
|
+
]
|
|
251
|
+
if empty:
|
|
252
|
+
raise RuntimeError(
|
|
253
|
+
f"kernel-fun cconv warmup autotuned nothing for {empty} — the call fell back "
|
|
254
|
+
f"to fla (KERNEL_FUN_DEBUG=1 logs why), so step 1 will still pay it."
|
|
255
|
+
)
|
|
256
|
+
return time.perf_counter() - t0
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""KDA — Kimi Delta Attention, the gated delta rule with a per-dimension gate.
|
|
2
|
+
|
|
3
|
+
from kernel_fun.kda import chunk_kda
|
|
4
|
+
|
|
5
|
+
Same signature and return contract as `fla.ops.kda.chunk_kda`; anything this package does
|
|
6
|
+
not implement is forwarded to fla, so the swap is one import line and reverting it is one
|
|
7
|
+
line back.
|
|
8
|
+
|
|
9
|
+
Measured on B300 at the production shape (B=16, T=8192, H=HV=16, K=128, V=256, chunk 64;
|
|
10
|
+
2026-09-02, holmes-cs-aus-515, the ladder's record kda/005 history 001):
|
|
11
|
+
|
|
12
|
+
forward+backward, gate and q/k norm in-op 23.74 ms vs fla's 36.56 1.540x
|
|
13
|
+
forward+backward, pre-computed gate 21.80 ms vs fla's 34.90 1.601x
|
|
14
|
+
forward only 5.95 ms vs fla's 7.50 1.260x
|
|
15
|
+
|
|
16
|
+
Ours are the forward scan+readout and four of the backward's seven stages; the rest are
|
|
17
|
+
fla's own kernels, called at fla's own stage boundaries. What each stage runs, and why, is
|
|
18
|
+
in chain.py.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .ops import chunk_kda, is_supported, warmup
|
|
22
|
+
|
|
23
|
+
__all__ = ["chunk_kda", "is_supported", "warmup"]
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Vendored kernels: copied from the research ladder at the commit in _provenance.py.
|
|
2
|
+
|
|
3
|
+
These files are edited on arrival (env knobs frozen to the branch that won, falsified
|
|
4
|
+
alternates deleted, the call cache replaced by kernel_fun._common.cache) and are otherwise
|
|
5
|
+
the measured code. Edit them HERE only for packaging concerns; kernel work happens in the
|
|
6
|
+
ladder, and comes back through tools/vendor.py.
|
|
7
|
+
"""
|