Files
vLLM/tests/models/glm5next/test_kda_recurrent.py
T
2026-09-21 04:16:26 -05:00

196 lines
7.3 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""GLM-5.3-Flash KDA recurrent (decode) kernel.
The decode path hands the kernel column slices of the merged ``q|k|v`` conv
output and of the fused ``qkvbfg_a`` projection (beta), so q/k/v/beta are
token-strided rather than contiguous. Both backends must match a pure-PyTorch
recurrence. CUDA reads these slices in place and rejects unsupported layouts;
the ROCm wrapper makes contiguous copies before launching its kernel.
"""
import pytest
import torch
from vllm.platforms import current_platform
if current_platform.is_rocm():
from vllm.models.glm5next.amd.ops.third_party.kda import fused_recurrent_kda
else:
from vllm.models.glm5next.nvidia.ops.third_party.kda import fused_recurrent_kda
pytestmark = pytest.mark.skipif(
not current_platform.is_cuda_alike(), reason="Requires CUDA or ROCm"
)
H, D = 16, 128
LOWER_BOUND = -5.0
def naive_recurrent_kda(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
raw_g: torch.Tensor,
raw_beta: torch.Tensor,
a_log: torch.Tensor,
g_bias: torch.Tensor,
state: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
"""fp32 reference for one sequence: ``[T, H, D]`` inputs, ``[H, D, D]``
(v-major) state; mirrors the kernel's in-kernel gate, beta sigmoid and
q/k l2norm.
"""
q, k, v, raw_g, raw_beta = (x.float() for x in (q, k, v, raw_g, raw_beta))
q = q / torch.sqrt(q.square().sum(-1, keepdim=True) + 1e-6) * D**-0.5
k = k / torch.sqrt(k.square().sum(-1, keepdim=True) + 1e-6)
gate = LOWER_BOUND * torch.sigmoid(a_log.exp()[:, None] * (raw_g + g_bias))
beta = torch.sigmoid(raw_beta)
s = state.clone()
out = torch.empty_like(v)
for t in range(q.shape[0]):
s = s * gate[t].exp()[:, None, :]
u = beta[t][:, None] * (v[t] - torch.einsum("hvk,hk->hv", s, k[t]))
s = s + u[:, :, None] * k[t][:, None, :]
out[t] = torch.einsum("hvk,hk->hv", s, q[t])
return out, s
def make_inputs(num_seqs: int, query_len: int, device: torch.device):
"""Token-strided q/k/v/beta as the decode path produces them: column
slices of a merged ``[T, q|k|v]`` conv output and of the fused
``[T, qkv|beta|f_a|g_a]`` projection.
"""
T, proj = num_seqs * query_len, H * D
qkv = torch.randn(T, 3 * proj, dtype=torch.bfloat16, device=device)
projected = torch.randn(
T, 3 * proj + H + 2 * D, dtype=torch.bfloat16, device=device
)
q, k, v = (x.view(1, T, H, D) for x in qkv.split(proj, dim=-1))
beta = projected[:, 3 * proj : 3 * proj + H].unsqueeze(0)
# (A size-1 token dim gets an arbitrary stride from `view`.)
assert T == 1 or (q.stride(1) == 3 * proj and beta.stride(1) == projected.stride(0))
inputs = dict(
q=q,
k=k,
v=v,
g=torch.randn(1, T, H, D, dtype=torch.bfloat16, device=device),
beta=beta,
a_log=0.5 * torch.randn(H, dtype=torch.float32, device=device),
g_bias=0.1 * torch.randn(H * D, dtype=torch.float32, device=device),
cu_seqlens=torch.arange(0, T + 1, query_len, dtype=torch.int32, device=device),
)
# Slot 0 is NULL_BLOCK_ID; sequences own random distinct slots (one per
# token in the spec-decode layout).
slots = torch.randperm(T, device=device).to(torch.int32) + 1
if query_len == 1:
inputs["ssm_state_indices"] = slots
else:
inputs["ssm_state_indices"] = slots.view(num_seqs, query_len)
inputs["num_accepted_tokens"] = torch.randint(
1, query_len + 1, (num_seqs,), dtype=torch.int32, device=device
)
state = torch.randn(T + 1, H, D, D, dtype=torch.float32, device=device)
return inputs, state
def run_kernel(inputs: dict, state: torch.Tensor) -> torch.Tensor:
out, _ = fused_recurrent_kda(
**inputs,
initial_state=state,
use_qk_l2norm_in_kernel=True,
sigmoid_beta=True,
compute_gate=True,
lower_bound=LOWER_BOUND,
)
return out
@pytest.mark.parametrize(
("num_seqs", "query_len"), [(1, 1), (7, 1), (3, 3)], ids=["1x1", "7x1", "3x3"]
)
@torch.inference_mode()
def test_fused_recurrent_kda_matches_reference(num_seqs: int, query_len: int):
torch.manual_seed(0)
device = torch.device("cuda")
inputs, state = make_inputs(num_seqs, query_len, device)
expected_state = state.clone()
out = run_kernel(inputs, state)
indices = inputs["ssm_state_indices"].view(num_seqs, query_len)
accepted = inputs.get("num_accepted_tokens")
expected = torch.empty_like(out[0], dtype=torch.float32)
for n in range(num_seqs):
first = indices[n, 0 if accepted is None else accepted[n] - 1]
s = expected_state[first]
for t in range(query_len):
tok = slice(n * query_len + t, n * query_len + t + 1)
expected[tok], s = naive_recurrent_kda(
inputs["q"][0, tok],
inputs["k"][0, tok],
inputs["v"][0, tok],
inputs["g"][0, tok],
inputs["beta"][0, tok],
inputs["a_log"],
inputs["g_bias"].view(H, D),
s,
)
expected_state[indices[n, t]] = s
torch.testing.assert_close(out[0].float(), expected, rtol=1e-2, atol=1e-3)
used = indices.flatten().long()
torch.testing.assert_close(state[used], expected_state[used], rtol=1e-5, atol=1e-5)
@pytest.mark.parametrize(
("num_seqs", "query_len"), [(7, 1), (3, 3)], ids=["7x1", "3x3"]
)
@torch.inference_mode()
def test_fused_recurrent_kda_strided_inputs_bit_identical_to_contiguous(
num_seqs: int, query_len: int
):
torch.manual_seed(0)
device = torch.device("cuda")
inputs, state = make_inputs(num_seqs, query_len, device)
for name in ("q", "k", "v", "beta"):
assert not inputs[name].is_contiguous()
contiguous = {
name: x.contiguous() if name in ("q", "k", "v", "beta") else x
for name, x in inputs.items()
}
state_ref = state.clone()
out_ref = run_kernel(contiguous, state_ref)
out = run_kernel(inputs, state)
torch.testing.assert_close(out, out_ref, rtol=0, atol=0)
torch.testing.assert_close(state, state_ref, rtol=0, atol=0)
@pytest.mark.skipif(
not current_platform.is_cuda(),
reason="Only the CUDA implementation requires zero-copy token-strided layouts",
)
@torch.inference_mode()
def test_fused_recurrent_kda_rejects_unaddressable_layouts():
"""Layouts the token-stride addressing cannot express must fail loudly
rather than read the wrong tokens: a batch slice of a wider buffer
(``stride(0) != T * stride(1)``), overlapping tokens, and a head-strided
(transposed) block.
"""
torch.manual_seed(0)
device = torch.device("cuda")
inputs, state = make_inputs(1, 1, device)
T = 4
base = torch.randn(4, T, 2 * H * D, dtype=torch.bfloat16, device=device)
bad_q = {
"batch slice": base[::2, :, : H * D].view(2, T, H, D),
"overlapping tokens": base[:1, :, : H * D].as_strided(
(1, T, H, D), (0, D, D, 1)
),
"head-strided": base[:1, :, : H * D].view(1, T, D, H).transpose(2, 3),
}
for q in bad_q.values():
broken = dict(inputs, q=q, k=q, v=q)
broken["cu_seqlens"] = None if q.shape[0] > 1 else inputs["cu_seqlens"]
with pytest.raises(AssertionError, match=r"torch.Size"):
run_kernel(broken, state)