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,18 @@
1
+ """Generated by tools/vendor.py — what this family was cut from.
2
+
3
+ Hand-edited after vendoring (knobs frozen, dead branches removed, call cache
4
+ unified), so these hashes identify the SOURCE, not the files here. drift.py
5
+ compares them against the ladder's current HEAD.
6
+ """
7
+
8
+ SOURCE_COMMIT = "6fc6309"
9
+ SOURCE_FILES = {
10
+ "kernels/kda/ideas/002-cute-bwd/kernel_dhu.py": "eddf8ded92731b6376ec760eec0b2631e35188aebf6f7b32b7b1f2999ff72e2e",
11
+ "kernels/kda/ideas/002-cute-bwd/kernel_fwd.py": "0387d74fc85e4bac541439e54b306b112d6e70b3f52e4c71e06aa126a1bca963",
12
+ "kernels/kda/ideas/002-cute-bwd/kernel_intra.py": "a18915e9ed4606edadbafacb2d146b99dc7d816ea749492985f4f6b165638878",
13
+ "kernels/kda/ideas/002-cute-bwd/kernel_scan.py": "3aed945213f0741fda017aae9ca904009d7ae606b9a690652bdc0c57cf00817b",
14
+ "kernels/kda/ideas/002-cute-bwd/kernel_wy2.py": "eee08ab2008ab7bc85aa861ba17f638e42a471abe901a68e97f62f6a8ff5392c",
15
+ "kernels/kda/ideas/003-intra-mma/kernel_intra_cute.py": "3e82e01b381a98711d9035077279c8b888d562fa431cf0fa180eb02deab89322",
16
+ "kernels/kda/ideas/004-fwd-block/kernel_fwd_intra_triton.py": "5f69a5e36746f6e96fa6c173d579b71a187c11251e82e7f686fdf0d4f6a42e00",
17
+ "kernels/kda/ideas/005-wy-transposed/kernel_wy_t.py": "0d1ba89945c4fcdaef9a72159e4eca98184f7d291e170f3f6f2730ea178bc7f7",
18
+ }
@@ -0,0 +1,114 @@
1
+ """The autograd Function behind `chunk_kda`.
2
+
3
+ Residuals are fla's own set — q, k, v, g2, beta, Aqk, Akk, h0 (+ the l2norm rstds) — so
4
+ peak activation memory matches fla's, which is a requirement for a drop-in and not an
5
+ accident. Two deliberate differences, both fla's own behaviour rather than ours:
6
+
7
+ - with the fused gate, g2 is NOT saved. The backward recomputes it from the raw (and
8
+ half-size) g, exactly as fla's recompute path does. Saving it would cost 1 GiB per
9
+ layer at prod8192 for a tensor a single kernel launch reproduces.
10
+ - dq/dk come back already in q's dtype when the intra kernel can emit them that way
11
+ (HV == H), so the casts below are no-ops rather than two extra launches.
12
+
13
+ Everything here runs under `torch.compiler.disable` at the entry point, so nothing in this
14
+ file is traced. See ops.py for why.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import torch
20
+
21
+ from .._common import support
22
+ from . import chain
23
+
24
+
25
+ def _make_fn():
26
+ """Built lazily: fla's decorators import triton, and importing this package must stay
27
+ cheap on a machine that will never call it."""
28
+ from fla.utils import autocast_custom_bwd, autocast_custom_fwd, input_guard
29
+
30
+ class ChunkKDAFunction(torch.autograd.Function):
31
+ @staticmethod
32
+ @input_guard
33
+ @autocast_custom_fwd
34
+ def forward(ctx, q, k, v, g, beta, h0, scale, chunk_size, use_qk_l2norm,
35
+ A_log, dt_bias, lower_bound, h0_was_none, warm_sig):
36
+ q_rstd = k_rstd = None
37
+ if use_qk_l2norm:
38
+ from fla.modules.l2norm import l2norm_fwd
39
+
40
+ q, q_rstd = l2norm_fwd(q)
41
+ k, k_rstd = l2norm_fwd(k)
42
+
43
+ g2 = chain.gate_cumsum(g, A_log, dt_bias, chunk_size, lower_bound)
44
+ o, ht, Aqk, Akk = chain.forward(q, k, v, g2, beta, h0, scale, chunk_size)
45
+
46
+ ctx.save_for_backward(
47
+ q, q_rstd, k, k_rstd, v,
48
+ None if A_log is not None else g2,
49
+ beta, Aqk, Akk, h0,
50
+ g if A_log is not None else None, A_log, dt_bias,
51
+ )
52
+ ctx.scale = scale
53
+ ctx.chunk_size = chunk_size
54
+ ctx.lower_bound = lower_bound
55
+ ctx.h0_was_none = h0_was_none
56
+ ctx.warm_sig = warm_sig
57
+ return o, ht
58
+
59
+ @staticmethod
60
+ @input_guard
61
+ @autocast_custom_bwd
62
+ def backward(ctx, do, dht):
63
+ (q, q_rstd, k, k_rstd, v, g2, beta, Aqk, Akk, h0,
64
+ g_org, A_log, dt_bias) = ctx.saved_tensors
65
+ if g2 is None:
66
+ g2 = chain.gate_cumsum(
67
+ g_org, A_log, dt_bias, ctx.chunk_size, ctx.lower_bound
68
+ )
69
+ dq, dk, dv, db, dg, dh0 = chain.backward(
70
+ q=q, k=k, v=v, g2=g2, beta=beta, Aqk=Aqk, Akk=Akk, h0=h0,
71
+ do=do.contiguous(),
72
+ # dht is None when the caller never asked for a final state and nothing
73
+ # differentiated it; the dhu stage needs a tensor, and a zero one is the
74
+ # honest gradient. Shared and read-only, like the zero initial state.
75
+ dht=dht.contiguous() if dht is not None else chain.zero_state(
76
+ *h0.shape, h0.device
77
+ ),
78
+ scale=ctx.scale, chunk_size=ctx.chunk_size,
79
+ )
80
+ if q_rstd is not None:
81
+ from fla.modules.l2norm import l2norm_bwd
82
+
83
+ dq = l2norm_bwd(q, q_rstd, dq)
84
+ dk = l2norm_bwd(k, k_rstd, dk)
85
+
86
+ dA = dbias = None
87
+ if A_log is not None:
88
+ dg, dA, dbias = chain.gate_backward(
89
+ g_org, A_log, dt_bias, dg, ctx.lower_bound
90
+ )
91
+ g_ref = g_org if g_org is not None else g2
92
+ support.mark_warm(ctx.warm_sig, "bwd") # capture may use this shape's backward now
93
+ return (
94
+ dq.to(q.dtype), dk.to(k.dtype), dv.to(v.dtype),
95
+ dg.to(g_ref.dtype), db.to(beta.dtype),
96
+ None if ctx.h0_was_none else dh0,
97
+ None, None, None, dA, dbias, None, None, None,
98
+ )
99
+
100
+ return ChunkKDAFunction
101
+
102
+
103
+ _FN = None
104
+
105
+
106
+ def apply(q, k, v, g, beta, h0, scale, chunk_size, use_qk_l2norm,
107
+ A_log, dt_bias, lower_bound, h0_was_none, warm_sig):
108
+ global _FN
109
+ if _FN is None:
110
+ _FN = _make_fn()
111
+ return _FN.apply(
112
+ q, k, v, g, beta, h0, scale, chunk_size, use_qk_l2norm,
113
+ A_log, dt_bias, lower_bound, h0_was_none, warm_sig,
114
+ )
@@ -0,0 +1,150 @@
1
+ """The KDA chain: which kernel runs at each stage, and in what order.
2
+
3
+ This is fla 0.5.2's own decomposition of `chunk_kda`, launch for launch, with five stages
4
+ replaced. Keeping fla's structure is deliberate — it is what makes a stage-by-stage
5
+ comparison meaningful, and it is why the parity tests can hold to fla's own tolerances.
6
+
7
+ Forward:
8
+ gate+cumsum fla kda_gate_chunk_cumsum (fused activation, or the plain cumsum)
9
+ intra+solve fla token_parallel + inter_solve_fused, preceded by OUR zero-fill of
10
+ Aqk's upper triangles (fla leaves them uninitialized and masks at
11
+ load; our scan contracts the full tile). The zero-fill replaces a
12
+ masked_fill that read the whole 268MB tile to write an eighth of it.
13
+ w/u prep fla recompute_w_u_fwd
14
+ scan + o OURS fused state scan and readout, state resident in registers
15
+
16
+ Backward (fla's recompute path):
17
+ 1 recompute fla recompute_w_u_fwd
18
+ 2 rescan OURS B1: forward re-scan
19
+ 3 dAv fla chunk_kda_bwd_dAv
20
+ 4 dhu OURS B2a: reverse dh/dv scan, dh^T resident in registers
21
+ 5 wy_dqkg OURS the transposed pair (ladder 005): M=K=128 MMAs over a loop that
22
+ streams at the DRAM roofline, a K-strip epilogue at 128 regs so
23
+ two CTAs share an SM, and the v-side/dA chain in a side kernel.
24
+ Below its CTA floor it is B2b, the full-K restructure of fla's
25
+ fused kernel (same math, one K slab instead of NK)
26
+ 6 intra OURS tcgen05 off-diagonals + SIMT diagonals, with the dg reverse-cumsum
27
+ and the bf16 dq/dk cast folded into its epilogue
28
+ 7 dg_cumsum — identity: stage 6 already emitted it
29
+
30
+ Every one of ours falls back to fla's kernel off its supported shape or below the CTA
31
+ floor, so this table is always safe to call; `is_supported` exists so a caller can know
32
+ whether that happened instead of reading a silent 1.00x.
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import torch
38
+
39
+ from .._common.support import log_once
40
+
41
+
42
+ def gate_cumsum(g, A_log, dt_bias, chunk_size, lower_bound=None):
43
+ """The chunk-local cumsum, with the gate activation fused in when there is one.
44
+
45
+ With use_gate_in_kernel the op receives w_g(x) raw and owes
46
+ -exp(A_log)*softplus(g + dt_bias) before the cumsum. Doing that in eager torch is four
47
+ passes over an fp32 [B,T,HV,K] tensor — at prod8192 that is ~2ms and ~2GiB of saved
48
+ activations PER LAYER, against a measured 1.9ms for the entire fused stage. fla fuses
49
+ it into the cumsum this chain launches anyway, so the fused form is free.
50
+ """
51
+ from fla.ops.utils.constant import RCP_LN2
52
+
53
+ if A_log is None:
54
+ from fla.ops.utils import chunk_local_cumsum
55
+
56
+ return chunk_local_cumsum(g=g, scale=RCP_LN2, chunk_size=chunk_size)
57
+
58
+ from fla.ops.kda.gate import kda_gate_chunk_cumsum
59
+
60
+ return kda_gate_chunk_cumsum(
61
+ g=g, A_log=A_log, dt_bias=dt_bias, scale=RCP_LN2, chunk_size=chunk_size,
62
+ lower_bound=lower_bound,
63
+ )
64
+
65
+
66
+ def forward(q, k, v, g2, beta, h0, scale, chunk_size):
67
+ """Returns (o, ht, Aqk, Akk) — o, the final state, and the backward's two residuals."""
68
+ from fla.ops.kda.wy_fast import recompute_w_u_fwd
69
+
70
+ from ._kernels import fwd_intra_triton, fwd_state
71
+
72
+ Aqk, Akk = fwd_intra_triton.chunk_kda_fwd_intra_zerofill(
73
+ q, k, g2, beta, float(scale), chunk_size
74
+ )
75
+ w, u, qg, kg = recompute_w_u_fwd(k=k, v=v, beta=beta, A=Akk, q=q, gk=g2)
76
+ # aqk_prezeroed: the zero-fill above already cleared the upper triangles, so the scan
77
+ # skips the masked_fill it would otherwise do.
78
+ o, ht = fwd_state.kda_cute_fwd_call(
79
+ qg, kg, w, u, Aqk, g2, h0, float(scale), aqk_prezeroed=True
80
+ )
81
+ return o, ht, Aqk, Akk
82
+
83
+
84
+ def backward(q, k, v, g2, beta, Aqk, Akk, h0, do, dht, scale, chunk_size):
85
+ """Returns (dq, dk, dv, dbeta, dg, dh0). dg is w.r.t. the pre-cumsum decay."""
86
+ from fla.ops.kda.chunk_bwd import chunk_kda_bwd_dAv
87
+ from fla.ops.kda.wy_fast import recompute_w_u_fwd
88
+
89
+ from ._kernels import bwd_dhu, bwd_intra, bwd_scan, bwd_wy_t
90
+
91
+ H, HV = q.shape[2], v.shape[2]
92
+
93
+ w, u, qg, kg = recompute_w_u_fwd(q=q, k=k, v=v, beta=beta, A=Akk, gk=g2)
94
+ # dq0 is B1's raw do@h^T when the dq fusion is on. It is off: the fusion measured
95
+ # negative, so wy_dqkg computes dq itself and B1 returns None here.
96
+ h, v_new, _dq0 = bwd_scan.kda_rescan_b1(kg, w, u, g2, h0, do, chunk_size)
97
+ dAqk, dv = chunk_kda_bwd_dAv(
98
+ q=q, k=k, v=v_new, do=do, A=Aqk, scale=scale, chunk_size=chunk_size,
99
+ )
100
+ dh, dh0, dv = bwd_dhu.kda_dhu_b2a(qg, kg, w, g2, h0, dht, do, dv, scale, chunk_size)
101
+ dq, dk, dv, db, dg, dAkk = bwd_wy_t.chunk_kda_bwd_wy_dqkg_t(
102
+ q=q, k=k, v=v, v_new=v_new, g=g2, beta=beta, A=Akk, h=h,
103
+ do=do, dh=dh, dv=dv, scale=scale, chunk_size=chunk_size,
104
+ )
105
+ # fold_dg: intra emits dg already chunk-reverse-cumsum'd, so fla's dg_cumsum stage
106
+ # disappears. emit_bf16: dq/dk come out in q's dtype, making the wrapper's casts no-ops
107
+ # — but only at HV == H, since the GVA reduction below must sum in fp32.
108
+ dq, dk, db, dg = bwd_intra.chunk_kda_bwd_intra_cutedsl(
109
+ q=q, k=k, g=g2, beta=beta, dAqk=dAqk, dAkk=dAkk,
110
+ dq=dq, dk=dk, db=db, dg=dg, chunk_size=chunk_size,
111
+ fold_dg=True, emit_bf16=HV == H,
112
+ )
113
+ # The GVA reduction sits where fla puts it: after intra, before the (folded) cumsum.
114
+ if HV > H:
115
+ G = HV // H
116
+ dq = dq.view(*dq.shape[:2], H, G, dq.shape[-1]).sum(dim=3)
117
+ dk = dk.view(*dk.shape[:2], H, G, dk.shape[-1]).sum(dim=3)
118
+ return dq, dk, dv, db, dg, dh0
119
+
120
+
121
+ def gate_backward(g_org, A_log, dt_bias, dg, lower_bound=None):
122
+ """Close the gate: dg w.r.t. the raw input, plus dA_log and ddt_bias.
123
+
124
+ Applied to the already-reverse-cumsum'd dg, exactly where fla applies it.
125
+ """
126
+ from fla.ops.kda.gate import kda_gate_bwd
127
+
128
+ return kda_gate_bwd(
129
+ g=g_org, A_log=A_log, dt_bias=dt_bias, dyg=dg, lower_bound=lower_bound
130
+ )
131
+
132
+
133
+ _ZEROS: dict = {}
134
+
135
+
136
+ def zero_state(B, HV, K, V, device) -> torch.Tensor:
137
+ """A shared read-only zero initial state.
138
+
139
+ The scan kernels read the initial state unconditionally — there is no null-h0 branch —
140
+ and production almost always passes initial_state=None. Nothing writes this buffer
141
+ (dh0 is a separate output), so one per (shape, device) is shared across every layer
142
+ rather than allocated per call: 134 MiB at prod8192, once.
143
+ """
144
+ key = (B, HV, K, V, device)
145
+ z = _ZEROS.get(key)
146
+ if z is None:
147
+ z = torch.zeros(B, HV, K, V, device=device, dtype=torch.float32)
148
+ _ZEROS[key] = z
149
+ log_once(f"kernel-fun kda: allocated a shared zero state {tuple(z.shape)}")
150
+ return z
kernel_fun/kda/ops.py ADDED
@@ -0,0 +1,342 @@
1
+ """`chunk_kda` — a drop-in for `fla.ops.kda.chunk_kda`.
2
+
3
+ Same signature, same return contract, same validation. On a supported call it runs this
4
+ package's kernels; on anything else it forwards the call to fla verbatim, which is why the
5
+ fallback is bit-identical rather than merely close.
6
+
7
+ The gate is a WHITELIST. Every argument is either known-supported, handled here, or forces
8
+ the fallback — a new fla flag we have never seen degrades to fla instead of being silently
9
+ dropped. `is_supported` returns the reason, because the worst failure mode for a kernel
10
+ port is not being slow, it is running fla while everyone believes otherwise.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+
17
+ import torch
18
+
19
+ from .._common import support
20
+ from . import autograd, chain
21
+
22
+ __all__ = ["chunk_kda", "is_supported", "warmup"]
23
+
24
+ log = logging.getLogger(__name__)
25
+
26
+ # Arguments this package understands. Anything else present and non-default -> fla.
27
+ _HANDLED = frozenset({
28
+ "q", "k", "v", "g", "beta", "scale", "initial_state", "output_final_state",
29
+ "use_qk_l2norm_in_kernel", "use_gate_in_kernel", "use_beta_sigmoid_in_kernel",
30
+ "allow_neg_eigval", "lower_bound", "A_log", "dt_bias", "chunk_size",
31
+ })
32
+ # Present in fla's signature, and each one forces the fallback (see is_supported).
33
+ _UNSUPPORTED = frozenset({
34
+ "cu_seqlens", "cu_seqlens_cpu", "cp_context", "safe_gate", "state_v_first",
35
+ "disable_recompute", "return_intermediate_states", "transpose_state_layout",
36
+ })
37
+
38
+
39
+ def _warm_sig(q: torch.Tensor, v: torch.Tensor, initial_state, kwargs: dict) -> tuple:
40
+ """Everything that decides which kernels a call launches — shapes, dtypes, device and
41
+ the kernel-selecting flags. Not the stream: see `support.capture_unsupported_reason`."""
42
+ return (
43
+ "kda", tuple(q.shape), tuple(v.shape), q.dtype, v.dtype, q.device.index,
44
+ initial_state is not None,
45
+ bool(kwargs.get("use_qk_l2norm_in_kernel", False)),
46
+ bool(kwargs.get("use_gate_in_kernel", False)),
47
+ bool(kwargs.get("use_beta_sigmoid_in_kernel", False)),
48
+ bool(kwargs.get("allow_neg_eigval", False)),
49
+ kwargs.get("lower_bound") is not None,
50
+ )
51
+
52
+
53
+ def is_supported(
54
+ q: torch.Tensor,
55
+ v: torch.Tensor,
56
+ *,
57
+ chunk_size: int = 64,
58
+ initial_state: torch.Tensor | None = None,
59
+ **kwargs,
60
+ ) -> tuple[bool, str | None]:
61
+ """Can this call use our kernels? Returns (ok, reason-if-not).
62
+
63
+ The reason strings are meant to end up in a training log verbatim.
64
+ """
65
+ reason = support.common_unsupported_reason(q, "kda")
66
+ if reason is not None:
67
+ return False, reason
68
+
69
+ for name in _UNSUPPORTED:
70
+ val = kwargs.get(name)
71
+ if val not in (None, False):
72
+ return False, f"{name}={val!r} is not implemented here"
73
+ for name in kwargs:
74
+ if name not in _HANDLED and name not in _UNSUPPORTED:
75
+ return False, f"unrecognized argument {name!r} (fla may have grown a flag)"
76
+
77
+ B, T, H, K = q.shape
78
+ HV, V = v.shape[2], v.shape[-1]
79
+ if chunk_size != 64:
80
+ return False, f"chunk_size={chunk_size} (only 64 is implemented)"
81
+ if T % 64 != 0:
82
+ return False, f"T={T} is not a multiple of the chunk size"
83
+ if K not in (64, 128):
84
+ return False, f"K={K} (only 64 and 128 are implemented)"
85
+ if V % 64 != 0:
86
+ return False, f"V={V} is not a multiple of 64"
87
+ if q.dtype not in (torch.bfloat16, torch.float16):
88
+ return False, f"dtype {q.dtype} (only bf16 and fp16)"
89
+ if not (q.dtype == v.dtype):
90
+ return False, f"mixed dtypes q={q.dtype} v={v.dtype}"
91
+ if initial_state is not None and initial_state.dtype != torch.float32:
92
+ return False, f"initial_state must be fp32, got {initial_state.dtype}"
93
+ # Below this the CuTe scans underfill the GPU and fla is genuinely faster. Not a
94
+ # correctness gate — the kernels would run — so it is worth stating in the log. Where
95
+ # the crossover actually falls is a property of the workload, which is why
96
+ # KERNEL_FUN_KDA_MIN_CTAS can move THIS gate for a shape somebody has timed. It moves
97
+ # nothing else: every check above is a capability, and every stage below keeps its own.
98
+ ctas = B * HV * (V // 64)
99
+ floor = support.min_ctas("kda")
100
+ if ctas < floor:
101
+ why = "fla is faster here" if floor == support.MIN_CTAS else "configured floor"
102
+ return False, f"grid too small ({ctas} CTAs < {floor}); {why}"
103
+ if floor != support.MIN_CTAS:
104
+ caveat = ""
105
+ if floor < support.MIN_CTAS:
106
+ caveat = (
107
+ f"; per-stage floors do not follow it, so the b1 scan and dhu backwards "
108
+ f"are fla's below {support.MIN_CTAS} CTAs"
109
+ )
110
+ support.log_once(
111
+ f"kernel-fun kda: dispatch floor is {floor} CTAs, not the default "
112
+ f"{support.MIN_CTAS} (KERNEL_FUN_KDA_MIN_CTAS){caveat}"
113
+ )
114
+ # Last, so it only speaks for a call that would otherwise be ours.
115
+ needs_grad = torch.is_grad_enabled() and (q.requires_grad or v.requires_grad)
116
+ reason = support.capture_unsupported_reason(
117
+ _warm_sig(q, v, initial_state, kwargs), needs_grad
118
+ )
119
+ if reason is not None:
120
+ return False, reason
121
+ return True, None
122
+
123
+
124
+ @torch.compiler.disable
125
+ def chunk_kda(
126
+ q: torch.Tensor,
127
+ k: torch.Tensor,
128
+ v: torch.Tensor,
129
+ g: torch.Tensor,
130
+ beta: torch.Tensor,
131
+ scale: float | None = None,
132
+ initial_state: torch.Tensor | None = None,
133
+ output_final_state: bool = False,
134
+ use_qk_l2norm_in_kernel: bool = False,
135
+ use_gate_in_kernel: bool = False,
136
+ use_beta_sigmoid_in_kernel: bool = False,
137
+ allow_neg_eigval: bool = False,
138
+ safe_gate: bool = False,
139
+ lower_bound: float | None = None,
140
+ disable_recompute: bool = False,
141
+ return_intermediate_states: bool = False,
142
+ state_v_first: bool = False,
143
+ cu_seqlens: torch.LongTensor | None = None,
144
+ cu_seqlens_cpu: torch.LongTensor | None = None,
145
+ cp_context=None,
146
+ **kwargs,
147
+ ):
148
+ """Drop-in for ``fla.ops.kda.chunk_kda``. See that function for the full argument docs.
149
+
150
+ ``@torch.compiler.disable``: the host path drives compiled CuTe objects through ctypes
151
+ pointer writes and a per-layout call cache, and reads
152
+ ``torch.cuda.current_stream().cuda_stream`` — none of which Dynamo can trace. This
153
+ MOVES a graph break rather than adding one: fla's own ``chunk_kda`` is wrapped in
154
+ ``@dispatch('kda')``, which applies ``torch.compiler.disable`` too. Keep every tensor
155
+ operation inside this function for that reason — a ``.float()`` in the caller would
156
+ split a compiled block in two and cost more than these kernels save.
157
+ """
158
+ from fla.ops.kda import chunk_kda as fla_chunk_kda
159
+
160
+ fla_kwargs = dict(
161
+ q=q, k=k, v=v, g=g, beta=beta, scale=scale, initial_state=initial_state,
162
+ output_final_state=output_final_state,
163
+ use_qk_l2norm_in_kernel=use_qk_l2norm_in_kernel,
164
+ use_gate_in_kernel=use_gate_in_kernel,
165
+ use_beta_sigmoid_in_kernel=use_beta_sigmoid_in_kernel,
166
+ allow_neg_eigval=allow_neg_eigval, safe_gate=safe_gate, lower_bound=lower_bound,
167
+ disable_recompute=disable_recompute,
168
+ return_intermediate_states=return_intermediate_states,
169
+ state_v_first=state_v_first, cu_seqlens=cu_seqlens,
170
+ cu_seqlens_cpu=cu_seqlens_cpu, cp_context=cp_context, **kwargs,
171
+ )
172
+
173
+ chunk_size = kwargs.get("chunk_size", 64)
174
+ ok, reason = is_supported(
175
+ q, v, chunk_size=chunk_size, initial_state=initial_state,
176
+ # The kernel-selecting flags are part of the capture gate's shape signature, so
177
+ # they must reach is_supported exactly as chunk_kda marks them warm below.
178
+ use_qk_l2norm_in_kernel=use_qk_l2norm_in_kernel,
179
+ use_gate_in_kernel=use_gate_in_kernel,
180
+ use_beta_sigmoid_in_kernel=use_beta_sigmoid_in_kernel,
181
+ allow_neg_eigval=allow_neg_eigval, lower_bound=lower_bound,
182
+ safe_gate=safe_gate, state_v_first=state_v_first,
183
+ disable_recompute=disable_recompute,
184
+ return_intermediate_states=return_intermediate_states,
185
+ cu_seqlens=cu_seqlens, cu_seqlens_cpu=cu_seqlens_cpu, cp_context=cp_context,
186
+ **{k_: v_ for k_, v_ in kwargs.items() if k_ != "chunk_size"},
187
+ )
188
+ if not ok:
189
+ support.log_once(
190
+ f"kernel-fun kda: falling back to fla — {reason}", logging.WARNING
191
+ )
192
+ return fla_chunk_kda(**fla_kwargs)
193
+
194
+ # Same validation fla does, so a malformed call fails identically on both paths.
195
+ B, T, H, K = q.shape
196
+ HV = v.shape[2]
197
+ assert q.shape == k.shape, f"q and k must match, got {q.shape} vs {k.shape}"
198
+ assert HV % H == 0, f"HV={HV} must be divisible by H={H}"
199
+ assert g.shape == (B, T, HV, K), f"g must be {[B, T, HV, K]}, got {list(g.shape)}"
200
+ assert beta.shape == (B, T, HV), f"beta must be {[B, T, HV]}, got {list(beta.shape)}"
201
+
202
+ A_log, dt_bias = kwargs.get("A_log"), kwargs.get("dt_bias")
203
+ if use_gate_in_kernel:
204
+ assert A_log is not None, "A_log is required when use_gate_in_kernel=True"
205
+ else:
206
+ A_log = dt_bias = None
207
+ if use_beta_sigmoid_in_kernel:
208
+ from fla.ops.common.gate import fused_beta_sigmoid
209
+
210
+ beta = fused_beta_sigmoid(beta, scale=2.0 if allow_neg_eigval else 1.0)
211
+ # The intra backward types beta as q's dtype; production computes it in fp32
212
+ # (w_b(x).float().sigmoid()*2). Casting here rather than widening the kernel — beta is
213
+ # bounded in (0, 2) and dbeta carries the loosest tolerance in the op.
214
+ if beta.dtype != q.dtype:
215
+ beta = beta.to(q.dtype)
216
+ if scale is None:
217
+ scale = K ** -0.5
218
+
219
+ h0, h0_was_none = initial_state, initial_state is None
220
+ if h0_was_none:
221
+ h0 = chain.zero_state(B, HV, K, v.shape[-1], q.device)
222
+
223
+ support.log_versions_once()
224
+ support.log_once(
225
+ f"kernel-fun kda: engaged (B={B} T={T} H={H} HV={HV} K={K} V={v.shape[-1]} "
226
+ f"chunk={chunk_size} l2norm={use_qk_l2norm_in_kernel} gate={use_gate_in_kernel})"
227
+ )
228
+ sig = _warm_sig(q, v, initial_state, dict(
229
+ use_qk_l2norm_in_kernel=use_qk_l2norm_in_kernel,
230
+ use_gate_in_kernel=use_gate_in_kernel,
231
+ use_beta_sigmoid_in_kernel=use_beta_sigmoid_in_kernel,
232
+ allow_neg_eigval=allow_neg_eigval, lower_bound=lower_bound,
233
+ ))
234
+ o, ht = autograd.apply(
235
+ q, k, v, g, beta, h0, scale, chunk_size, use_qk_l2norm_in_kernel,
236
+ A_log, dt_bias, lower_bound, h0_was_none, sig,
237
+ )
238
+ support.mark_warm(sig, "fwd") # the backward marks itself, from the Function
239
+ return o.type_as(q), (ht if output_final_state else None)
240
+
241
+
242
+ def warmup(
243
+ *,
244
+ K: int = 128,
245
+ V: int = 256,
246
+ HV: int = 16,
247
+ dtype: torch.dtype = torch.bfloat16,
248
+ device: str | torch.device = "cuda",
249
+ use_qk_l2norm: bool = True,
250
+ use_gate_in_kernel: bool = True,
251
+ ) -> float:
252
+ """Compile every kernel this op needs, so training step 1 does not.
253
+
254
+ Four `cute.compile` calls plus fla's Triton autotuning is tens of seconds. Paid inside
255
+ step 1 it looks exactly like a regression — that is what the last port's reported
256
+ "-3% tokens/sec" turned out to be.
257
+
258
+ Compile keys carry no B, T or HV, so a tiny shape compiles the production kernels — but
259
+ it has to clear BOTH floors, the chain's (B*HV*(V//64) >= 256) and the intra kernel's
260
+ own (B*(T/64)*HV >= 1024). Miss the second and three of the four CuTe kernels compile
261
+ while the fourth quietly warms up its Triton fallback instead. Also runs the fla
262
+ compatibility probe, so a version mismatch raises here rather than at step 40,000.
263
+
264
+ KERNEL_FUN_KDA_MIN_CTAS earns a second pass. Raised, the first shape has to reach the
265
+ new floor or this call falls back and compiles nothing. LOWERED, it puts production on
266
+ a chain this shape never takes — the b1 scan and dhu keep their own 256 floors, so
267
+ below it they are fla's, and fla autotunes them at step 1 unless something warms them
268
+ here. Hence one pass per grid the process will actually dispatch at.
269
+
270
+ Returns the elapsed seconds — log it.
271
+ """
272
+ import time
273
+
274
+ from .._common.compat import check_fla
275
+ from ._kernels.bwd_intra import _MIN_CTAS as INTRA_MIN_CTAS
276
+
277
+ def one_pass(B: int) -> None:
278
+ kw = dict(device=device, dtype=dtype)
279
+ q = torch.randn(B, T, HV, K, **kw, requires_grad=True)
280
+ k = torch.randn(B, T, HV, K, **kw, requires_grad=True)
281
+ v = torch.randn(B, T, HV, V, **kw, requires_grad=True)
282
+ beta = torch.rand(B, T, HV, **kw, requires_grad=True)
283
+ A_log = dt_bias = None
284
+ if use_gate_in_kernel:
285
+ g = torch.randn(B, T, HV, K, **kw, requires_grad=True)
286
+ A_log = torch.rand(HV, device=device, dtype=torch.float32).add(1).log()
287
+ dt_bias = torch.zeros(HV * K, device=device, dtype=torch.float32)
288
+ else:
289
+ g = torch.nn.functional.logsigmoid(
290
+ torch.randn(B, T, HV, K, device=device, dtype=torch.float32)
291
+ ).requires_grad_(True)
292
+ o, ht = chunk_kda(
293
+ q, k, v, g, beta, initial_state=None, output_final_state=True,
294
+ use_qk_l2norm_in_kernel=use_qk_l2norm, use_gate_in_kernel=use_gate_in_kernel,
295
+ A_log=A_log, dt_bias=dt_bias,
296
+ )
297
+ (o.float().square().sum() + ht.float().square().sum()).backward()
298
+ torch.cuda.synchronize()
299
+
300
+ check_fla()
301
+ T = 1024
302
+ per_b = HV * max(V // 64, 1)
303
+ dispatch = support.min_ctas("kda")
304
+ # The compile pass: the four CuTe kernels, so it clears the default floor (two of them
305
+ # have their own 256) AND the intra kernel's, and a raised dispatch floor on top.
306
+ B = max(
307
+ -(-max(support.MIN_CTAS, dispatch) // per_b),
308
+ -(-INTRA_MIN_CTAS // (HV * (T // 64))),
309
+ 1,
310
+ )
311
+ grids = [B]
312
+ # The autotune pass, only for a lowered floor: the fla stages production is about to
313
+ # run there. Nothing NEW of ours compiles at this grid — the compile keys are the same
314
+ # — so what it costs is one small fwd+bwd and what it buys is fla's autotune.
315
+ if dispatch < support.MIN_CTAS:
316
+ B_low = max(-(-dispatch // per_b), 1)
317
+ if B_low < B:
318
+ grids.append(B_low)
319
+
320
+ t0 = time.perf_counter()
321
+ torch.manual_seed(0)
322
+ for b in grids:
323
+ one_pass(b)
324
+
325
+ # A warmup that silently compiled nothing is worse than none: it hides the cost it was
326
+ # supposed to move, and the run pays it at step 1 anyway.
327
+ from ._kernels import bwd_dhu, bwd_intra, bwd_scan, fwd_state
328
+
329
+ empty = [
330
+ m.__name__.rsplit(".", 1)[-1]
331
+ for m in (fwd_state, bwd_scan, bwd_dhu, bwd_intra)
332
+ if not getattr(m, "_COMPILE_CACHE", {})
333
+ ]
334
+ if empty:
335
+ raise RuntimeError(
336
+ f"kernel-fun kda warmup compiled nothing for {empty} — the shape "
337
+ f"(B={B} T={T} HV={HV} K={K} V={V}) did not reach those kernels, so the cost "
338
+ f"this call exists to move is still waiting in step 1. Most likely a CTA floor: "
339
+ f"the chain needs B*HV*(V//64) >= {support.MIN_CTAS} and the intra kernel needs "
340
+ f"B*(T/64)*HV >= {INTRA_MIN_CTAS}."
341
+ )
342
+ return time.perf_counter() - t0