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 |
+
]
|