[KDA] Support CuTeDSL KDA decode kernel (#21203)

Co-authored-by: luoyuan.luo <luoyuan.luo@antgroup.com>
This commit is contained in:
Yuan Luo
2026-03-25 09:47:09 +08:00
committed by GitHub
co-authored by luoyuan.luo
parent dfc15b78b0
commit f273ba1ccc
5 changed files with 2045 additions and 2 deletions
@@ -0,0 +1,472 @@
"""Benchmark & Correctness: CuTe DSL KDA Decode vs Triton KDA Decode.
This benchmark assumes the production / Triton canonical state layout:
ssm_states.shape == (pool_size, HV, V, K)
Both the Triton baseline and the CuTe DSL candidate operate directly on that VK
layout. No transpose is performed anywhere in the benchmark.
"""
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.jit_kernel.cutedsl_kda import cutedsl_fused_sigmoid_gating_kda_update
from sglang.srt.layers.attention.fla.fused_sigmoid_gating_recurrent import (
fused_sigmoid_gating_delta_rule_update,
)
from sglang.srt.layers.attention.fla.kda import chunk_kda
def make_inputs(
B: int,
H: int,
HV: int,
K: int,
V: int,
pool_size: int,
device: str,
dtype: torch.dtype,
layout: str,
seed: int = 42,
):
torch.manual_seed(seed)
assert K == 128
assert V % 16 == 0 and V % 32 == 0
if layout == "varlen":
q = torch.randn(1, B, H, K, device=device, dtype=dtype)
k = torch.randn(1, B, H, K, device=device, dtype=dtype)
v = torch.randn(1, B, HV, V, device=device, dtype=dtype)
# decode params
a = torch.randn(B, HV, K, device=device, dtype=dtype)
b = torch.randn(B, HV, device=device, dtype=dtype)
# prefill params for chunk_kda must keep batch dim = 1
prefill_g = torch.randn(1, B, HV, K, device=device, dtype=dtype)
prefill_beta = torch.sigmoid(torch.randn(1, B, HV, device=device, dtype=dtype))
cu_seqlens = torch.arange(B + 1, device=device, dtype=torch.int32)
elif layout == "dense":
q = torch.randn(B, 1, H, K, device=device, dtype=dtype)
k = torch.randn(B, 1, H, K, device=device, dtype=dtype)
v = torch.randn(B, 1, HV, V, device=device, dtype=dtype)
# decode params
a = torch.randn(B, 1, HV, K, device=device, dtype=dtype)
b = torch.randn(B, 1, HV, device=device, dtype=dtype)
# prefill params for chunk_kda dense path
prefill_g = torch.randn(B, 1, HV, K, device=device, dtype=dtype)
prefill_beta = torch.sigmoid(torch.randn(B, 1, HV, device=device, dtype=dtype))
cu_seqlens = torch.arange(B + 1, device=device, dtype=torch.int32)
else:
raise ValueError(f"Unknown layout: {layout}")
A_log = torch.randn(HV, device=device, dtype=torch.float32)
dt_bias = torch.randn(HV, K, device=device, dtype=dtype)
ssm_states = (
torch.randn(pool_size, HV, V, K, device=device, dtype=torch.float32) * 0.1
)
cache_indices = torch.arange(B, device=device, dtype=torch.int32)
return dict(
B=B,
H=H,
HV=HV,
K=K,
V=V,
pool_size=pool_size,
layout=layout,
q=q,
k=k,
v=v,
a=a,
b=b,
prefill_g=prefill_g,
prefill_beta=prefill_beta,
A_log=A_log,
dt_bias=dt_bias,
ssm_states=ssm_states,
cache_indices=cache_indices,
cu_seqlens=cu_seqlens,
)
def run_baseline(inp):
state = inp["ssm_states"].clone()
o = fused_sigmoid_gating_delta_rule_update(
A_log=inp["A_log"],
dt_bias=inp["dt_bias"],
q=inp["q"],
k=inp["k"],
v=inp["v"],
a=inp["a"],
b=inp["b"],
initial_state_source=state,
initial_state_indices=inp["cache_indices"],
cu_seqlens=inp["cu_seqlens"] if inp["layout"] == "varlen" else None,
use_qk_l2norm_in_kernel=True,
softplus_beta=1.0,
softplus_threshold=20.0,
is_kda=True,
)
return o, state
def run_cutedsl(inp):
state = inp["ssm_states"].clone()
o = cutedsl_fused_sigmoid_gating_kda_update(
A_log=inp["A_log"],
dt_bias=inp["dt_bias"],
q=inp["q"],
k=inp["k"],
v=inp["v"],
a=inp["a"],
b=inp["b"],
initial_state_source=state,
initial_state_indices=inp["cache_indices"],
cu_seqlens=inp["cu_seqlens"] if inp["layout"] == "varlen" else None,
use_qk_l2norm_in_kernel=True,
softplus_beta=1.0,
softplus_threshold=20.0,
)
return o, state
def run_prefill_then_decode_baseline(inp):
ssm_states = inp["ssm_states"].clone()
v_clone = inp["v"].clone()
_ = chunk_kda(
q=inp["q"],
k=inp["k"],
v=v_clone,
g=inp["prefill_g"],
beta=inp["prefill_beta"],
initial_state=ssm_states,
initial_state_indices=inp["cache_indices"],
use_qk_l2norm_in_kernel=True,
cu_seqlens=inp["cu_seqlens"] if inp["layout"] == "varlen" else None,
)
o = fused_sigmoid_gating_delta_rule_update(
A_log=inp["A_log"],
dt_bias=inp["dt_bias"],
q=inp["q"],
k=inp["k"],
v=v_clone,
a=inp["a"],
b=inp["b"],
initial_state_source=ssm_states,
initial_state_indices=inp["cache_indices"],
cu_seqlens=inp["cu_seqlens"] if inp["layout"] == "varlen" else None,
use_qk_l2norm_in_kernel=True,
softplus_beta=1.0,
softplus_threshold=20.0,
is_kda=True,
)
return o, ssm_states
def run_prefill_then_decode_cutedsl(inp):
ssm_states = inp["ssm_states"].clone()
v_clone = inp["v"].clone()
_ = chunk_kda(
q=inp["q"],
k=inp["k"],
v=v_clone,
g=inp["prefill_g"],
beta=inp["prefill_beta"],
initial_state=ssm_states,
initial_state_indices=inp["cache_indices"],
use_qk_l2norm_in_kernel=True,
cu_seqlens=inp["cu_seqlens"] if inp["layout"] == "varlen" else None,
)
o = cutedsl_fused_sigmoid_gating_kda_update(
A_log=inp["A_log"],
dt_bias=inp["dt_bias"],
q=inp["q"],
k=inp["k"],
v=v_clone,
a=inp["a"],
b=inp["b"],
initial_state_source=ssm_states,
initial_state_indices=inp["cache_indices"],
cu_seqlens=inp["cu_seqlens"] if inp["layout"] == "varlen" else None,
use_qk_l2norm_in_kernel=True,
softplus_beta=1.0,
softplus_threshold=20.0,
)
return o, ssm_states
def _assert_close(name, x, y, atol=3e-2, rtol=2e-2):
try:
torch.testing.assert_close(x.float(), y.float(), atol=atol, rtol=rtol)
return True, 0.0
except AssertionError:
max_diff = (x - y).abs().max().item()
return False, max_diff
def check_correctness(B, H, HV, K, V, pool_size, device, dtype, layout):
tag = (
f"layout={layout:<6} B={B:>4} H={H:>2} HV={HV:>2} "
f"K={K:>3} V={V:>3} pool={pool_size:>4}"
)
inp = make_inputs(B, H, HV, K, V, pool_size, device, dtype, layout)
o_ref, st_ref = run_baseline(inp)
o_cute, st_cute = run_cutedsl(inp)
ok_o, diff_o = _assert_close("output", o_cute, o_ref)
valid_mask = inp["cache_indices"] >= 0
valid_idx = inp["cache_indices"][valid_mask]
ok_s, diff_s = _assert_close("state", st_cute[valid_idx], st_ref[valid_idx])
if ok_o and ok_s:
print(f" [PASS] {tag}")
return True
details = []
if not ok_o:
details.append(f"output max_diff={diff_o:.6f}")
if not ok_s:
details.append(f"state max_diff={diff_s:.6f}")
print(f" [FAIL] {tag} ({', '.join(details)})")
return False
def check_prefill_chain(B, H, HV, K, V, pool_size, device, dtype, layout):
tag = (
f"[prefill->decode] layout={layout:<6} B={B:>4} H={H:>2} HV={HV:>2} "
f"K={K:>3} V={V:>3} pool={pool_size:>4}"
)
inp = make_inputs(B, H, HV, K, V, pool_size, device, dtype, layout)
o_ref, st_ref = run_prefill_then_decode_baseline(inp)
o_cute, st_cute = run_prefill_then_decode_cutedsl(inp)
ok_o, diff_o = _assert_close("output", o_cute, o_ref)
valid_mask = inp["cache_indices"] >= 0
valid_idx = inp["cache_indices"][valid_mask]
ok_s, diff_s = _assert_close("state", st_cute[valid_idx], st_ref[valid_idx])
if ok_o and ok_s:
print(f" [PASS] {tag}")
return True
details = []
if not ok_o:
details.append(f"output max_diff={diff_o:.6f}")
if not ok_s:
details.append(f"state max_diff={diff_s:.6f}")
print(f" [FAIL] {tag} ({', '.join(details)})")
return False
def bench_shape(B, H, HV, K, V, pool_size, device, dtype, layout):
inp = make_inputs(B, H, HV, K, V, pool_size, device, dtype, layout)
def fn_triton():
fused_sigmoid_gating_delta_rule_update(
A_log=inp["A_log"],
dt_bias=inp["dt_bias"],
q=inp["q"],
k=inp["k"],
v=inp["v"],
a=inp["a"],
b=inp["b"],
initial_state_source=inp["ssm_states"],
initial_state_indices=inp["cache_indices"],
cu_seqlens=inp["cu_seqlens"] if inp["layout"] == "varlen" else None,
use_qk_l2norm_in_kernel=True,
softplus_beta=1.0,
softplus_threshold=20.0,
is_kda=True,
)
def fn_cute():
cutedsl_fused_sigmoid_gating_kda_update(
A_log=inp["A_log"],
dt_bias=inp["dt_bias"],
q=inp["q"],
k=inp["k"],
v=inp["v"],
a=inp["a"],
b=inp["b"],
initial_state_source=inp["ssm_states"],
initial_state_indices=inp["cache_indices"],
cu_seqlens=inp["cu_seqlens"] if inp["layout"] == "varlen" else None,
use_qk_l2norm_in_kernel=True,
softplus_beta=1.0,
softplus_threshold=20.0,
)
for _ in range(10):
fn_triton()
fn_cute()
torch.cuda.synchronize()
try:
ms_triton, _, _ = triton.testing.do_bench(
fn_triton, quantiles=[0.5, 0.2, 0.8], warmup=50, rep=200
)
ms_cute, _, _ = triton.testing.do_bench(
fn_cute, quantiles=[0.5, 0.2, 0.8], warmup=50, rep=200
)
except Exception:
rep = 100
st = time.perf_counter()
for _ in range(rep):
fn_triton()
torch.cuda.synchronize()
ms_triton = (time.perf_counter() - st) / rep * 1000
st = time.perf_counter()
for _ in range(rep):
fn_cute()
torch.cuda.synchronize()
ms_cute = (time.perf_counter() - st) / rep * 1000
speedup = ms_triton / ms_cute if ms_cute > 0 else float("inf")
delta = (ms_cute - ms_triton) * 1000
print(
f" {layout:>6} {B:>5} {H:>3} {HV:>3} {K:>3} {V:>3} | "
f"{ms_triton * 1000:>12.1f} | "
f"{ms_cute * 1000:>13.1f} | "
f"{speedup:>8.2f} | "
f"{delta:>11.1f}"
)
def run_correctness(device, dtype):
print("=" * 78)
print("Correctness: Triton KDA Decode vs CuTe DSL KDA Decode")
print("=" * 78)
shapes = [
("dense", 1, 8, 16, 128, 128, 32),
("dense", 4, 8, 16, 128, 128, 32),
("dense", 32, 8, 16, 128, 128, 128),
("dense", 64, 8, 16, 128, 128, 128),
("varlen", 4, 8, 16, 128, 128, 32),
("varlen", 16, 8, 16, 128, 128, 64),
("varlen", 32, 8, 16, 128, 128, 128),
("varlen", 64, 8, 16, 128, 128, 128),
("varlen", 1, 16, 32, 128, 128, 32),
("varlen", 32, 16, 32, 128, 128, 128),
("varlen", 64, 16, 16, 128, 128, 128),
]
all_pass = True
for layout, B, H, HV, K, V, pool_size in shapes:
if not check_correctness(B, H, HV, K, V, pool_size, device, dtype, layout):
all_pass = False
print()
print("=" * 78)
print("Correctness: Triton prefill/extend -> CuTe decode chain")
print("=" * 78)
for layout, B, H, HV, K, V, pool_size in shapes[:8]:
if not check_prefill_chain(B, H, HV, K, V, pool_size, device, dtype, layout):
all_pass = False
print()
print("ALL PASSED." if all_pass else "SOME FAILED.")
return all_pass
def run_benchmark(device, dtype):
print()
print("=" * 92)
print("Benchmark: Triton KDA Decode vs CuTe DSL KDA Decode")
print("=" * 92)
bench_configs = [
("dense", 1, 8, 16),
("dense", 4, 8, 16),
("dense", 32, 8, 16),
("dense", 64, 8, 16),
("varlen", 1, 8, 16),
("varlen", 4, 8, 16),
("varlen", 8, 8, 16),
("varlen", 16, 8, 16),
("varlen", 32, 8, 16),
("varlen", 64, 8, 16),
("varlen", 128, 8, 16),
("varlen", 32, 16, 32),
("varlen", 64, 16, 16),
]
K = 128
V = 128
pool_size = 512
print(f" Config: K={K}, V={V}, pool_size={pool_size}, dtype={dtype}")
print(
f" {'layout':>6} {'B':>5} {'H':>3} {'HV':>3} {'K':>3} {'V':>3} | "
f"{'triton (μs)':>12} | "
f"{'cutedsl (μs)':>13} | "
f"{'speedup':>8} | "
f"{'delta (μs)':>11}"
)
print(" " + "-" * 82)
for layout, B, H, HV in bench_configs:
actual_pool = max(pool_size, B + 16)
bench_shape(B, H, HV, K, V, actual_pool, device, dtype, layout)
def main():
parser = argparse.ArgumentParser(
description="Benchmark & Correctness: Triton KDA Decode vs CuTe DSL KDA Decode"
)
parser.add_argument(
"--mode",
choices=["all", "correctness", "bench"],
default="all",
help="Run mode (default: all)",
)
parser.add_argument(
"--dtype",
choices=["float16", "bfloat16", "float32"],
default="bfloat16",
)
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)
return 0
if __name__ == "__main__":
raise SystemExit(main())
File diff suppressed because it is too large Load Diff
@@ -64,7 +64,7 @@ class GDNKernelDispatcher:
self.decode_kernel = triton_kernel
elif decode_backend.is_cutedsl():
if not is_cuda():
raise ValueError("CuTe DSL backend requires CUDA")
raise ValueError("GDN CuTe DSL backend requires CUDA")
from sglang.srt.layers.attention.linear.kernels.gdn_cutedsl import (
CuteDSLGDNKernel,
)
@@ -3,6 +3,9 @@ from typing import Tuple, Union
import torch
from sglang.srt.layers.attention.hybrid_linear_attn_backend import MambaAttnBackendBase
from sglang.srt.layers.attention.linear.kernels.kda_cutedsl import (
CuteDSLKDAKernel,
)
from sglang.srt.layers.attention.linear.kernels.kda_triton import TritonKDAKernel
from sglang.srt.layers.attention.linear.utils import (
LinearAttnKernelBackend,
@@ -14,7 +17,7 @@ from sglang.srt.layers.attention.mamba.causal_conv1d_triton import (
causal_conv1d_update,
)
from sglang.srt.layers.radix_linear_attention import RadixLinearAttention
from sglang.srt.utils import is_cpu, is_npu
from sglang.srt.utils import is_cpu, is_cuda, is_npu
from sglang.srt.utils.common import rank0_log
# KDA always uses the triton causal_conv1d_fn (no CUDA override).
@@ -44,6 +47,10 @@ class KDAKernelDispatcher:
if decode_backend.is_triton():
self.decode_kernel = triton_kernel
elif decode_backend.is_cutedsl():
if not is_cuda():
raise ValueError("KDA CuTe DSL backend requires CUDA")
self.decode_kernel = CuteDSLKDAKernel()
else:
raise ValueError(
f"Unsupported KDA decode backend: {decode_backend}. "
@@ -0,0 +1,47 @@
import torch
from sglang.jit_kernel.cutedsl_kda import cutedsl_fused_sigmoid_gating_kda_update
from sglang.srt.layers.attention.linear.kernels.kernel_backend import (
LinearAttnKernelBase,
)
class CuteDSLKDAKernel(LinearAttnKernelBase):
"""CuTe DSL kernel for KDA decode (CUDA only)."""
def decode(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
a: torch.Tensor,
b: torch.Tensor,
*,
A_log: torch.Tensor,
dt_bias: torch.Tensor,
ssm_states: torch.Tensor,
cache_indices: torch.Tensor,
query_start_loc: torch.Tensor,
**kwargs,
) -> torch.Tensor:
return cutedsl_fused_sigmoid_gating_kda_update(
A_log=A_log,
dt_bias=dt_bias,
q=q,
k=k,
v=v,
a=a,
b=b,
initial_state_source=ssm_states,
initial_state_indices=cache_indices,
cu_seqlens=query_start_loc,
use_qk_l2norm_in_kernel=True,
softplus_beta=1.0,
softplus_threshold=20.0,
)
def extend(self, *args, **kwargs):
raise NotImplementedError("CuteDSLKDAKernel only supports decode")
def target_verify(self, *args, **kwargs):
raise NotImplementedError("CuteDSLKDAKernel only supports decode")