Hakureirm commited on
Commit
021e09d
·
verified ·
1 Parent(s): 6896120

Upload build/torch-universal/rwkv7_kernel/__init__.py with huggingface_hub

Browse files
build/torch-universal/rwkv7_kernel/__init__.py ADDED
@@ -0,0 +1,527 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The RWKV team and The HuggingFace Inc. team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """RWKV-7 WKV kernel, Hub layout.
15
+
16
+ Complete-semantics entry point `rwkv7_wkv`: the single-token decode step runs a
17
+ Triton kernel that reads the recurrent state once per (batch, head) instead of
18
+ three times; every other shape -- prefill, packed rows, CPU, non-power-of-two
19
+ head widths -- takes the portable reference paths, which are carried verbatim
20
+ from the `transformers` RWKV-7 implementation (same author, same license) so
21
+ this package is self-contained and correct on any input.
22
+
23
+ Honest scope: the decode kernel's gain grows with batch size and is nil at
24
+ batch 1 -- at batch 1 the step is not bound by state reads. Prefill is the
25
+ reference chunk-parallel form, unaccelerated here.
26
+ """
27
+
28
+ import math
29
+
30
+ import torch
31
+
32
+ try:
33
+ import triton
34
+ import triton.language as tl
35
+
36
+ _HAVE_TRITON = True
37
+ except ImportError: # keep the module importable; the entry point falls back
38
+ _HAVE_TRITON = False
39
+
40
+
41
+ _INV_SQRT_E = 0.6065306597126334
42
+
43
+
44
+ # ---- portable reference paths (verbatim from transformers) ----
45
+
46
+
47
+ def rwkv7_recurrent(
48
+ r: torch.Tensor,
49
+ w_log: torch.Tensor,
50
+ k: torch.Tensor,
51
+ v: torch.Tensor,
52
+ kk: torch.Tensor,
53
+ a: torch.Tensor,
54
+ state: torch.Tensor,
55
+ compute_dtype: torch.dtype = torch.float32,
56
+ ) -> tuple[torch.Tensor, torch.Tensor]:
57
+ """Reference RWKV-7 WKV recurrence (generalised delta rule).
58
+
59
+ All inputs are `[batch, seq_len, num_heads, head_dim]`; `state` is
60
+ `[batch, num_heads, head_dim, head_dim]` and is carried across calls.
61
+
62
+ Per token, with `S` the per-head state and every product outer over the two
63
+ head_dim axes::
64
+
65
+ decay = exp(w_log)
66
+ sa = (-kk) @ S # uses the PRE-update state
67
+ S = decay * S + (kk * a) ⊗ sa + k ⊗ v
68
+ out = r @ S # uses the POST-update state
69
+
70
+ The state axes are (key, value); `S[i, j]` accumulates key channel `i` against
71
+ value channel `j`. Accumulation is fp32 regardless of the activation dtype:
72
+ the recurrence is unrolled over the whole sequence, so a lower-precision state
73
+ drifts. This is the portable path; a fused kernel may replace it as long as it
74
+ reproduces these values.
75
+ """
76
+ batch, seq_len, num_heads, head_dim = r.shape
77
+ dtype = r.dtype
78
+ r, w_log, k, v, kk, a = (t.to(compute_dtype) for t in (r, w_log, k, v, kk, a))
79
+ state = state.to(compute_dtype)
80
+ out = torch.empty(
81
+ batch, seq_len, num_heads, head_dim, device=r.device, dtype=compute_dtype
82
+ )
83
+
84
+ for t in range(seq_len):
85
+ decay = torch.exp(w_log[:, t])[..., None] # [B, H, K, 1]
86
+ kk_t = kk[:, t]
87
+ b = (kk_t * a[:, t])[..., None] # [B, H, K, 1]
88
+ # sa[b, h, j] = sum_i (-kk[b, h, i]) * S[b, h, i, j]
89
+ sa = torch.einsum("bhi,bhij->bhj", -kk_t, state)[:, :, None, :] # [B, H, 1, V]
90
+ state = decay * state + b * sa + k[:, t][..., None] * v[:, t][:, :, None, :]
91
+ out[:, t] = torch.einsum("bhi,bhij->bhj", r[:, t], state)
92
+
93
+ return out.to(dtype), state
94
+
95
+
96
+ def _unit_lower_triangular_inverse(
97
+ strict: torch.Tensor, block: int = 8
98
+ ) -> torch.Tensor:
99
+ """Inverse of a batch of unit lower triangular matrices `I + strict`.
100
+
101
+ `strict` is strictly lower triangular, so it is nilpotent and the Neumann series
102
+ `I - strict + strict^2 - ...` terminates -- which makes an inverse by repeated
103
+ squaring look like the obvious choice, and it is wrong here. The intermediate
104
+ powers are not bounded by the answer: on a real chunk from a 3-layer model at
105
+ T=1024, `strict` has entries at most 0.977 and the true inverse has entries at
106
+ most 1.0, but `strict^32` reaches 1.3e11. Summing a series whose terms exceed the
107
+ result by eleven orders of magnitude cancels away every digit float32 has -- both
108
+ Newton doubling and a plain Neumann sum came back with entries of 1e4 where the
109
+ answer is 1, and the NaNs that followed reached the states two layers on. It shows
110
+ up in a fifth of randomly initialised 3-layer models at T=1024, one in forty at
111
+ T=256 and none at all at T=128 or below: it needs several chunks to compound, so a
112
+ short-prompt test cannot see it however many initialisations it tries.
113
+
114
+ Block forward substitution never forms a high power. `block` bounds the largest
115
+ power taken, and the same chunk that breaks the series gives 6.6e-7 at block 8 and
116
+ 1.3e-7 at block 4, against float64 `linalg.solve_triangular`; 16 already costs a
117
+ digit and a half (1.9e-5) because the within-block series runs longer.
118
+
119
+ Random triangular matrices do not show any of this, which is worth stating because
120
+ it is what a test would reach for first: they agree with the series to 1e-6, since
121
+ their own inverses are as large as the intermediate powers. The cancellation needs
122
+ an inverse that stays near 1 while the powers do not, which is what the delta rule
123
+ produces and what a random matrix does not.
124
+
125
+ `linalg.solve_triangular` is the obvious call and is the accurate one, but it
126
+ decomposes for export into a graph carrying a scalar tensor constant, which
127
+ `lift_fresh` turns into `aten.alias`, functionalization turns into the in-place
128
+ `aten.detach_`, and aot_autograd rejects -- one of the two ops that made every ONNX
129
+ export subtest fail. Inverting once for every chunk at a time, rather than solving
130
+ inside the serial chunk loop, is what keeps that affordable; it is not free even
131
+ so. Measured against the per-chunk solve, loop included, on an RTX 5090 at 1.5B
132
+ shapes: 2.5x at T=1024 where 16 chunks cannot fill the card, falling to 1.2x at
133
+ T=4096, and flat in batch and head count. End to end on CPU, a T=1024 forward
134
+ runs about a third slower than it did on the solve.
135
+ """
136
+ span = strict.shape[-1]
137
+ block = min(block, span)
138
+ # Padding with zeros extends the matrix by an identity block, whose inverse is
139
+ # itself, so the leading span x span block of the result is unchanged.
140
+ pad = -span % block
141
+ if pad:
142
+ strict = torch.nn.functional.pad(strict, (0, pad, 0, pad))
143
+ width = span + pad
144
+ blocks = width // block
145
+
146
+ eye = torch.eye(block, device=strict.device, dtype=strict.dtype)
147
+ # Every diagonal block at once: [..., blocks, block, block].
148
+ diag = strict.reshape(*strict.shape[:-2], blocks, block, blocks, block)
149
+ diag = diag.diagonal(dim1=-4, dim2=-2).movedim(-1, -3)
150
+ # Exact after `block` terms, and `block` is small enough that the powers stay near
151
+ # the answer -- which is the whole point of the blocking.
152
+ inverse, term = eye - diag, -diag
153
+ for _ in range(block - 2):
154
+ term = -(diag @ term)
155
+ inverse = inverse + term
156
+
157
+ # `out` grows one block row at a time and is the inverse of the leading principal
158
+ # submatrix throughout, so the substitution reads exactly the columns it has
159
+ # already filled and never multiplies the zeros above the diagonal.
160
+ out = inverse[..., 0, :, :]
161
+ for i in range(1, blocks):
162
+ start, stop = i * block, (i + 1) * block
163
+ d = inverse[..., i, :, :]
164
+ row = torch.cat([-d @ (strict[..., start:stop, :start] @ out), d], dim=-1)
165
+ out = torch.cat([torch.nn.functional.pad(out, (0, block)), row], dim=-2)
166
+ return out[..., :span, :span] if pad else out
167
+
168
+
169
+ def rwkv7_chunked(
170
+ r: torch.Tensor,
171
+ w_log: torch.Tensor,
172
+ k: torch.Tensor,
173
+ v: torch.Tensor,
174
+ kk: torch.Tensor,
175
+ a: torch.Tensor,
176
+ state: torch.Tensor,
177
+ chunk_size: int = 64,
178
+ compute_dtype: torch.dtype = torch.float32,
179
+ ) -> tuple[torch.Tensor, torch.Tensor]:
180
+ """Chunk-parallel form of [`rwkv7_recurrent`], for sequences.
181
+
182
+ The step is `S_t = A_t S_{t-1} + k_t v_t^T` with `A_t = diag(w_t) - b_t kk_t^T`
183
+ and `b_t = kk_t * a_t`, i.e. diagonal-plus-rank-one. Substituting
184
+ `S_t = diag(c_t) P_t` with the running decay `c_t = prod_{s<=t} w_s` removes the
185
+ diagonal part and leaves a plain delta rule::
186
+
187
+ P_t = (I - b~_t q~_t^T) P_{t-1} + k~_t v_t^T
188
+ b~ = b / c, k~ = k / c, q~ = kk * c_{t-1}, r~ = r * c
189
+
190
+ Writing `u_t = q~_t^T P_{t-1}` turns a whole chunk into one unit-lower-triangular
191
+ system, so the chunk is a handful of matmuls instead of `chunk_size` sequential
192
+ steps; only the chunk-to-chunk carry stays serial::
193
+
194
+ (I + tril(Q~ B~^T, -1)) U = Q~ P_0 + tril(Q~ K~^T, -1) V
195
+ O = R~ P_0 + tril(R~ K~^T, 0) V - tril(R~ B~^T, 0) U
196
+ P_C = P_0 + K~^T V - B~^T U
197
+
198
+ `chunk_size` is bounded by that division by `c`, but by OVERFLOW rather than by
199
+ precision, and the difference is worth a factor of four. The per-step decay is at
200
+ least `exp(-e^-0.5)` = 0.5452, so at the worst case, every channel pinned at that
201
+ floor, `1/c` grows like `e^(e^-0.5 * chunk_size)`. That reaches 7.2e16 at 64 and
202
+ fp32 tops out near 3.4e38, so there are twenty-odd decades of headroom left at the
203
+ default, and the ceiling is `ln(finfo.max) / e^-0.5` = 146 in fp32.
204
+
205
+ That derivation is worth stating carefully because the first version of it was
206
+ wrong in a way the numbers hid. It said the decay floor was `e^-0.5` rather than
207
+ `exp(-e^-0.5)`, hence growth like `e^(0.5 * chunk_size)`, which puts the ceiling at
208
+ 177, and measured at the decay floor this function returns all-NaN from 147 up. The
209
+ quoted 7.2e16 came from the correct law all along (`e^(0.5*64)` is 7.9e13), so the
210
+ arithmetic had been done right and written up wrong, and following the prose rather
211
+ than the number led into the overflow band. `chunk_size` is checked below rather
212
+ than left to that reasoning.
213
+
214
+ Precision does not degrade with it, because the substitution is a similarity
215
+ transform: whatever `1/c` inflates, `c` deflates again on the way out, and a
216
+ common scale factor does not move a floating-point relative error. Measured at
217
+ the decay floor, `T=256`, against the sequential form: chunk 16 gives 2.0e-07 and
218
+ chunk 64 gives 2.7e-07, both fp32 noise, while the recurrence runs 3.9x faster
219
+ (3.771 ms -> 0.913 ms). 16 was the conservative reading of the same bound.
220
+ """
221
+ batch, seq_len, num_heads, head_dim = r.shape
222
+ dtype = r.dtype
223
+ r, w_log, k, v, kk, a = (t.to(compute_dtype) for t in (r, w_log, k, v, kk, a))
224
+ state = state.to(compute_dtype)
225
+ # Everything that does not touch the carried state is computed for ALL chunks at
226
+ # once. Only the state recurrence is serial, and it was pulling the rest of the
227
+ # arithmetic into the Python loop with it: at T=256 and chunk 64 that was four
228
+ # iterations of ten-odd launches each, plus three constant matrices rebuilt every
229
+ # iteration. The loop below now does the state-dependent terms and nothing else.
230
+ # Never pad past the sequence: a 16-token prefill grouped into 64-wide chunks does
231
+ # four times the arithmetic for the same answer, which cost 1x16 nine points before
232
+ # this line existed. The old loop avoided it by shortening its last chunk.
233
+ chunk_size = min(chunk_size, seq_len)
234
+ # Refuse rather than return NaN. `1/c` reaches `e^(e^-0.5 * chunk_size)` at the
235
+ # decay floor, so the widest chunk this dtype can carry is derived from the dtype
236
+ # rather than written down: 146 in fp32, 12 in fp16. A caller who raises
237
+ # `chunk_size` for a longer prefill gets an error naming the limit instead of
238
+ # all-NaN logits several layers later, which is what the previous version did.
239
+ widest = int(math.log(torch.finfo(compute_dtype).max) / _INV_SQRT_E)
240
+ if chunk_size > widest:
241
+ raise ValueError(
242
+ f"chunk_size={chunk_size} overflows {compute_dtype}: the running decay's "
243
+ f"reciprocal grows like e^(e^-0.5 * chunk_size), so the widest chunk that "
244
+ f"stays finite is {widest}. Lower chunk_size, or raise compute_dtype."
245
+ )
246
+ chunks = (seq_len + chunk_size - 1) // chunk_size
247
+ padded = chunks * chunk_size
248
+ if padded != seq_len:
249
+ pad = (0, 0, 0, 0, 0, padded - seq_len)
250
+ r, k, v, kk, a = (torch.nn.functional.pad(t, pad) for t in (r, k, v, kk, a))
251
+ # A pad step must be the identity for the recurrence: decay 1, nothing added.
252
+ w_log = torch.nn.functional.pad(w_log, pad)
253
+ grouped = lambda t: t.reshape(batch, chunks, chunk_size, num_heads, head_dim)
254
+ rg, kg, vg, kkg, ag, wg = (grouped(t) for t in (r, k, v, kk, a, w_log))
255
+
256
+ # `cumprod(exp(x))` and `exp(cumsum(x))` are the same function, and the running
257
+ # decay is only ever needed in the exponentiated form, so the sum is taken first.
258
+ # Two reasons to prefer it. Numerically, a cumulative product of up-to-1 factors
259
+ # underflows the further it runs while the equivalent sum of logs does not, and
260
+ # `c_prev` becomes a subtraction instead of a division by a possibly-tiny `w_c`.
261
+ # For export, `aten.cumprod` decomposes into a graph carrying a scalar tensor
262
+ # constant; `aten.lift_fresh` on that constant decomposes to `aten.alias`, which
263
+ # functionalization rewrites to the in-place `aten.detach_`, and aot_autograd
264
+ # rejects the result -- which is what made every ONNX export subtest fail.
265
+ cum = torch.cumsum(wg, dim=2)
266
+ c = torch.exp(cum)
267
+ c_prev = torch.exp(cum - wg)
268
+ bg = kkg * ag
269
+ b_t, k_t = bg / c, kg / c
270
+ q_t, r_t = kkg * c_prev, rg * c
271
+
272
+ span = chunk_size
273
+ tri = torch.ones(span, span, device=r.device, dtype=r.dtype).tril(-1)
274
+ causal = torch.ones(span, span, device=r.device, dtype=r.dtype).tril(0)
275
+
276
+ # [batch, chunks, heads, span, span] -- one launch each instead of one per chunk.
277
+ qb = torch.einsum("bcthn,bcshn->bchts", q_t, b_t) * tri
278
+ qk = torch.einsum("bcthn,bcshn->bchts", q_t, k_t) * tri
279
+ rk = torch.einsum("bcthn,bcshn->bchts", r_t, k_t) * causal
280
+ rb = torch.einsum("bcthn,bcshn->bchts", r_t, b_t) * causal
281
+ lhs_inv = _unit_lower_triangular_inverse(qb)
282
+ qkv = torch.einsum("bchts,bcshv->bchtv", qk, vg)
283
+ rkv = torch.einsum("bchts,bcshv->bchtv", rk, vg)
284
+ c_last = c[:, :, -1].unsqueeze(-1)
285
+
286
+ # `k~^T v` carries no state either, so it joins the batched half above; only the
287
+ # two products that read the carried state and the solve that depends on them are
288
+ # genuinely serial.
289
+ kv = torch.einsum("bcthn,bcthv->bchnv", k_t, vg)
290
+ # Both state products read the SAME state, so they are one matmul over a stacked
291
+ # token axis rather than two -- the loop is short and launch-bound, and this is one
292
+ # launch per chunk instead of two.
293
+ qr = torch.cat([q_t, r_t], dim=2)
294
+
295
+ outputs = []
296
+ for i in range(chunks):
297
+ qr_s = torch.einsum("bthn,bhnv->bhtv", qr[:, i], state)
298
+ rhs = qr_s[:, :, :chunk_size] + qkv[:, i]
299
+ u = lhs_inv[:, i] @ rhs
300
+ out_c = (
301
+ qr_s[:, :, chunk_size:]
302
+ + rkv[:, i]
303
+ - torch.einsum("bhts,bhsv->bhtv", rb[:, i], u)
304
+ )
305
+ outputs.append(out_c.permute(0, 2, 1, 3))
306
+ state = c_last[:, i] * (
307
+ state + kv[:, i] - torch.einsum("bthn,bhtv->bhnv", b_t[:, i], u)
308
+ )
309
+
310
+ # The chunked loop pads the sequence up to a multiple of the chunk size and
311
+ # computes in a wider dtype, so both the trailing slice and the cast back are
312
+ # no-ops whenever the input needed neither. A no-op slice or cast still returns
313
+ # a view, ONNX export turns that view into `aten.alias`, functionalization
314
+ # rewrites the alias to the in-place `aten.detach_`, and aot_autograd rejects
315
+ # the graph. Doing each step only when it changes something keeps the common
316
+ # path free of views without changing any value.
317
+ out = torch.cat(outputs, dim=1)
318
+ if out.shape[1] != seq_len:
319
+ out = out[:, :seq_len]
320
+ if out.dtype != dtype:
321
+ out = out.to(dtype)
322
+ return out, state
323
+
324
+
325
+ def rwkv7_eager(
326
+ r: torch.Tensor,
327
+ w_log: torch.Tensor,
328
+ k: torch.Tensor,
329
+ v: torch.Tensor,
330
+ kk: torch.Tensor,
331
+ a: torch.Tensor,
332
+ state: torch.Tensor,
333
+ cu_seq_lens_q: torch.Tensor | None = None,
334
+ **kwargs,
335
+ ) -> tuple[torch.Tensor, torch.Tensor]:
336
+ """Portable WKV: the sequential step for a single token, chunk-parallel otherwise.
337
+
338
+ With `cu_seq_lens_q` the row is a *packed* batch: several independent sequences
339
+ laid end to end, the layout a varlen kernel consumes. Each one has to start
340
+ from a fresh state, so they are run in turn and their outputs concatenated;
341
+ the state returned is the last segment's, which is the one a continuation
342
+ would resume from. This is the reference behaviour, not the fast path: a
343
+ fused varlen kernel does the same segments in one launch, which is the shape
344
+ of optimised implementation the hub-kernels mechanism can substitute in.
345
+ """
346
+ if cu_seq_lens_q is not None:
347
+ bounds = cu_seq_lens_q.tolist()
348
+ outputs = []
349
+ for start, stop in zip(bounds[:-1], bounds[1:]):
350
+ if stop <= start:
351
+ continue
352
+ segment_state = torch.zeros_like(state)
353
+ out, state = rwkv7_eager(
354
+ r[:, start:stop],
355
+ w_log[:, start:stop],
356
+ k[:, start:stop],
357
+ v[:, start:stop],
358
+ kk[:, start:stop],
359
+ a[:, start:stop],
360
+ segment_state,
361
+ )
362
+ outputs.append(out)
363
+ return torch.cat(outputs, dim=1), state
364
+
365
+ kernel = rwkv7_recurrent if r.shape[1] == 1 else rwkv7_chunked
366
+ return kernel(r, w_log, k, v, kk, a, state)
367
+
368
+
369
+ # ---- Triton decode specialisation (defined only when Triton imports) ----
370
+
371
+ if _HAVE_TRITON:
372
+
373
+ @triton.jit
374
+ def _wkv_one_kernel(
375
+ state_ptr,
376
+ r_ptr,
377
+ w_ptr,
378
+ k_ptr,
379
+ v_ptr,
380
+ kk_ptr,
381
+ a_ptr,
382
+ out_ptr,
383
+ stride_sb,
384
+ stride_sh,
385
+ stride_si,
386
+ stride_sj,
387
+ stride_vb,
388
+ stride_vh,
389
+ N: tl.constexpr,
390
+ ):
391
+ """One program per (batch, head).
392
+
393
+ `N` is `head_dim` and is a compile-time constant, so the `[N, N]` tile lives in
394
+ registers for the whole body: 64x64 fp32 is 16 KB, which is what makes reading
395
+ the state once sufficient.
396
+ """
397
+ batch = tl.program_id(0)
398
+ head = tl.program_id(1)
399
+
400
+ i = tl.arange(0, N)
401
+ j = tl.arange(0, N)
402
+ vec = batch * stride_vb + head * stride_vh
403
+ r = tl.load(r_ptr + vec + i).to(tl.float32)
404
+ w = tl.load(w_ptr + vec + i).to(tl.float32)
405
+ k = tl.load(k_ptr + vec + i).to(tl.float32)
406
+ v = tl.load(v_ptr + vec + j).to(tl.float32)
407
+ kk = tl.load(kk_ptr + vec + i).to(tl.float32)
408
+ a = tl.load(a_ptr + vec + i).to(tl.float32)
409
+
410
+ # Every axis of the state is strided, the last one included. Hardcoding unit
411
+ # stride there reads the tile transposed when the caller hands over anything
412
+ # that is not contiguous, and the result is wrong by 80-98% with nothing
413
+ # raised: the shape is right, so neither the kernel nor Triton notices.
414
+ off = (
415
+ batch * stride_sb
416
+ + head * stride_sh
417
+ + i[:, None] * stride_si
418
+ + j[None, :] * stride_sj
419
+ )
420
+ state = tl.load(state_ptr + off).to(tl.float32)
421
+
422
+ # Same three lines as the reference, in the same order and in fp32: `sa` uses the
423
+ # PRE-update state and the output uses the POST-update one.
424
+ sa = tl.sum((-kk)[:, None] * state, axis=0)
425
+ state = (
426
+ tl.exp(w)[:, None] * state
427
+ + (kk * a)[:, None] * sa[None, :]
428
+ + k[:, None] * v[None, :]
429
+ )
430
+ out = tl.sum(r[:, None] * state, axis=0)
431
+
432
+ tl.store(state_ptr + off, state.to(state_ptr.dtype.element_ty))
433
+ tl.store(out_ptr + vec + j, out.to(out_ptr.dtype.element_ty))
434
+
435
+ def fused_wkv_one(
436
+ r: torch.Tensor,
437
+ w_log: torch.Tensor,
438
+ k: torch.Tensor,
439
+ v: torch.Tensor,
440
+ kk: torch.Tensor,
441
+ a: torch.Tensor,
442
+ state: torch.Tensor,
443
+ ) -> torch.Tensor:
444
+ batch, _, heads, head_dim = r.shape
445
+ # The grid comes from `r`, so a state that disagrees with it is read at offsets
446
+ # belonging to another row and returns a plausible answer for the wrong sequence.
447
+ # Checked rather than assumed: the kernel cannot tell, and neither can the caller.
448
+ if tuple(state.shape) != (batch, heads, head_dim, head_dim):
449
+ raise ValueError(
450
+ f"state has shape {tuple(state.shape)}, but the vectors imply {(batch, heads, head_dim, head_dim)}"
451
+ )
452
+ out = torch.empty(batch, heads, head_dim, device=r.device, dtype=r.dtype)
453
+ flat = [
454
+ t.reshape(batch, heads, head_dim).contiguous()
455
+ for t in (r, w_log, k, v, kk, a)
456
+ ]
457
+ _wkv_one_kernel[(batch, heads)](
458
+ state,
459
+ *flat,
460
+ out,
461
+ state.stride(0),
462
+ state.stride(1),
463
+ state.stride(2),
464
+ state.stride(3),
465
+ out.stride(0),
466
+ out.stride(1),
467
+ N=head_dim,
468
+ )
469
+ return out.view(batch, 1, heads, head_dim)
470
+
471
+ else:
472
+ fused_wkv_one = None
473
+
474
+
475
+ def _use_kernel(r, cu_seq_lens) -> bool:
476
+ """Whether the fused step applies, as a predicate rather than inline conditions.
477
+
478
+ Separated out because it is the part worth testing and the part that cannot be
479
+ tested through `rwkv7_fused` on a machine without CUDA: there every case falls back
480
+ for the same reason, so a parametrised test over prefill/packed/width passes even
481
+ with the other conditions deleted. Checked that by deleting them.
482
+
483
+ `head_dim` a power of two is part of the kernel's applicability domain, not a
484
+ detail: `tl.arange` requires it, and without that term a width like 192 reached
485
+ Triton and came back as a CompilationError raised from inside the kernel rather
486
+ than as a fallback.
487
+ """
488
+ width = r.shape[-1]
489
+ return bool(
490
+ r.shape[1] == 1 # decode only; prefill has its own chunked form
491
+ and cu_seq_lens is None # a packed row is many sequences, not one step
492
+ and r.is_cuda
493
+ and _HAVE_TRITON
494
+ and width & (width - 1) == 0
495
+ )
496
+
497
+
498
+ def rwkv7_wkv(
499
+ r: torch.Tensor,
500
+ w_log: torch.Tensor,
501
+ k: torch.Tensor,
502
+ v: torch.Tensor,
503
+ kk: torch.Tensor,
504
+ a: torch.Tensor,
505
+ state: torch.Tensor,
506
+ cu_seq_lens_q: torch.Tensor | None = None,
507
+ **kwargs,
508
+ ) -> tuple[torch.Tensor, torch.Tensor]:
509
+ """The WKV recurrence with the decode step fused; reference semantics everywhere.
510
+
511
+ Drop-in for the `transformers` RWKV-7 `rwkv7_eager` contract: same tensor
512
+ layout ([batch, seq_len, num_heads, head_dim] vectors, [batch, num_heads,
513
+ head_dim, head_dim] state), same `(output, new_state)` return, `cu_seq_lens_q`
514
+ packed rows honoured by the reference path.
515
+ """
516
+ if _use_kernel(r, cu_seq_lens_q):
517
+ return fused_wkv_one(r, w_log, k, v, kk, a, state), state
518
+ return rwkv7_eager(r, w_log, k, v, kk, a, state, cu_seq_lens_q=cu_seq_lens_q)
519
+
520
+
521
+ __all__ = [
522
+ "fused_wkv_one",
523
+ "rwkv7_chunked",
524
+ "rwkv7_eager",
525
+ "rwkv7_recurrent",
526
+ "rwkv7_wkv",
527
+ ]