[GDN] Support GDN packed decode (#20627)

This commit is contained in:
Yuan Luo
2026-03-18 13:20:07 +08:00
committed by GitHub
parent 4cc19862ef
commit 9c87e137ee
4 changed files with 829 additions and 2 deletions

View File

@@ -0,0 +1,488 @@
"""
Benchmark & Correctness: GDN Packed Decode vs Baseline Decode.
Compares:
- Baseline: split(mixed_qkv) → view → fused_sigmoid_gating_delta_rule_update
- Packed: fused_recurrent_gated_delta_rule_packed_decode (single kernel)
The packed path eliminates:
- torch.split() + .view() tensor materialization
- Separate gating kernel launches
- Intermediate tensor allocations
Reports correctness (output & state matching) and performance (ms, speedup).
Usage:
python bench_gdn_decode.py # default sweep
python bench_gdn_decode.py --mode bench # benchmark only
python bench_gdn_decode.py --mode correctness # correctness only
python bench_gdn_decode.py --preset qwen3.5-35b # Qwen3.5-35B-A3B config
"""
import argparse
import os
import sys
import time
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "python"))
import torch
import triton
from sglang.srt.layers.attention.fla.fused_recurrent import (
fused_recurrent_gated_delta_rule_packed_decode,
)
from sglang.srt.layers.attention.fla.fused_sigmoid_gating_recurrent import (
fused_sigmoid_gating_delta_rule_update,
)
# ---------------------------------------------------------------------------
# Input factory
# ---------------------------------------------------------------------------
def make_inputs(
B: int,
H: int,
HV: int,
K: int,
V: int,
pool_size: int,
device: str,
dtype: torch.dtype,
seed: int = 42,
):
"""Create all input tensors for a single benchmark / correctness run."""
torch.manual_seed(seed)
qkv_dim = 2 * H * K + HV * V
mixed_qkv = torch.randn(B, qkv_dim, device=device, dtype=dtype)
a = torch.randn(B, HV, device=device, dtype=dtype)
b = torch.randn(B, HV, device=device, dtype=dtype)
A_log = torch.randn(HV, device=device, dtype=dtype)
dt_bias = torch.randn(HV, device=device, dtype=dtype)
ssm_states = torch.randn(pool_size, HV, V, K, device=device, dtype=dtype) * 0.1
cache_indices = torch.arange(B, device=device, dtype=torch.int32)
cu_seqlens = torch.arange(B + 1, device=device, dtype=torch.long)
return dict(
B=B,
H=H,
HV=HV,
K=K,
V=V,
qkv_dim=qkv_dim,
pool_size=pool_size,
mixed_qkv=mixed_qkv,
a=a,
b=b,
A_log=A_log,
dt_bias=dt_bias,
ssm_states=ssm_states,
cache_indices=cache_indices,
cu_seqlens=cu_seqlens,
)
# ---------------------------------------------------------------------------
# Runner wrappers
# ---------------------------------------------------------------------------
def run_baseline(inp):
"""Baseline path: split → view → fused_sigmoid_gating_delta_rule_update.
This mirrors the FULL original decode path in GDNAttnBackend.forward_decode,
including the split, view, and kernel call.
"""
B, H, HV, K, V = inp["B"], inp["H"], inp["HV"], inp["K"], inp["V"]
mixed_qkv = inp["mixed_qkv"]
ssm_states = inp["ssm_states"].clone()
# Step 1: split (same as forward_decode)
q_flat, k_flat, v_flat = torch.split(mixed_qkv, [H * K, H * K, HV * V], dim=-1)
# Step 2: view + reshape (same as forward_decode)
q = q_flat.view(1, B, H, K)
k = k_flat.view(1, B, H, K)
v = v_flat.view(1, B, HV, V)
# Step 3: fused gating + recurrent update
o = fused_sigmoid_gating_delta_rule_update(
A_log=inp["A_log"],
dt_bias=inp["dt_bias"],
q=q,
k=k,
v=v,
a=inp["a"],
b=inp["b"],
initial_state_source=ssm_states,
initial_state_indices=inp["cache_indices"],
cu_seqlens=inp["cu_seqlens"],
use_qk_l2norm_in_kernel=True,
softplus_beta=1.0,
softplus_threshold=20.0,
)
return o, ssm_states
def run_packed(inp):
"""Packed path: single fused kernel directly on mixed_qkv."""
B, HV, K, V = inp["B"], inp["HV"], inp["K"], inp["V"]
ssm_states = inp["ssm_states"].clone()
out = inp["mixed_qkv"].new_empty(B, 1, HV, V)
fused_recurrent_gated_delta_rule_packed_decode(
mixed_qkv=inp["mixed_qkv"],
a=inp["a"],
b=inp["b"],
A_log=inp["A_log"],
dt_bias=inp["dt_bias"],
scale=inp["K"] ** -0.5,
initial_state=ssm_states,
out=out,
ssm_state_indices=inp["cache_indices"],
use_qk_l2norm_in_kernel=True,
)
# Convert [B, 1, HV, V] → [1, B, HV, V] to match baseline layout
return out.transpose(0, 1), ssm_states
# ---------------------------------------------------------------------------
# Correctness check
# ---------------------------------------------------------------------------
def check_correctness(B, H, HV, K, V, pool_size, device, dtype, seed=42):
"""Run correctness check for a single config. Returns True if PASS."""
tag = f"B={B:>4} H={H:>2} HV={HV:>2} K={K:>3} V={V:>3} pool={pool_size:>4}"
inp = make_inputs(B, H, HV, K, V, pool_size, device, dtype, seed=seed)
o_baseline, state_baseline = run_baseline(inp)
o_packed, state_packed = run_packed(inp)
# Output comparison
atol = 2e-2 if dtype != torch.float32 else 1e-4
rtol = 1e-2 if dtype != torch.float32 else 1e-4
try:
torch.testing.assert_close(o_packed, o_baseline, atol=atol, rtol=rtol)
output_ok = True
except AssertionError as e:
output_ok = False
out_diff = (o_packed - o_baseline).abs().max().item()
# State comparison (only for slots that were updated)
indices = inp["cache_indices"]
try:
torch.testing.assert_close(
state_packed[indices], state_baseline[indices], atol=atol, rtol=rtol
)
state_ok = True
except AssertionError:
state_ok = False
st_diff = (state_packed[indices] - state_baseline[indices]).abs().max().item()
passed = output_ok and state_ok
if passed:
print(f" [PASS] {tag}")
else:
details = []
if not output_ok:
details.append(f"output max_diff={out_diff:.6f}")
if not state_ok:
details.append(f"state max_diff={st_diff:.6f}")
print(f" [FAIL] {tag} ({', '.join(details)})")
return passed
# ---------------------------------------------------------------------------
# Benchmark
# ---------------------------------------------------------------------------
def bench_shape(B, H, HV, K, V, pool_size, device, dtype):
"""Benchmark baseline vs packed for a single config."""
inp = make_inputs(B, H, HV, K, V, pool_size, device, dtype)
# ── Baseline: full path including split + view ──
def fn_baseline():
q_flat, k_flat, v_flat = torch.split(
inp["mixed_qkv"], [H * K, H * K, HV * V], dim=-1
)
q = q_flat.view(1, B, H, K)
k = k_flat.view(1, B, H, K)
v = v_flat.view(1, B, HV, V)
fused_sigmoid_gating_delta_rule_update(
A_log=inp["A_log"],
dt_bias=inp["dt_bias"],
q=q,
k=k,
v=v,
a=inp["a"],
b=inp["b"],
initial_state_source=inp["ssm_states"],
initial_state_indices=inp["cache_indices"],
cu_seqlens=inp["cu_seqlens"],
use_qk_l2norm_in_kernel=True,
softplus_beta=1.0,
softplus_threshold=20.0,
)
# ── Packed: single kernel ──
out_buf = inp["mixed_qkv"].new_empty(B, 1, HV, V)
def fn_packed():
fused_recurrent_gated_delta_rule_packed_decode(
mixed_qkv=inp["mixed_qkv"],
a=inp["a"],
b=inp["b"],
A_log=inp["A_log"],
dt_bias=inp["dt_bias"],
scale=K**-0.5,
initial_state=inp["ssm_states"],
out=out_buf,
ssm_state_indices=inp["cache_indices"],
use_qk_l2norm_in_kernel=True,
)
# Warmup
for _ in range(10):
fn_baseline()
fn_packed()
torch.cuda.synchronize()
quantiles = [0.5, 0.2, 0.8]
try:
ms_baseline, ms_base_lo, ms_base_hi = triton.testing.do_bench(
fn_baseline, quantiles=quantiles, warmup=50, rep=200
)
ms_packed, ms_pack_lo, ms_pack_hi = triton.testing.do_bench(
fn_packed, quantiles=quantiles, warmup=50, rep=200
)
except Exception:
# Fallback to manual timing
torch.cuda.synchronize()
N = 200
start = time.perf_counter()
for _ in range(N):
fn_baseline()
torch.cuda.synchronize()
ms_baseline = (time.perf_counter() - start) / N * 1000
start = time.perf_counter()
for _ in range(N):
fn_packed()
torch.cuda.synchronize()
ms_packed = (time.perf_counter() - start) / N * 1000
speedup = ms_baseline / ms_packed if ms_packed > 0 else float("inf")
saved_us = (ms_baseline - ms_packed) * 1000
print(
f" {B:>5} {H:>3} {HV:>3} {K:>3} {V:>3} | "
f"{ms_baseline * 1000:>10.1f} | "
f"{ms_packed * 1000:>10.1f} | "
f"{speedup:>7.2f}x | "
f"{saved_us:>+9.1f}"
)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def run_correctness(device, dtype):
print("=" * 70)
print("Correctness: Baseline GDN Decode vs Packed GDN Decode")
print("=" * 70)
shapes = [
# (B, H, HV, K, V, pool_size)
# --- Qwen3.5-35B-A3B style (TP=2: H=8, HV=16) ---
(1, 8, 16, 128, 128, 32),
(4, 8, 16, 128, 128, 32),
(16, 8, 16, 128, 128, 64),
(32, 8, 16, 128, 128, 128),
(64, 8, 16, 128, 128, 128),
(128, 8, 16, 128, 128, 256),
(256, 8, 16, 128, 128, 512),
# --- Qwen3.5-35B-A3B style (TP=1: H=16, HV=32) ---
(1, 16, 32, 128, 128, 32),
(32, 16, 32, 128, 128, 128),
(64, 16, 32, 128, 128, 128),
# --- Qwen3-Next-80B-A3B style ---
(32, 16, 16, 128, 128, 128),
(64, 16, 16, 128, 128, 128),
# --- With PAD_SLOT_ID ---
(32, 8, 16, 128, 128, 128), # some indices may be padded
# --- Edge cases ---
(1, 8, 16, 128, 128, 32),
(2, 8, 16, 128, 128, 32),
]
all_pass = True
for B, H, HV, K, V, pool_size in shapes:
if not check_correctness(B, H, HV, K, V, pool_size, device, dtype):
all_pass = False
# PAD_SLOT_ID test
print("\n PAD_SLOT_ID test (indices with -1):")
inp = make_inputs(32, 8, 16, 128, 128, 128, device, dtype)
o_baseline, st_baseline = run_baseline(inp)
o_packed, st_packed = run_packed(inp)
try:
torch.testing.assert_close(o_packed, o_baseline, atol=2e-2, rtol=1e-2)
print(" [PASS] PAD_SLOT_ID=-1 handling")
except AssertionError:
print(" [FAIL] PAD_SLOT_ID=-1 handling")
all_pass = False
print()
if all_pass:
print("ALL PASSED.")
else:
print("SOME FAILED.")
return all_pass
def run_benchmark(device, dtype, args):
print()
print("=" * 85)
print("Benchmark: Baseline GDN Decode vs Packed GDN Decode")
print("=" * 85)
K = args.head_size_k
V = args.head_size_v
pool_size = args.pool_size
if args.preset == "qwen3.5-35b":
# Qwen3.5-35B-A3B: H_qk=16, H_v=32, K=128, V=128
# After TP=2: H=8, HV=16
bench_configs = [
# (B, H, HV) — TP=2 config
(1, 8, 16),
(2, 8, 16),
(4, 8, 16),
(8, 8, 16),
(16, 8, 16),
(32, 8, 16),
(64, 8, 16),
(128, 8, 16),
(256, 8, 16),
(512, 8, 16),
# TP=1 config (full heads)
(1, 16, 32),
(8, 16, 32),
(32, 16, 32),
(64, 16, 32),
(128, 16, 32),
(256, 16, 32),
]
elif args.preset == "qwen3-next-80b":
bench_configs = [
# Qwen3-Next-80B-A3B: all same H=HV=16 after TP
(1, 16, 16),
(8, 16, 16),
(32, 16, 16),
(64, 16, 16),
(128, 16, 16),
(256, 16, 16),
]
else:
bench_configs = []
for B in args.batch_sizes:
for H in args.num_q_heads:
for HV in args.num_v_heads:
bench_configs.append((B, H, HV))
print(f" Config: K={K}, V={V}, pool_size={pool_size}, dtype={dtype}")
print(
f" {'B':>5} {'H':>3} {'HV':>3} {'K':>3} {'V':>3} | "
f"{'base (μs)':>10} | "
f"{'packed (μs)':>10} | "
f"{'speedup':>8} | "
f"{'saved (μs)':>10}"
)
print(" " + "-" * 75)
for B, H, HV in bench_configs:
actual_pool = max(pool_size, B + 16)
bench_shape(B, H, HV, K, V, actual_pool, device, dtype)
def main():
parser = argparse.ArgumentParser(
description="Benchmark & Correctness: GDN Packed Decode vs Baseline"
)
parser.add_argument(
"--mode",
choices=["all", "correctness", "bench"],
default="all",
help="Run mode (default: all)",
)
parser.add_argument(
"--preset",
choices=["qwen3.5-35b", "qwen3-next-80b", "custom"],
default="qwen3.5-35b",
help="Preset config (default: qwen3.5-35b)",
)
parser.add_argument(
"--dtype",
choices=["float16", "bfloat16", "float32"],
default="bfloat16",
)
parser.add_argument("--head-size-k", type=int, default=128)
parser.add_argument("--head-size-v", type=int, default=128)
parser.add_argument("--pool-size", type=int, default=512)
parser.add_argument(
"--batch-sizes",
type=int,
nargs="+",
default=[1, 4, 8, 16, 32, 64, 128, 256, 512],
)
parser.add_argument(
"--num-q-heads",
type=int,
nargs="+",
default=[8, 16],
)
parser.add_argument(
"--num-v-heads",
type=int,
nargs="+",
default=[16, 32],
)
args = parser.parse_args()
device = "cuda"
dtype = getattr(torch, args.dtype)
cap = torch.cuda.get_device_capability()
dev_name = torch.cuda.get_device_name()
print(f"Device: {dev_name} (SM {cap[0]}{cap[1]})")
if args.mode in ("all", "correctness"):
all_pass = run_correctness(device, dtype)
if not all_pass and args.mode == "all":
print("\nSkipping benchmark due to correctness failures.")
return 1
if args.mode in ("all", "bench"):
run_benchmark(device, dtype, args)
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -181,6 +181,227 @@ def fused_recurrent_gated_delta_rule_fwd(
return o, final_state
# Adapted from vllm project.
@triton.jit
def fused_recurrent_gated_delta_rule_packed_decode_kernel(
mixed_qkv,
a,
b,
A_log,
dt_bias,
o,
h0,
ht,
ssm_state_indices,
scale,
stride_mixed_qkv_tok: tl.constexpr,
stride_a_tok: tl.constexpr,
stride_b_tok: tl.constexpr,
stride_init_state_token: tl.constexpr,
stride_final_state_token: tl.constexpr,
stride_indices_seq: tl.constexpr,
H: tl.constexpr,
HV: tl.constexpr,
K: tl.constexpr,
V: tl.constexpr,
BK: tl.constexpr,
BV: tl.constexpr,
SOFTPLUS_THRESHOLD: tl.constexpr,
USE_QK_L2NORM_IN_KERNEL: tl.constexpr,
):
i_v, i_nh = tl.program_id(0), tl.program_id(1)
i_n, i_hv = i_nh // HV, i_nh % HV
i_h = i_hv // (HV // H)
o_k = tl.arange(0, BK)
o_v = i_v * BV + tl.arange(0, BV)
mask_k = o_k < K
mask_v = o_v < V
mask_h = mask_v[:, None] & mask_k[None, :]
state_idx = tl.load(ssm_state_indices + i_n * stride_indices_seq).to(tl.int64)
p_o = o + (i_n * HV + i_hv) * V + o_v
if state_idx < 0:
zero = tl.zeros([BV], dtype=tl.float32).to(p_o.dtype.element_ty)
tl.store(p_o, zero, mask=mask_v)
return
p_h0 = h0 + state_idx * stride_init_state_token
p_h0 = p_h0 + i_hv * V * K + o_v[:, None] * K + o_k[None, :]
b_h = tl.load(p_h0, mask=mask_h, other=0).to(tl.float32)
p_mixed = mixed_qkv + i_n * stride_mixed_qkv_tok
q_off = i_h * K + o_k
k_off = (H * K) + i_h * K + o_k
v_off = (2 * H * K) + i_hv * V + o_v
b_q = tl.load(p_mixed + q_off, mask=mask_k, other=0).to(tl.float32)
b_k = tl.load(p_mixed + k_off, mask=mask_k, other=0).to(tl.float32)
b_v = tl.load(p_mixed + v_off, mask=mask_v, other=0).to(tl.float32)
if USE_QK_L2NORM_IN_KERNEL:
b_q = b_q / tl.sqrt(tl.sum(b_q * b_q) + 1e-6)
b_k = b_k / tl.sqrt(tl.sum(b_k * b_k) + 1e-6)
b_q = b_q * scale
a_val = tl.load(a + i_n * stride_a_tok + i_hv).to(tl.float32)
b_val = tl.load(b + i_n * stride_b_tok + i_hv).to(tl.float32)
A_log_val = tl.load(A_log + i_hv).to(tl.float32)
dt_bias_val = tl.load(dt_bias + i_hv).to(tl.float32)
x = a_val + dt_bias_val
softplus_x = tl.where(x <= SOFTPLUS_THRESHOLD, tl.log(1.0 + tl.exp(x)), x)
g_val = -tl.exp(A_log_val) * softplus_x
beta_val = tl.sigmoid(b_val).to(b.dtype.element_ty).to(tl.float32)
b_h *= exp(g_val)
b_v -= tl.sum(b_h * b_k[None, :], 1)
b_v *= beta_val
b_h += b_v[:, None] * b_k[None, :]
b_o = tl.sum(b_h * b_q[None, :], 1)
tl.store(p_o, b_o.to(p_o.dtype.element_ty), mask=mask_v)
p_ht = ht + state_idx * stride_final_state_token
p_ht = p_ht + i_hv * V * K + o_v[:, None] * K + o_k[None, :]
tl.store(p_ht, b_h.to(p_ht.dtype.element_ty), mask=mask_h)
def fused_recurrent_gated_delta_rule_packed_decode(
mixed_qkv: torch.Tensor,
a: torch.Tensor,
b: torch.Tensor,
A_log: torch.Tensor,
dt_bias: torch.Tensor,
scale: float,
initial_state: torch.Tensor,
out: torch.Tensor,
ssm_state_indices: torch.Tensor,
use_qk_l2norm_in_kernel: bool = False,
) -> tuple[torch.Tensor, torch.Tensor]:
if mixed_qkv.ndim != 2:
raise ValueError(
f"`mixed_qkv` must be a 2D tensor (got ndim={mixed_qkv.ndim})."
)
if mixed_qkv.stride(-1) != 1:
raise ValueError("`mixed_qkv` must be contiguous in the last dim.")
if a.ndim != 2 or b.ndim != 2:
raise ValueError(
f"`a` and `b` must be 2D tensors (got a.ndim={a.ndim}, b.ndim={b.ndim})."
)
if a.stride(-1) != 1 or b.stride(-1) != 1:
raise ValueError("`a`/`b` must be contiguous in the last dim.")
if A_log.ndim != 1 or dt_bias.ndim != 1:
raise ValueError("`A_log`/`dt_bias` must be 1D tensors.")
if A_log.stride(0) != 1 or dt_bias.stride(0) != 1:
raise ValueError("`A_log`/`dt_bias` must be contiguous.")
if ssm_state_indices.ndim != 1:
raise ValueError(
f"`ssm_state_indices` must be 1D for packed decode (got ndim={ssm_state_indices.ndim})."
)
if not out.is_contiguous():
raise ValueError("`out` must be contiguous.")
dev = mixed_qkv.device
if any(
t.device != dev
for t in (a, b, A_log, dt_bias, initial_state, out, ssm_state_indices)
):
raise ValueError("All inputs must be on the same device.")
B = mixed_qkv.shape[0]
if a.shape[0] != B or b.shape[0] != B:
raise ValueError(
"Mismatched batch sizes: "
f"mixed_qkv.shape[0]={B}, a.shape[0]={a.shape[0]}, b.shape[0]={b.shape[0]}."
)
if ssm_state_indices.shape[0] != B:
raise ValueError(
f"`ssm_state_indices` must have shape [B] (got {tuple(ssm_state_indices.shape)}; expected ({B},))."
)
if initial_state.ndim != 4:
raise ValueError(
f"`initial_state` must be a 4D tensor (got ndim={initial_state.ndim})."
)
if initial_state.stride(-1) != 1:
raise ValueError("`initial_state` must be contiguous in the last dim.")
HV, V, K = initial_state.shape[-3:]
if a.shape[1] != HV or b.shape[1] != HV:
raise ValueError(
f"`a`/`b` must have shape [B, HV] with HV={HV} (got a.shape={tuple(a.shape)}, b.shape={tuple(b.shape)})."
)
if A_log.numel() != HV or dt_bias.numel() != HV:
raise ValueError(
f"`A_log` and `dt_bias` must have {HV} elements (got A_log.numel()={A_log.numel()}, dt_bias.numel()={dt_bias.numel()})."
)
if out.shape != (B, 1, HV, V):
raise ValueError(
f"`out` must have shape {(B, 1, HV, V)} (got out.shape={tuple(out.shape)})."
)
qkv_dim = mixed_qkv.shape[1]
qk_dim = qkv_dim - HV * V
if qk_dim <= 0 or qk_dim % 2 != 0:
raise ValueError(
f"Invalid packed `mixed_qkv` last dim={qkv_dim} for HV={HV}, V={V}."
)
q_dim = qk_dim // 2
if q_dim % K != 0:
raise ValueError(f"Invalid packed Q size {q_dim}: must be divisible by K={K}.")
H = q_dim // K
if H <= 0 or HV % H != 0:
raise ValueError(
f"Invalid head config inferred from mixed_qkv: H={H}, HV={HV}."
)
BK = triton.next_power_of_2(K)
if triton.cdiv(K, BK) != 1:
raise ValueError(
f"Packed decode kernel only supports NK=1 (got K={K}, BK={BK})."
)
BV = min(triton.next_power_of_2(V), 32)
num_stages = 3
num_warps = 1
stride_mixed_qkv_tok = mixed_qkv.stride(0)
stride_a_tok = a.stride(0)
stride_b_tok = b.stride(0)
stride_init_state_token = initial_state.stride(0)
stride_final_state_token = initial_state.stride(0)
stride_indices_seq = ssm_state_indices.stride(0)
NV = triton.cdiv(V, BV)
grid = (NV, B * HV)
fused_recurrent_gated_delta_rule_packed_decode_kernel[grid](
mixed_qkv=mixed_qkv,
a=a,
b=b,
A_log=A_log,
dt_bias=dt_bias,
o=out,
h0=initial_state,
ht=initial_state,
ssm_state_indices=ssm_state_indices,
scale=scale,
stride_mixed_qkv_tok=stride_mixed_qkv_tok,
stride_a_tok=stride_a_tok,
stride_b_tok=stride_b_tok,
stride_init_state_token=stride_init_state_token,
stride_final_state_token=stride_final_state_token,
stride_indices_seq=stride_indices_seq,
H=H,
HV=HV,
K=K,
V=V,
BK=BK,
BV=BV,
SOFTPLUS_THRESHOLD=20.0,
USE_QK_L2NORM_IN_KERNEL=use_qk_l2norm_in_kernel,
num_warps=num_warps,
num_stages=num_stages,
)
return out, initial_state
class FusedRecurrentFunction(torch.autograd.Function):
@staticmethod

View File

@@ -1,4 +1,4 @@
from typing import Tuple, Union
from typing import Optional, Tuple, Union
import torch
@@ -111,10 +111,48 @@ class GDNKernelDispatcher:
else:
self.verify_kernel = triton_kernel
self.supports_packed_decode = getattr(
self.decode_kernel, "supports_packed_decode", False
)
rank0_log(
f"GDN kernel dispatcher: decode={self.decode_kernel.__class__.__name__}, "
f"extend={self.extend_kernel.__class__.__name__}, "
f"verify={self.verify_kernel.__class__.__name__}"
f"verify={self.verify_kernel.__class__.__name__} "
f"packed_decode={self.supports_packed_decode}"
)
def packed_decode(
self,
mixed_qkv: torch.Tensor,
a: torch.Tensor,
b: torch.Tensor,
*,
A_log: torch.Tensor,
dt_bias: torch.Tensor,
scale: float,
ssm_states: torch.Tensor,
cache_indices: torch.Tensor,
num_v_heads: int,
head_v_dim: int,
**kwargs,
) -> Optional[torch.Tensor]:
"""Attempt packed decode. Returns output tensor or None if
the decode kernel does not support packed decode."""
if not self.supports_packed_decode:
return None
return self.decode_kernel.packed_decode(
mixed_qkv,
a,
b,
A_log=A_log,
dt_bias=dt_bias,
scale=scale,
ssm_states=ssm_states,
cache_indices=cache_indices,
num_v_heads=num_v_heads,
head_v_dim=head_v_dim,
**kwargs,
)
def decode(
@@ -243,6 +281,26 @@ class GDNAttnBackend(MambaAttnBackendBase):
conv_state_indices=cache_indices,
)
# Skip split + reshape + separate gating kernel by consuming
# the packed mixed_qkv directly in a single fused Triton kernel.
if self.kernel_dispatcher.supports_packed_decode:
core_attn_out = self.kernel_dispatcher.packed_decode(
mixed_qkv=mixed_qkv,
a=a,
b=b,
A_log=layer.A_log,
dt_bias=layer.dt_bias,
scale=layer.head_k_dim**-0.5,
ssm_states=ssm_states,
cache_indices=cache_indices,
num_v_heads=layer.num_v_heads,
head_v_dim=layer.head_v_dim,
)
self._track_mamba_state_decode(
forward_batch, conv_states, ssm_states, cache_indices
)
return core_attn_out
query, key, value = torch.split(
mixed_qkv,
[layer.q_dim, layer.k_dim, layer.v_dim],

View File

@@ -7,6 +7,9 @@ from sglang.srt.utils import is_cpu, is_npu
if not is_cpu():
from sglang.srt.layers.attention.fla.chunk import chunk_gated_delta_rule
from sglang.srt.layers.attention.fla.fused_recurrent import (
fused_recurrent_gated_delta_rule_packed_decode,
)
from sglang.srt.layers.attention.fla.fused_sigmoid_gating_recurrent import (
fused_sigmoid_gating_delta_rule_update,
)
@@ -31,6 +34,63 @@ elif is_cpu():
class TritonGDNKernel(LinearAttnKernelBase):
"""Triton-based kernel for GDN (Gated Delta Network) linear attention."""
supports_packed_decode: bool = not is_cpu() and not is_npu()
def packed_decode(
self,
mixed_qkv: torch.Tensor,
a: torch.Tensor,
b: torch.Tensor,
*,
A_log: torch.Tensor,
dt_bias: torch.Tensor,
scale: float,
ssm_states: torch.Tensor,
cache_indices: torch.Tensor,
num_v_heads: int,
head_v_dim: int,
**kwargs,
) -> torch.Tensor:
"""Packed decode fast path: fuse QKV extraction + gating + recurrent
update into a single Triton kernel, eliminating intermediate tensors
and extra kernel launches.
Args:
mixed_qkv: [B, qkv_dim] packed projection output after conv1d.
a, b: [B, HV] gating inputs.
A_log: [HV] log-space decay parameter.
dt_bias: [HV] time-step bias.
scale: attention scale factor (typically head_k_dim ** -0.5).
ssm_states: [num_slots, HV, V, K] full state pool.
cache_indices: [B] per-request state slot indices.
num_v_heads: number of value heads (after TP sharding).
head_v_dim: dimension per value head.
Returns:
output tensor of shape [1, B, HV, V] matching the existing
decode kernel output layout.
"""
B = mixed_qkv.shape[0]
# Packed kernel expects output shape [B, 1, HV, V]
out = mixed_qkv.new_empty(B, 1, num_v_heads, head_v_dim)
fused_recurrent_gated_delta_rule_packed_decode(
mixed_qkv=mixed_qkv,
a=a,
b=b,
A_log=A_log,
dt_bias=dt_bias,
scale=scale,
initial_state=ssm_states,
out=out,
ssm_state_indices=cache_indices,
use_qk_l2norm_in_kernel=True,
)
# Convert [B, 1, HV, V] → [1, B, HV, V] to match existing output
# layout. transpose() returns a view — zero cost.
return out.transpose(0, 1)
def decode(
self,
q: torch.Tensor,