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.
@@ -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
+ """