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
|
@@ -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
|
+
)
|
kernel_fun/kda/chain.py
ADDED
|
@@ -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
|