94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
383 lines
14 KiB
Python
383 lines
14 KiB
Python
"""FlashInfer-based kernels for GDN (Gated Delta Network) linear attention.
|
|
|
|
Both SM90 and SM100 use the same pool layout: [pool, HV, V, K] (K-last).
|
|
|
|
SM90 (Hopper): full support — decode, prefill, MTP. State dtype: fp32.
|
|
SM100 (Blackwell): full support — decode, prefill, MTP.
|
|
|
|
Requires flashinfer >= 0.6.7.
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
from typing import Optional
|
|
|
|
import torch
|
|
|
|
from sglang.srt.layers.attention.linear.kernels.kernel_backend import (
|
|
LinearAttnKernelBase,
|
|
)
|
|
from sglang.srt.utils import is_cuda
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Lazy import for FlashInfer GDN kernels
|
|
# ---------------------------------------------------------------------------
|
|
_flashinfer_gdn_available: Optional[bool] = None
|
|
_flashinfer_chunk_gated_delta_rule = None
|
|
_flashinfer_gated_delta_rule_mtp = None
|
|
_flashinfer_gated_delta_rule_decode = None
|
|
_flashinfer_gated_delta_rule_mtp_bf16 = None
|
|
|
|
|
|
def _get_flashinfer_gdn_kernels():
|
|
"""Lazy import for FlashInfer GDN prefill, decode and verify (MTP) kernels.
|
|
|
|
Returns (available, prefill_fn, mtp_fn, decode_fn, mtp_bf16_fn).
|
|
"""
|
|
global _flashinfer_gdn_available, _flashinfer_chunk_gated_delta_rule, _flashinfer_gated_delta_rule_mtp, _flashinfer_gated_delta_rule_decode, _flashinfer_gated_delta_rule_mtp_bf16
|
|
if _flashinfer_gdn_available is None:
|
|
try:
|
|
os.environ.setdefault("FLASHINFER_DISABLE_VERSION_CHECK", "1")
|
|
|
|
from flashinfer.gdn_decode import (
|
|
gated_delta_rule_decode_pretranspose,
|
|
gated_delta_rule_mtp,
|
|
)
|
|
from flashinfer.gdn_kernels.gdn_decode_bf16_state import (
|
|
gated_delta_rule_mtp as gated_delta_rule_mtp_bf16,
|
|
)
|
|
from flashinfer.gdn_prefill import chunk_gated_delta_rule
|
|
|
|
_flashinfer_chunk_gated_delta_rule = chunk_gated_delta_rule
|
|
_flashinfer_gated_delta_rule_mtp = gated_delta_rule_mtp
|
|
_flashinfer_gated_delta_rule_mtp_bf16 = gated_delta_rule_mtp_bf16
|
|
_flashinfer_gated_delta_rule_decode = gated_delta_rule_decode_pretranspose
|
|
_flashinfer_gdn_available = (
|
|
is_cuda() and torch.cuda.get_device_capability()[0] >= 9
|
|
)
|
|
if _flashinfer_gdn_available:
|
|
logger.info("FlashInfer GDN kernels loaded successfully")
|
|
except (ImportError, RuntimeError) as e:
|
|
logger.warning(f"FlashInfer GDN kernels not available: {e}")
|
|
_flashinfer_gdn_available = False
|
|
_flashinfer_gated_delta_rule_decode = None
|
|
return (
|
|
_flashinfer_gdn_available,
|
|
_flashinfer_chunk_gated_delta_rule,
|
|
_flashinfer_gated_delta_rule_mtp,
|
|
_flashinfer_gated_delta_rule_decode,
|
|
_flashinfer_gated_delta_rule_mtp_bf16,
|
|
)
|
|
|
|
|
|
def is_flashinfer_gdn_prefill_available() -> bool:
|
|
"""Return whether the kernel loader can construct the prefill path."""
|
|
available, prefill_fn, *_ = _get_flashinfer_gdn_kernels()
|
|
return bool(available and prefill_fn is not None)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Kernel implementation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class FlashInferGDNKernel(LinearAttnKernelBase):
|
|
"""FlashInfer kernel for GDN with K-last SSM state layout.
|
|
|
|
SM90 (Hopper): decode uses gather/scatter; prefill and MTP verify supported.
|
|
SM100 (Blackwell): decode uses gather/scatter; prefill and MTP verify supported.
|
|
|
|
Requires flashinfer >= 0.6.7.
|
|
"""
|
|
|
|
def __init__(self):
|
|
(
|
|
available,
|
|
self._prefill_fn,
|
|
self._mtp_fn,
|
|
self._decode_fn,
|
|
mtp_bf16_fn,
|
|
) = _get_flashinfer_gdn_kernels()
|
|
|
|
if not available:
|
|
raise RuntimeError(
|
|
"FlashInfer GDN kernels are not available. "
|
|
"Requires SM90+ and FlashInfer with GDN kernel support."
|
|
)
|
|
if self._decode_fn is None:
|
|
raise RuntimeError("FlashInfer GDN decode kernel is unavailable.")
|
|
|
|
sm_major = torch.cuda.get_device_capability()[0]
|
|
self.use_state_pool = sm_major >= 10
|
|
self.supports_target_verify = sm_major in (9, 10)
|
|
|
|
if sm_major == 9 and self._prefill_fn is None:
|
|
raise RuntimeError("FlashInfer GDN prefill kernel is unavailable.")
|
|
if self._mtp_fn is None:
|
|
raise RuntimeError("FlashInfer GDN MTP (verify) kernel is unavailable.")
|
|
|
|
if self.use_state_pool and mtp_bf16_fn is not None:
|
|
# Adapt bf16 kernel to fp32 kernel interface so target_verify needs no branching.
|
|
def _mtp_bf16_adapted(
|
|
q,
|
|
k,
|
|
v,
|
|
initial_state,
|
|
initial_state_indices,
|
|
A_log,
|
|
a,
|
|
dt_bias,
|
|
b,
|
|
use_qk_l2norm=True,
|
|
**kw,
|
|
):
|
|
out = mtp_bf16_fn(
|
|
A_log=A_log.float(),
|
|
a=a,
|
|
dt_bias=dt_bias,
|
|
softplus_beta=1.0,
|
|
softplus_threshold=20.0,
|
|
q=q,
|
|
k=k,
|
|
v=v,
|
|
b=b,
|
|
initial_state_source=initial_state,
|
|
initial_state_indices=initial_state_indices,
|
|
use_qk_l2norm_in_kernel=use_qk_l2norm,
|
|
**kw,
|
|
)
|
|
return out, None
|
|
|
|
self._mtp_fn = _mtp_bf16_adapted
|
|
|
|
logger.info("Using FlashInfer GDN kernels")
|
|
|
|
# ---- decode ----
|
|
|
|
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:
|
|
batch_size = cache_indices.shape[0]
|
|
num_heads = q.shape[2]
|
|
head_k_dim = q.shape[3]
|
|
num_v_heads = v.shape[2]
|
|
head_v_dim = v.shape[3]
|
|
|
|
query_fi = q.view(batch_size, 1, num_heads, head_k_dim)
|
|
key_fi = k.view(batch_size, 1, num_heads, head_k_dim)
|
|
value_fi = v.view(batch_size, 1, num_v_heads, head_v_dim)
|
|
a_fi = a.view(batch_size, 1, num_v_heads)
|
|
b_fi = b.view(batch_size, 1, num_v_heads)
|
|
|
|
if self.use_state_pool:
|
|
output_fi, _ = self._decode_fn(
|
|
q=query_fi,
|
|
k=key_fi,
|
|
v=value_fi,
|
|
state=None,
|
|
A_log=A_log.detach().float(),
|
|
a=a_fi,
|
|
dt_bias=dt_bias.detach(),
|
|
b=b_fi,
|
|
use_qk_l2norm=True,
|
|
initial_state=ssm_states,
|
|
initial_state_indices=cache_indices,
|
|
)
|
|
else:
|
|
# TODO: Once FlashInfer PR#2521 is merged for SM90, gather/scatter
|
|
# will no longer be needed here.
|
|
state_batch = ssm_states[cache_indices]
|
|
output_fi, new_state = self._decode_fn(
|
|
q=query_fi,
|
|
k=key_fi,
|
|
v=value_fi,
|
|
state=state_batch,
|
|
A_log=A_log.detach(),
|
|
a=a_fi,
|
|
dt_bias=dt_bias.detach(),
|
|
b=b_fi,
|
|
scale=None,
|
|
output=None,
|
|
use_qk_l2norm=True,
|
|
)
|
|
ssm_states[cache_indices] = new_state
|
|
|
|
return output_fi.view(1, batch_size, num_v_heads, head_v_dim)
|
|
|
|
# ---- extend (prefill) ----
|
|
|
|
def extend(
|
|
self,
|
|
q: torch.Tensor,
|
|
k: torch.Tensor,
|
|
v: torch.Tensor,
|
|
g: torch.Tensor,
|
|
beta: torch.Tensor,
|
|
*,
|
|
ssm_states: torch.Tensor,
|
|
cache_indices: torch.Tensor,
|
|
query_start_loc: torch.Tensor,
|
|
**kwargs,
|
|
) -> tuple:
|
|
from sglang.srt.layers.attention.fla.l2norm import l2norm_fwd
|
|
|
|
total_seq_len = q.shape[1]
|
|
num_v_heads = v.shape[2]
|
|
head_v_dim = v.shape[3]
|
|
|
|
q_fi = l2norm_fwd(q[0].contiguous())
|
|
k_fi = l2norm_fwd(k[0].contiguous())
|
|
v_fi = v[0].contiguous()
|
|
|
|
# g (alpha) and beta: [1, seq, HV] -> [seq, HV], float32 for FlashInfer
|
|
alpha_fi = torch.exp(g[0].to(torch.float32))
|
|
beta_fi = beta[0].to(torch.float32)
|
|
|
|
if self.use_state_pool:
|
|
# Negative indices (e.g. -1) are padding markers for slots not yet
|
|
# assigned to a real sequence; clamp them to 0 (the reserved dummy
|
|
# slot) so the FlashInfer kernel never reads out-of-bounds state.
|
|
ssm_cache_indices = cache_indices.clamp(min=0).to(torch.int64)
|
|
initial_state_fi = ssm_states[ssm_cache_indices].contiguous()
|
|
# Pre-allocate bf16 output_state so the kernel compiles and writes the
|
|
# bf16 state path directly, avoiding a fp32 allocation and a subsequent
|
|
# fp32->bf16 conversion in the scatter step.
|
|
output_state_fi = torch.empty_like(initial_state_fi)
|
|
output_fi, output_state_fi = self._prefill_fn(
|
|
q=q_fi,
|
|
k=k_fi,
|
|
v=v_fi,
|
|
g=alpha_fi,
|
|
beta=beta_fi,
|
|
scale=None,
|
|
initial_state=initial_state_fi,
|
|
output_final_state=True,
|
|
cu_seqlens=query_start_loc, # already int32
|
|
use_qk_l2norm_in_kernel=False,
|
|
output_state=output_state_fi,
|
|
)
|
|
else:
|
|
# SM90: preserve original negative-index handling (remap to last slot).
|
|
ssm_cache_indices = torch.where(
|
|
cache_indices >= 0,
|
|
cache_indices,
|
|
ssm_states.shape[0] - 1,
|
|
).to(torch.int64)
|
|
# State must be float32; kernel requires int64 cu_seqlens.
|
|
initial_state_fi = ssm_states[ssm_cache_indices].to(torch.float32)
|
|
output_fi, output_state_fi = self._prefill_fn(
|
|
q=q_fi,
|
|
k=k_fi,
|
|
v=v_fi,
|
|
g=alpha_fi,
|
|
beta=beta_fi,
|
|
scale=None,
|
|
initial_state=initial_state_fi,
|
|
output_final_state=True,
|
|
cu_seqlens=query_start_loc.to(torch.int64),
|
|
use_qk_l2norm_in_kernel=False,
|
|
)
|
|
|
|
# Write back state to pool
|
|
ssm_states.index_copy_(
|
|
0,
|
|
ssm_cache_indices,
|
|
output_state_fi.to(ssm_states.dtype),
|
|
)
|
|
|
|
# Output: [seq, HV, V] -> [1, seq, HV, V]
|
|
core_attn_out = output_fi.view(1, total_seq_len, num_v_heads, head_v_dim)
|
|
|
|
# Return (output, last_recurrent_state, h) to match Triton kernel interface.
|
|
# h=None since FlashInfer doesn't provide intermediate states.
|
|
return core_attn_out, None, None
|
|
|
|
# ---- target_verify (MTP) ----
|
|
|
|
def target_verify(
|
|
self,
|
|
A_log: torch.Tensor,
|
|
dt_bias: torch.Tensor,
|
|
q: torch.Tensor,
|
|
k: torch.Tensor,
|
|
v: torch.Tensor,
|
|
a: torch.Tensor,
|
|
b: torch.Tensor,
|
|
*,
|
|
ssm_states: torch.Tensor,
|
|
cache_indices: torch.Tensor,
|
|
query_start_loc: torch.Tensor,
|
|
intermediate_states_buffer: torch.Tensor,
|
|
intermediate_state_indices: torch.Tensor,
|
|
cache_steps: int,
|
|
retrieve_parent_token: torch.Tensor,
|
|
**kwargs,
|
|
) -> torch.Tensor:
|
|
# MTP verify using FlashInfer gated_delta_rule_mtp kernel (SM90 + SM100+).
|
|
if retrieve_parent_token is not None:
|
|
raise RuntimeError(
|
|
"FlashInfer GDN verify kernel only supports topk=1 "
|
|
"(retrieve_parent_token must be None)."
|
|
)
|
|
|
|
seq_len = q.shape[1]
|
|
batch_size = query_start_loc.shape[0] - 1
|
|
draft_token_num = seq_len // batch_size
|
|
|
|
num_heads = q.shape[2]
|
|
head_k_dim = q.shape[3]
|
|
num_v_heads = v.shape[2]
|
|
head_v_dim = v.shape[3]
|
|
|
|
query_mtp = q.view(batch_size, draft_token_num, num_heads, head_k_dim)
|
|
key_mtp = k.view(batch_size, draft_token_num, num_heads, head_k_dim)
|
|
value_mtp = v.view(batch_size, draft_token_num, num_v_heads, head_v_dim)
|
|
|
|
if a is None or b is None or A_log is None or dt_bias is None:
|
|
raise RuntimeError(
|
|
"FlashInfer GDN MTP kernel requires a, b, A_log, dt_bias."
|
|
)
|
|
|
|
a_mtp = a.view(batch_size, draft_token_num, num_v_heads)
|
|
b_mtp = b.view(batch_size, draft_token_num, num_v_heads)
|
|
|
|
intermediate_states_buffer_mtp = intermediate_states_buffer
|
|
if self.use_state_pool and intermediate_states_buffer is not None:
|
|
# The SM100 bf16 MTP kernel indexes this scratch buffer by the
|
|
# per-call batch id, while SGLang's speculative state cache is
|
|
# pool-scoped and may include an extra dummy slot.
|
|
intermediate_states_buffer_mtp = intermediate_states_buffer[:batch_size]
|
|
|
|
output_fi, _ = self._mtp_fn(
|
|
q=query_mtp,
|
|
k=key_mtp,
|
|
v=value_mtp,
|
|
initial_state=ssm_states,
|
|
initial_state_indices=cache_indices,
|
|
A_log=A_log.detach(),
|
|
a=a_mtp,
|
|
dt_bias=dt_bias.detach(),
|
|
b=b_mtp,
|
|
scale=None,
|
|
output=None,
|
|
intermediate_states_buffer=intermediate_states_buffer_mtp,
|
|
disable_state_update=True,
|
|
use_qk_l2norm=True,
|
|
)
|
|
|
|
return output_fi.view(1, seq_len, num_v_heads, head_v_dim)
|