chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,613 @@
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.layers.attention.fla.fused_gdn_gating import fused_gdn_gating
|
||||
from sglang.srt.layers.attention.hybrid_linear_attn_backend import MambaAttnBackendBase
|
||||
from sglang.srt.layers.attention.linear.kernels.gdn_triton import TritonGDNKernel
|
||||
from sglang.srt.layers.attention.linear.utils import (
|
||||
LinearAttnKernelBackend,
|
||||
get_linear_attn_decode_backend,
|
||||
get_linear_attn_prefill_backend,
|
||||
)
|
||||
from sglang.srt.layers.attention.mamba.causal_conv1d_triton import (
|
||||
causal_conv1d_fn,
|
||||
causal_conv1d_update,
|
||||
)
|
||||
from sglang.srt.layers.radix_linear_attention import RadixLinearAttention
|
||||
from sglang.srt.mem_cache.memory_pool import MambaPool
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||
from sglang.srt.model_executor.model_runner import ModelRunner
|
||||
from sglang.srt.utils import is_cpu, is_cuda, is_hip, is_npu
|
||||
from sglang.srt.utils.common import rank0_log
|
||||
|
||||
if not is_cpu():
|
||||
from sglang.srt.layers.attention.fla.chunk_delta_h import (
|
||||
CHUNK_SIZE as FLA_CHUNK_SIZE,
|
||||
)
|
||||
|
||||
if is_cuda() or is_hip():
|
||||
from sglang.jit_kernel.triton.gdn_fused_proj import fused_qkv_split_gdn_prefill
|
||||
|
||||
MAX_FUSED_QKV_SPLIT_DIM = 8192
|
||||
|
||||
if is_cuda():
|
||||
from sglang.srt.layers.attention.mamba.causal_conv1d import (
|
||||
causal_conv1d_fn as causal_conv1d_fn_cuda,
|
||||
)
|
||||
|
||||
causal_conv1d_fn = causal_conv1d_fn_cuda
|
||||
elif is_npu():
|
||||
from sgl_kernel_npu.fla.fused_gdn_gating import fused_gdn_gating_npu
|
||||
from sgl_kernel_npu.mamba.causal_conv1d import (
|
||||
causal_conv1d_fn_npu,
|
||||
causal_conv1d_update_npu,
|
||||
)
|
||||
|
||||
fused_gdn_gating = fused_gdn_gating_npu
|
||||
causal_conv1d_fn = causal_conv1d_fn_npu
|
||||
causal_conv1d_update = causal_conv1d_update_npu
|
||||
elif is_cpu():
|
||||
from sgl_kernel.mamba import causal_conv1d_fn_cpu, causal_conv1d_update_cpu
|
||||
|
||||
causal_conv1d_fn = causal_conv1d_fn_cpu
|
||||
causal_conv1d_update = causal_conv1d_update_cpu
|
||||
fused_gdn_gating = torch.ops.sgl_kernel.fused_gdn_gating_cpu
|
||||
|
||||
|
||||
def maybe_set_default_flashinfer_gdn_prefill(model_runner: ModelRunner) -> None:
|
||||
"""Use FlashInfer for the narrow SM100 GDN prefill domain we validated."""
|
||||
args = model_runner.server_args
|
||||
if (
|
||||
args.linear_attn_prefill_backend is not None
|
||||
or args.linear_attn_backend != "triton"
|
||||
or args.enable_page_major_kv_layout
|
||||
or not is_cuda()
|
||||
or torch.cuda.get_device_capability()[0] != 10
|
||||
):
|
||||
return
|
||||
|
||||
# Extra-buffer strategies need intermediate state checkpoints.
|
||||
if args.uses_mamba_radix_cache and args.mamba_radix_cache_strategy != "no_buffer":
|
||||
return
|
||||
|
||||
cuda_version = torch.version.cuda
|
||||
chunk_size = args.chunked_prefill_size
|
||||
config = model_runner.hybrid_gdn_config
|
||||
if (
|
||||
cuda_version is None
|
||||
or int(cuda_version.split(".", 1)[0]) < 13
|
||||
or args.enable_dynamic_chunking
|
||||
or chunk_size is None
|
||||
or not 1 <= chunk_size <= 8192
|
||||
or getattr(config, "linear_key_head_dim", None) != 128
|
||||
or getattr(config, "linear_value_head_dim", None) != 128
|
||||
or model_runner.req_to_token_pool.mamba_pool.mamba_cache.temporal.dtype
|
||||
!= torch.bfloat16
|
||||
):
|
||||
return
|
||||
|
||||
from sglang.srt.layers.attention.linear.kernels.gdn_flashinfer import (
|
||||
is_flashinfer_gdn_prefill_available,
|
||||
)
|
||||
|
||||
if is_flashinfer_gdn_prefill_available():
|
||||
args.linear_attn_prefill_backend = "flashinfer"
|
||||
rank0_log("Defaulting SM100 GDN prefill backend to FlashInfer.")
|
||||
|
||||
|
||||
class GDNKernelDispatcher:
|
||||
"""Dispatches GDN kernel calls to the appropriate backend per mode."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
decode_backend: LinearAttnKernelBackend,
|
||||
prefill_backend: LinearAttnKernelBackend,
|
||||
):
|
||||
triton_kernel = TritonGDNKernel()
|
||||
self.tree_verify_kernel = triton_kernel
|
||||
|
||||
cutedsl_kernel = None
|
||||
if decode_backend.is_triton():
|
||||
self.decode_kernel = triton_kernel
|
||||
elif decode_backend.is_cutedsl():
|
||||
if not is_cuda():
|
||||
raise ValueError("GDN CuTe DSL backend requires CUDA")
|
||||
from sglang.srt.layers.attention.linear.kernels.gdn_cutedsl import (
|
||||
CuteDSLGDNKernel,
|
||||
)
|
||||
|
||||
cutedsl_kernel = CuteDSLGDNKernel()
|
||||
self.decode_kernel = cutedsl_kernel
|
||||
elif decode_backend.is_flashinfer():
|
||||
if not is_cuda():
|
||||
raise ValueError("FlashInfer GDN backend requires CUDA")
|
||||
from sglang.srt.layers.attention.linear.kernels.gdn_flashinfer import (
|
||||
FlashInferGDNKernel,
|
||||
)
|
||||
|
||||
flashinfer_kernel = FlashInferGDNKernel()
|
||||
self.decode_kernel = flashinfer_kernel
|
||||
else:
|
||||
raise ValueError(f"Unsupported GDN decode backend: {decode_backend}")
|
||||
|
||||
if prefill_backend.is_triton():
|
||||
self.extend_kernel = triton_kernel
|
||||
elif prefill_backend.is_cutedsl():
|
||||
if not is_cuda():
|
||||
raise ValueError("GDN CuTe DSL backend requires CUDA")
|
||||
# Reuse the CuteDSL kernel if already created for decode
|
||||
if cutedsl_kernel is None:
|
||||
from sglang.srt.layers.attention.linear.kernels.gdn_cutedsl import (
|
||||
CuteDSLGDNKernel,
|
||||
)
|
||||
|
||||
cutedsl_kernel = CuteDSLGDNKernel()
|
||||
# The CuteDSL prefill kernel only exists on SM100+ (Blackwell).
|
||||
# On SM90 (Hopper) fall back to Triton so users can pick
|
||||
# `cutedsl` uniformly across hardware.
|
||||
if cutedsl_kernel.supports_prefill:
|
||||
self.extend_kernel = cutedsl_kernel
|
||||
else:
|
||||
rank0_log(
|
||||
"CuTe DSL GDN prefill is not supported on this GPU "
|
||||
"(requires SM100+). Falling back to Triton for prefill."
|
||||
)
|
||||
self.extend_kernel = triton_kernel
|
||||
elif prefill_backend.is_flashinfer():
|
||||
if not is_cuda():
|
||||
raise ValueError("FlashInfer GDN backend requires CUDA")
|
||||
# Reuse the FlashInfer kernel if already created for decode
|
||||
if decode_backend.is_flashinfer():
|
||||
self.extend_kernel = flashinfer_kernel
|
||||
else:
|
||||
from sglang.srt.layers.attention.linear.kernels.gdn_flashinfer import (
|
||||
FlashInferGDNKernel,
|
||||
)
|
||||
|
||||
flashinfer_kernel = FlashInferGDNKernel()
|
||||
self.extend_kernel = flashinfer_kernel
|
||||
else:
|
||||
raise ValueError(f"Unsupported GDN prefill backend: {prefill_backend}")
|
||||
|
||||
# Verify kernel: use FlashInfer when the selected FlashInfer kernel
|
||||
# supports MTP verify. SM90 uses the fp32-state path; SM100 uses the
|
||||
# bf16-state adapter in FlashInferGDNKernel.
|
||||
if (
|
||||
decode_backend.is_flashinfer() or prefill_backend.is_flashinfer()
|
||||
) and flashinfer_kernel.supports_target_verify:
|
||||
self.verify_kernel = flashinfer_kernel
|
||||
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"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(
|
||||
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 self.decode_kernel.decode(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
a,
|
||||
b,
|
||||
A_log=A_log,
|
||||
dt_bias=dt_bias,
|
||||
ssm_states=ssm_states,
|
||||
cache_indices=cache_indices,
|
||||
query_start_loc=query_start_loc,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
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:
|
||||
return self.extend_kernel.extend(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
g,
|
||||
beta,
|
||||
ssm_states=ssm_states,
|
||||
cache_indices=cache_indices,
|
||||
query_start_loc=query_start_loc,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
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,
|
||||
**kwargs,
|
||||
) -> torch.Tensor:
|
||||
# FlashInfer verify supports a linear MTP chain. Tree-shaped drafts
|
||||
# carry parent indices and must use Triton even when decode/prefill use
|
||||
# FlashInfer.
|
||||
verify_kernel = (
|
||||
self.tree_verify_kernel
|
||||
if kwargs.get("retrieve_parent_token") is not None
|
||||
else self.verify_kernel
|
||||
)
|
||||
return verify_kernel.target_verify(
|
||||
A_log=A_log,
|
||||
dt_bias=dt_bias,
|
||||
q=q,
|
||||
k=k,
|
||||
v=v,
|
||||
a=a,
|
||||
b=b,
|
||||
ssm_states=ssm_states,
|
||||
cache_indices=cache_indices,
|
||||
query_start_loc=query_start_loc,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class GDNAttnBackend(MambaAttnBackendBase):
|
||||
"""Attention backend for GDN (Gated Delta Network) linear attention."""
|
||||
|
||||
needs_cpu_seq_lens: bool = False
|
||||
|
||||
def __init__(self, model_runner: ModelRunner):
|
||||
super().__init__(model_runner)
|
||||
self.conv_states_shape = (
|
||||
model_runner.req_to_token_pool.mamba_pool.mamba_cache.conv[0].shape
|
||||
)
|
||||
if not is_cpu() and not is_npu():
|
||||
assert (
|
||||
self.conv_states_shape[-1] < FLA_CHUNK_SIZE
|
||||
), f"{self.conv_states_shape[-1]=} should be less than {FLA_CHUNK_SIZE}"
|
||||
|
||||
decode_backend = get_linear_attn_decode_backend()
|
||||
prefill_backend = get_linear_attn_prefill_backend()
|
||||
self.kernel_dispatcher = GDNKernelDispatcher(decode_backend, prefill_backend)
|
||||
self.verify_intermediate_state_indices = torch.arange(
|
||||
self.req_to_token_pool.size, dtype=torch.int32, device=model_runner.device
|
||||
)
|
||||
|
||||
def init_forward_metadata(self, forward_batch: ForwardBatch):
|
||||
super().init_forward_metadata(forward_batch)
|
||||
if self.forward_metadata.has_mamba_track_mask:
|
||||
self.forward_metadata.mamba_track_mask_indices = (
|
||||
forward_batch.mamba_track_mask.nonzero(as_tuple=True)[0]
|
||||
)
|
||||
self.forward_metadata.conv_states_mask_indices = (
|
||||
forward_batch.mamba_track_indices[
|
||||
self.forward_metadata.mamba_track_mask_indices
|
||||
]
|
||||
)
|
||||
|
||||
def forward_decode(
|
||||
self,
|
||||
layer: RadixLinearAttention,
|
||||
forward_batch: ForwardBatch,
|
||||
mixed_qkv: Union[torch.Tensor, Tuple[torch.Tensor, ...]],
|
||||
a: torch.Tensor,
|
||||
b: torch.Tensor,
|
||||
**kwargs,
|
||||
):
|
||||
layer_cache = self.req_to_token_pool.mamba2_layer_cache(layer.layer_id)
|
||||
conv_states = layer_cache.conv[0]
|
||||
ssm_states = layer_cache.temporal
|
||||
query_start_loc = self.forward_metadata.query_start_loc
|
||||
cache_indices = self.forward_metadata.mamba_cache_indices
|
||||
# GDN ReplaySSM (slice 1a): per-layer ring slices + the once-per-forward
|
||||
# per-row write cursor. All None unless --enable-linear-replayssm, so the
|
||||
# legacy dispatch below is byte-identical when the flag is off.
|
||||
replayssm_write_pos = self.forward_metadata.replayssm_write_pos
|
||||
# GDN ReplaySSM (slice 2b): per-row force-flush at radix track
|
||||
# boundaries (None unless --enable-linear-replayssm). When present the
|
||||
# kernel folds the ring into temporal[slot] on the snapshot steps.
|
||||
replayssm_force_flush = self.forward_metadata.replayssm_force_flush
|
||||
replayssm_d = layer_cache.replayssm_d
|
||||
replayssm_k = layer_cache.replayssm_k
|
||||
replayssm_g = layer_cache.replayssm_g
|
||||
|
||||
assert isinstance(mixed_qkv, torch.Tensor)
|
||||
mixed_qkv = causal_conv1d_update(
|
||||
mixed_qkv,
|
||||
conv_states,
|
||||
layer.conv_weights,
|
||||
layer.bias,
|
||||
layer.activation,
|
||||
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,
|
||||
replayssm_d=replayssm_d,
|
||||
replayssm_k=replayssm_k,
|
||||
replayssm_g=replayssm_g,
|
||||
replayssm_write_pos=replayssm_write_pos,
|
||||
replayssm_force_flush=replayssm_force_flush,
|
||||
)
|
||||
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],
|
||||
dim=-1,
|
||||
)
|
||||
# Reshape from [bs, h*d] to [1, bs, h, d]
|
||||
bs = forward_batch.batch_size
|
||||
query = query.view(1, bs, layer.num_q_heads, layer.head_q_dim)
|
||||
key = key.view(1, bs, layer.num_k_heads, layer.head_k_dim)
|
||||
value = value.view(1, bs, layer.num_v_heads, layer.head_v_dim)
|
||||
|
||||
core_attn_out = self.kernel_dispatcher.decode(
|
||||
q=query,
|
||||
k=key,
|
||||
v=value,
|
||||
a=a,
|
||||
b=b,
|
||||
A_log=layer.A_log,
|
||||
dt_bias=layer.dt_bias,
|
||||
ssm_states=ssm_states,
|
||||
cache_indices=cache_indices,
|
||||
query_start_loc=query_start_loc,
|
||||
)
|
||||
|
||||
self._track_mamba_state_decode(
|
||||
forward_batch, conv_states, ssm_states, cache_indices
|
||||
)
|
||||
|
||||
return core_attn_out
|
||||
|
||||
def forward_extend(
|
||||
self,
|
||||
layer: RadixLinearAttention,
|
||||
forward_batch: ForwardBatch,
|
||||
mixed_qkv: Union[torch.Tensor, Tuple[torch.Tensor, ...]],
|
||||
a: torch.Tensor,
|
||||
b: torch.Tensor,
|
||||
**kwargs,
|
||||
):
|
||||
assert isinstance(mixed_qkv, torch.Tensor)
|
||||
seq_len = mixed_qkv.shape[0]
|
||||
|
||||
is_target_verify = forward_batch.forward_mode.is_target_verify()
|
||||
forward_metadata = self.forward_metadata
|
||||
|
||||
query_start_loc = forward_metadata.query_start_loc
|
||||
cache_indices = forward_metadata.mamba_cache_indices
|
||||
retrieve_next_token = forward_metadata.retrieve_next_token
|
||||
retrieve_next_sibling = forward_metadata.retrieve_next_sibling
|
||||
retrieve_parent_token = forward_metadata.retrieve_parent_token
|
||||
|
||||
mamba_cache_params = self.req_to_token_pool.mamba2_layer_cache(layer.layer_id)
|
||||
conv_states = mamba_cache_params.conv[0]
|
||||
ssm_states = mamba_cache_params.temporal
|
||||
if is_target_verify:
|
||||
assert isinstance(mamba_cache_params, MambaPool.SpeculativeState)
|
||||
intermediate_state_cache = mamba_cache_params.intermediate_ssm
|
||||
intermediate_conv_window_cache = (
|
||||
mamba_cache_params.intermediate_conv_window[0]
|
||||
)
|
||||
intermediate_state_indices = self.verify_intermediate_state_indices
|
||||
else:
|
||||
has_initial_states = forward_batch.extend_prefix_lens > 0
|
||||
|
||||
# Page-major envelope: the prefill kernels (CUDA causal_conv1d_fwd,
|
||||
# chunk_gated_delta_rule) write state back in place assuming a contiguous
|
||||
# slot layout, so they silently drop the write to the strided envelope
|
||||
# pool. Run them on contiguous per-sequence copies (identity-indexed) and
|
||||
# scatter the result back. No-op for the default contiguous pool.
|
||||
# TODO(ch-wan): drop these .contiguous() copies by making the prefill conv
|
||||
# and chunk_gated_delta_rule kernels honor the pool's real slot stride +
|
||||
# int64 indexing, like packed_decode / causal_conv1d_update already do.
|
||||
needs_state_gather = (not is_target_verify) and (
|
||||
not conv_states.is_contiguous() or not ssm_states.is_contiguous()
|
||||
)
|
||||
if needs_state_gather:
|
||||
conv_states_contig = conv_states[cache_indices].contiguous()
|
||||
ssm_states_contig = ssm_states[cache_indices].contiguous()
|
||||
state_cache_indices = torch.arange(
|
||||
cache_indices.shape[0],
|
||||
device=cache_indices.device,
|
||||
dtype=cache_indices.dtype,
|
||||
)
|
||||
else:
|
||||
conv_states_contig = conv_states
|
||||
ssm_states_contig = ssm_states
|
||||
state_cache_indices = cache_indices
|
||||
|
||||
if is_target_verify:
|
||||
batch_size = seq_len // forward_batch.spec_info.draft_token_num
|
||||
draft_token_num = forward_batch.spec_info.draft_token_num
|
||||
mixed_qkv_reshaped = mixed_qkv.view(
|
||||
batch_size, draft_token_num, -1
|
||||
).transpose(1, 2)
|
||||
mixed_qkv_processed = causal_conv1d_update(
|
||||
mixed_qkv_reshaped,
|
||||
conv_states,
|
||||
layer.conv_weights,
|
||||
layer.bias,
|
||||
layer.activation,
|
||||
conv_state_indices=cache_indices[:batch_size],
|
||||
intermediate_conv_window=intermediate_conv_window_cache,
|
||||
intermediate_state_indices=intermediate_state_indices[:batch_size],
|
||||
retrieve_next_token=retrieve_next_token,
|
||||
retrieve_next_sibling=retrieve_next_sibling,
|
||||
retrieve_parent_token=retrieve_parent_token,
|
||||
)
|
||||
mixed_qkv = mixed_qkv_processed.transpose(1, 2).view(seq_len, -1)
|
||||
else:
|
||||
mixed_qkv = mixed_qkv.transpose(0, 1)
|
||||
if forward_metadata.has_mamba_track_mask:
|
||||
mixed_qkv_to_track = mixed_qkv[
|
||||
:, forward_metadata.track_conv_indices
|
||||
].transpose(0, 1)
|
||||
conv_states[forward_metadata.conv_states_mask_indices] = (
|
||||
mixed_qkv_to_track
|
||||
)
|
||||
|
||||
mixed_qkv = causal_conv1d_fn(
|
||||
mixed_qkv,
|
||||
layer.conv_weights,
|
||||
layer.bias,
|
||||
activation=layer.activation,
|
||||
conv_states=conv_states_contig,
|
||||
has_initial_state=has_initial_states,
|
||||
cache_indices=state_cache_indices,
|
||||
query_start_loc=query_start_loc,
|
||||
seq_lens_cpu=forward_batch.extend_seq_lens_cpu,
|
||||
).transpose(0, 1)[:seq_len]
|
||||
|
||||
actual_seq_len = mixed_qkv.shape[0]
|
||||
qkv_dim = layer.q_dim + layer.k_dim + layer.v_dim
|
||||
if (is_cuda() or is_hip()) and qkv_dim <= MAX_FUSED_QKV_SPLIT_DIM:
|
||||
query, key, value = fused_qkv_split_gdn_prefill(
|
||||
mixed_qkv,
|
||||
layer.num_q_heads,
|
||||
layer.num_k_heads,
|
||||
layer.num_v_heads,
|
||||
layer.head_q_dim,
|
||||
layer.head_k_dim,
|
||||
layer.head_v_dim,
|
||||
)
|
||||
else:
|
||||
query, key, value = torch.split(
|
||||
mixed_qkv,
|
||||
[layer.q_dim, layer.k_dim, layer.v_dim],
|
||||
dim=-1,
|
||||
)
|
||||
query = query.view(1, actual_seq_len, layer.num_q_heads, layer.head_q_dim)
|
||||
key = key.view(1, actual_seq_len, layer.num_k_heads, layer.head_k_dim)
|
||||
value = value.view(1, actual_seq_len, layer.num_v_heads, layer.head_v_dim)
|
||||
|
||||
if is_target_verify:
|
||||
core_attn_out = self.kernel_dispatcher.target_verify(
|
||||
A_log=layer.A_log,
|
||||
dt_bias=layer.dt_bias,
|
||||
q=query,
|
||||
k=key,
|
||||
v=value,
|
||||
a=a,
|
||||
b=b,
|
||||
ssm_states=ssm_states,
|
||||
cache_indices=cache_indices,
|
||||
query_start_loc=query_start_loc,
|
||||
intermediate_states_buffer=intermediate_state_cache,
|
||||
intermediate_state_indices=intermediate_state_indices,
|
||||
cache_steps=forward_batch.spec_info.draft_token_num,
|
||||
retrieve_parent_token=retrieve_parent_token,
|
||||
)
|
||||
else:
|
||||
g, beta = fused_gdn_gating(layer.A_log, a, b, layer.dt_bias)
|
||||
core_attn_out, last_recurrent_state, h = self.kernel_dispatcher.extend(
|
||||
q=query,
|
||||
k=key,
|
||||
v=value,
|
||||
g=g,
|
||||
beta=beta,
|
||||
ssm_states=ssm_states_contig,
|
||||
cache_indices=state_cache_indices,
|
||||
query_start_loc=query_start_loc,
|
||||
)
|
||||
|
||||
if is_npu() and last_recurrent_state is not None:
|
||||
last_recurrent_state = last_recurrent_state.to(
|
||||
ssm_states.dtype, copy=False
|
||||
)
|
||||
ssm_states[cache_indices] = last_recurrent_state
|
||||
|
||||
if needs_state_gather:
|
||||
# Scatter the in-place-updated contiguous copies back to the
|
||||
# strided envelope pool (advanced indexing handles the strides).
|
||||
conv_states[cache_indices] = conv_states_contig
|
||||
ssm_states[cache_indices] = ssm_states_contig
|
||||
|
||||
if h is not None:
|
||||
self._track_mamba_state_extend(
|
||||
forward_batch, h, ssm_states, forward_metadata
|
||||
)
|
||||
|
||||
return core_attn_out
|
||||
@@ -0,0 +1,387 @@
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.layers.attention.hybrid_linear_attn_backend import MambaAttnBackendBase
|
||||
from sglang.srt.layers.attention.linear.kernels.kda_triton import TritonKDAKernel
|
||||
from sglang.srt.layers.attention.linear.utils import (
|
||||
LinearAttnKernelBackend,
|
||||
get_linear_attn_decode_backend,
|
||||
get_linear_attn_prefill_backend,
|
||||
)
|
||||
from sglang.srt.layers.attention.mamba.causal_conv1d_triton import (
|
||||
causal_conv1d_fn,
|
||||
causal_conv1d_update,
|
||||
)
|
||||
from sglang.srt.layers.radix_linear_attention import RadixLinearAttention
|
||||
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).
|
||||
# Only causal_conv1d_update needs platform-specific overrides for decode.
|
||||
if is_npu():
|
||||
from sgl_kernel_npu.mamba.causal_conv1d import causal_conv1d_update_npu
|
||||
|
||||
causal_conv1d_update = causal_conv1d_update_npu
|
||||
elif is_cpu():
|
||||
from sgl_kernel.mamba import causal_conv1d_update_cpu
|
||||
|
||||
causal_conv1d_update = causal_conv1d_update_cpu
|
||||
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||
from sglang.srt.model_executor.model_runner import ModelRunner
|
||||
|
||||
|
||||
class KDAKernelDispatcher:
|
||||
"""Dispatches KDA kernel calls to the appropriate backend per mode."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
decode_backend: LinearAttnKernelBackend,
|
||||
prefill_backend: LinearAttnKernelBackend,
|
||||
):
|
||||
triton_kernel = TritonKDAKernel()
|
||||
|
||||
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")
|
||||
from sglang.srt.layers.attention.linear.kernels.kda_cutedsl import (
|
||||
CuteDSLKDAKernel,
|
||||
)
|
||||
|
||||
self.decode_kernel = CuteDSLKDAKernel()
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported KDA decode backend: {decode_backend}. "
|
||||
"KDA currently only supports 'triton'."
|
||||
)
|
||||
|
||||
if prefill_backend.is_triton():
|
||||
self.extend_kernel = triton_kernel
|
||||
elif prefill_backend.is_flashkda():
|
||||
from sglang.srt.layers.attention.linear.kernels.kda_flashkda import (
|
||||
FlashKDAKernel,
|
||||
)
|
||||
|
||||
self.extend_kernel = FlashKDAKernel()
|
||||
elif prefill_backend.is_cutedsl():
|
||||
if not is_cuda():
|
||||
raise ValueError("KDA CuTe DSL backend requires CUDA")
|
||||
from sglang.srt.layers.attention.linear.kernels.kda_cutedsl import (
|
||||
CuteDSLKDAKernel,
|
||||
)
|
||||
|
||||
cutedsl_kernel = CuteDSLKDAKernel()
|
||||
if getattr(cutedsl_kernel, "supports_prefill", False):
|
||||
# SM100 chunk prefill pipeline.
|
||||
self.extend_kernel = cutedsl_kernel
|
||||
else:
|
||||
# CuTe DSL prefill kernels need SM100 (Blackwell); on older GPUs
|
||||
# fall back to the Triton chunk kernel.
|
||||
self.extend_kernel = triton_kernel
|
||||
rank0_log(
|
||||
"KDA cutedsl prefill needs SM100; falling back to Triton extend."
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported KDA prefill backend: {prefill_backend}. "
|
||||
"KDA supports 'triton', 'flashkda', or 'cutedsl' "
|
||||
"(cutedsl prefill needs SM100)."
|
||||
)
|
||||
|
||||
self.supports_packed_decode = getattr(
|
||||
self.decode_kernel, "supports_packed_decode", False
|
||||
)
|
||||
|
||||
rank0_log(
|
||||
f"KDA kernel dispatcher: decode={self.decode_kernel.__class__.__name__}, "
|
||||
f"extend={self.extend_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(
|
||||
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 self.decode_kernel.decode(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
a,
|
||||
b,
|
||||
A_log=A_log,
|
||||
dt_bias=dt_bias,
|
||||
ssm_states=ssm_states,
|
||||
cache_indices=cache_indices,
|
||||
query_start_loc=query_start_loc,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
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,
|
||||
) -> torch.Tensor:
|
||||
return self.extend_kernel.extend(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
g,
|
||||
beta,
|
||||
ssm_states=ssm_states,
|
||||
cache_indices=cache_indices,
|
||||
query_start_loc=query_start_loc,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class KDAAttnBackend(MambaAttnBackendBase):
|
||||
"""Attention backend for KDA (Kimi Delta Attention) linear attention."""
|
||||
|
||||
def __init__(self, model_runner: ModelRunner):
|
||||
super().__init__(model_runner)
|
||||
decode_backend = get_linear_attn_decode_backend()
|
||||
prefill_backend = get_linear_attn_prefill_backend()
|
||||
self.kernel_dispatcher = KDAKernelDispatcher(decode_backend, prefill_backend)
|
||||
|
||||
def forward_decode(
|
||||
self,
|
||||
layer: RadixLinearAttention,
|
||||
mixed_qkv: Union[torch.Tensor, Tuple[torch.Tensor, ...]],
|
||||
a: torch.Tensor,
|
||||
b: torch.Tensor,
|
||||
**kwargs,
|
||||
):
|
||||
layer_cache = self.req_to_token_pool.mamba2_layer_cache(layer.layer_id)
|
||||
conv_states = layer_cache.conv[0]
|
||||
ssm_states = layer_cache.temporal
|
||||
query_start_loc = self.forward_metadata.query_start_loc
|
||||
cache_indices = self.forward_metadata.mamba_cache_indices
|
||||
|
||||
# ReplaySSM ring: per-layer ring slices + the once-per-forward per-row
|
||||
# write cursor. All None unless --enable-linear-replayssm, so packed_decode
|
||||
# falls through to the byte-identical legacy KDA path. KDA ships WITHOUT
|
||||
# radix coordination for now, so force_flush is None/zeroed (the ring
|
||||
# flushes only at the natural write_pos == L-1 wrap; set in the shared
|
||||
# HybridLinearAttn metadata, which zeroes force_flush for KDA models).
|
||||
# NOTE: ReplaySSM decode is a GDN (scalar-gate) bandwidth win; on KDA the
|
||||
# per-K g_cache is K x larger and the reconstruction refolds the per-K
|
||||
# decay every step, so it is correct but SLOWER than packed (a measured
|
||||
# decode regression). Kept wired for correctness + the spec-decode path;
|
||||
# not recommended for KDA decode. Revisit on Blackwell (more tensor-core
|
||||
# throughput may flip the compute/bandwidth tradeoff).
|
||||
replayssm_write_pos = getattr(
|
||||
self.forward_metadata, "replayssm_write_pos", None
|
||||
)
|
||||
replayssm_force_flush = getattr(
|
||||
self.forward_metadata, "replayssm_force_flush", None
|
||||
)
|
||||
replayssm_d = layer_cache.replayssm_d
|
||||
replayssm_k = layer_cache.replayssm_k
|
||||
replayssm_g = layer_cache.replayssm_g
|
||||
|
||||
qkv = causal_conv1d_update(
|
||||
mixed_qkv,
|
||||
conv_states.transpose(-1, -2),
|
||||
layer.conv_weights,
|
||||
layer.bias,
|
||||
activation="silu",
|
||||
conv_state_indices=cache_indices,
|
||||
)
|
||||
|
||||
# Skip split + reshape by consuming the packed mixed_qkv directly in a
|
||||
# single fused Triton kernel (KDA per-K gate variant of GDN PR #20627).
|
||||
#
|
||||
# The packed kernel hard-assumes one token per sequence (T=1): it has no
|
||||
# query_start_loc / per-sequence loop. forward_decode is only entered in
|
||||
# decode mode (see HybridLinearAttnBackend.forward dispatch), where each
|
||||
# request contributes exactly one token, so #tokens == #requests. Multi-
|
||||
# token-per-seq speculative paths (target_verify / draft_extend) go
|
||||
# through forward_extend instead. Assert the invariant so a future
|
||||
# routing change fails loudly rather than silently corrupting state.
|
||||
if self.kernel_dispatcher.supports_packed_decode:
|
||||
assert qkv.shape[0] == cache_indices.shape[0], (
|
||||
"KDA packed decode requires one token per sequence (T=1): "
|
||||
f"got {qkv.shape[0]} tokens for {cache_indices.shape[0]} requests."
|
||||
)
|
||||
return self.kernel_dispatcher.packed_decode(
|
||||
mixed_qkv=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,
|
||||
replayssm_d=replayssm_d,
|
||||
replayssm_k=replayssm_k,
|
||||
replayssm_g=replayssm_g,
|
||||
replayssm_write_pos=replayssm_write_pos,
|
||||
replayssm_force_flush=replayssm_force_flush,
|
||||
)
|
||||
|
||||
q, k, v = qkv.split([layer.q_dim, layer.k_dim, layer.v_dim], dim=-1)
|
||||
q = q.unflatten(-1, (-1, layer.head_q_dim)).unsqueeze(0) # n (h d) -> 1 n h d
|
||||
k = k.unflatten(-1, (-1, layer.head_k_dim)).unsqueeze(0) # n (h d) -> 1 n h d
|
||||
v = v.unflatten(-1, (-1, layer.head_v_dim)).unsqueeze(0) # n (h d) -> 1 n h d
|
||||
|
||||
return self.kernel_dispatcher.decode(
|
||||
q=q,
|
||||
k=k,
|
||||
v=v,
|
||||
a=a,
|
||||
b=b,
|
||||
A_log=layer.A_log,
|
||||
dt_bias=layer.dt_bias,
|
||||
ssm_states=ssm_states,
|
||||
cache_indices=cache_indices,
|
||||
query_start_loc=query_start_loc,
|
||||
)
|
||||
|
||||
def forward_extend(
|
||||
self,
|
||||
layer: RadixLinearAttention,
|
||||
forward_batch: ForwardBatch,
|
||||
mixed_qkv: Union[torch.Tensor, Tuple[torch.Tensor, ...]],
|
||||
a: torch.Tensor,
|
||||
b: torch.Tensor,
|
||||
**kwargs,
|
||||
):
|
||||
query_start_loc = self.forward_metadata.query_start_loc
|
||||
cache_indices = self.forward_metadata.mamba_cache_indices
|
||||
|
||||
mamba_cache_params = self.req_to_token_pool.mamba2_layer_cache(layer.layer_id)
|
||||
conv_states = mamba_cache_params.conv[0].transpose(-1, -2)
|
||||
|
||||
ssm_states = mamba_cache_params.temporal
|
||||
|
||||
has_initial_state = forward_batch.extend_prefix_lens > 0
|
||||
|
||||
splits = [layer.q_dim, layer.k_dim, layer.v_dim]
|
||||
q, k, v = mixed_qkv.transpose(0, 1).split(splits, dim=0)
|
||||
q_conv_weight, k_conv_weight, v_conv_weight = layer.conv_weights.split(
|
||||
splits, dim=0
|
||||
)
|
||||
q_conv_state, k_conv_state, v_conv_state = conv_states.split(splits, dim=-2)
|
||||
if layer.bias is not None:
|
||||
q_bias, k_bias, v_bias = layer.bias.split(splits, dim=0)
|
||||
else:
|
||||
q_bias, k_bias, v_bias = None, None, None
|
||||
|
||||
q = causal_conv1d_fn(
|
||||
q,
|
||||
q_conv_weight,
|
||||
q_bias,
|
||||
activation="silu",
|
||||
conv_states=q_conv_state,
|
||||
has_initial_state=has_initial_state,
|
||||
cache_indices=cache_indices,
|
||||
query_start_loc=query_start_loc,
|
||||
seq_lens_cpu=forward_batch.extend_seq_lens_cpu,
|
||||
).transpose(0, 1)
|
||||
k = causal_conv1d_fn(
|
||||
k,
|
||||
k_conv_weight,
|
||||
k_bias,
|
||||
activation="silu",
|
||||
conv_states=k_conv_state,
|
||||
has_initial_state=has_initial_state,
|
||||
cache_indices=cache_indices,
|
||||
query_start_loc=query_start_loc,
|
||||
seq_lens_cpu=forward_batch.extend_seq_lens_cpu,
|
||||
).transpose(0, 1)
|
||||
v = causal_conv1d_fn(
|
||||
v,
|
||||
v_conv_weight,
|
||||
v_bias,
|
||||
activation="silu",
|
||||
conv_states=v_conv_state,
|
||||
has_initial_state=has_initial_state,
|
||||
cache_indices=cache_indices,
|
||||
query_start_loc=query_start_loc,
|
||||
seq_lens_cpu=forward_batch.extend_seq_lens_cpu,
|
||||
).transpose(0, 1)
|
||||
|
||||
q = q.unflatten(-1, (-1, layer.head_q_dim)).unsqueeze(0) # n (h d) -> 1 n h d
|
||||
k = k.unflatten(-1, (-1, layer.head_k_dim)).unsqueeze(0) # n (h d) -> 1 n h d
|
||||
v = v.unflatten(-1, (-1, layer.head_v_dim)).unsqueeze(0) # n (h d) -> 1 n h d
|
||||
|
||||
core_attn_out = self.kernel_dispatcher.extend(
|
||||
q=q,
|
||||
k=k,
|
||||
v=v,
|
||||
g=a,
|
||||
beta=b,
|
||||
ssm_states=ssm_states,
|
||||
cache_indices=cache_indices,
|
||||
query_start_loc=query_start_loc,
|
||||
A_log=layer.A_log,
|
||||
dt_bias=layer.dt_bias,
|
||||
lower_bound=getattr(layer, "lower_bound", None),
|
||||
extend_seq_lens_cpu=forward_batch.extend_seq_lens_cpu,
|
||||
# target_verify / draft_extend_v2 also reach forward_extend; they must
|
||||
# stay rollback-able, so a kernel that commits state in place (e.g.
|
||||
# FlashKDA) must not run for them.
|
||||
is_spec_decode=(
|
||||
forward_batch.forward_mode.is_target_verify()
|
||||
or forward_batch.forward_mode.is_draft_extend_v2()
|
||||
),
|
||||
)
|
||||
|
||||
return core_attn_out
|
||||
@@ -0,0 +1,251 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adapted from https://github.com/vllm-project/vllm/blob/4868b542c9dfd166662eecc4bb8be3a36a3feaa2/vllm/model_executor/layers/mamba/ops/gdn_chunk_cutedsl/__init__.py
|
||||
|
||||
from functools import cache
|
||||
|
||||
import cutlass
|
||||
import torch
|
||||
import triton
|
||||
from cuda.bindings.driver import CUstream
|
||||
from cutlass import Int32, cute
|
||||
from quack.compile_utils import make_fake_tensor
|
||||
|
||||
from .kernel_h import h_cutedsl
|
||||
from .kernel_kkt_inv_uw import kkt_inv_uw_cutedsl
|
||||
from .kernel_o import o_cutedsl
|
||||
|
||||
|
||||
class PrepMetaKernel:
|
||||
def __init__(self, BT: int) -> None:
|
||||
self.BT = BT
|
||||
self.num_warps = 8
|
||||
|
||||
@cute.jit
|
||||
def __call__(
|
||||
self,
|
||||
cu_seqlens: cute.Tensor,
|
||||
chunk_indices: cute.Tensor,
|
||||
chunk_offsets: cute.Tensor,
|
||||
stream: CUstream,
|
||||
):
|
||||
block = (self.num_warps * 32, 1, 1)
|
||||
self.kernel(
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
chunk_offsets,
|
||||
).launch(grid=(1, 1, 1), block=block, stream=stream)
|
||||
|
||||
@cute.kernel
|
||||
def kernel(
|
||||
self,
|
||||
cu_seqlens: cute.Tensor,
|
||||
chunk_indices: cute.Tensor,
|
||||
chunk_offsets: cute.Tensor,
|
||||
):
|
||||
tid, _, _ = cute.arch.thread_idx()
|
||||
warp_id = cute.arch.make_warp_uniform(tid // 32)
|
||||
lane_id = tid % 32
|
||||
|
||||
num_seqs = cu_seqlens.shape[0] - 1
|
||||
num_warps = self.num_warps
|
||||
tb_size = num_warps * 32
|
||||
|
||||
if tid == 0:
|
||||
chunk_offsets[0] = 0
|
||||
|
||||
coarsen = cute.ceil_div(num_seqs, tb_size)
|
||||
seq_start = tid * coarsen
|
||||
num_iters = cutlass.min(seq_start + coarsen, num_seqs) - seq_start
|
||||
|
||||
# First pass: compute this thread's total chunk count.
|
||||
thread_sum = Int32(0)
|
||||
for i in range(num_iters):
|
||||
seq_id = seq_start + i
|
||||
seqlen = cu_seqlens[seq_id + 1] - cu_seqlens[seq_id]
|
||||
thread_sum += cute.ceil_div(seqlen, self.BT)
|
||||
|
||||
# warp parallel scan
|
||||
cu_num_chunks = thread_sum
|
||||
for i in cutlass.range_constexpr(5):
|
||||
offset = cutlass.const_expr(1 << i)
|
||||
lower = cute.arch.shuffle_sync_up(
|
||||
cu_num_chunks, offset=offset, mask_and_clamp=0
|
||||
)
|
||||
if lane_id >= offset:
|
||||
cu_num_chunks += lower
|
||||
|
||||
# cross-warp cumsum (CTA-wide)
|
||||
smem = cutlass.utils.SmemAllocator()
|
||||
warp_num_chunks = smem.allocate_array(Int32, num_warps)
|
||||
if lane_id == 31:
|
||||
warp_num_chunks[warp_id] = cu_num_chunks
|
||||
cute.arch.sync_threads()
|
||||
|
||||
for i in cutlass.range_constexpr(1, num_warps):
|
||||
if warp_id >= i:
|
||||
cu_num_chunks += warp_num_chunks[i - 1]
|
||||
|
||||
chunk_start = cu_num_chunks - thread_sum
|
||||
|
||||
# Second pass: recompute per-sequence chunk counts and write results.
|
||||
for i in range(num_iters):
|
||||
seq_id = seq_start + i
|
||||
seqlen = cu_seqlens[seq_id + 1] - cu_seqlens[seq_id]
|
||||
num_chunks = cute.ceil_div(seqlen, self.BT)
|
||||
chunk_end = chunk_start + num_chunks
|
||||
chunk_offsets[seq_id + 1] = chunk_end
|
||||
|
||||
for chunk_id in range(num_chunks):
|
||||
chunk_indices[chunk_start + chunk_id, 0] = seq_id
|
||||
chunk_indices[chunk_start + chunk_id, 1] = chunk_id
|
||||
|
||||
chunk_start = chunk_end
|
||||
|
||||
@cache
|
||||
@staticmethod
|
||||
def compile(BT: int):
|
||||
cu_entries = cute.sym_int()
|
||||
upper_bound_chunks = cute.sym_int()
|
||||
|
||||
cu_seqlens = make_fake_tensor(Int32, (cu_entries,), divisibility=1)
|
||||
chunk_indices = make_fake_tensor(Int32, (upper_bound_chunks, 2), divisibility=2)
|
||||
chunk_offsets = make_fake_tensor(Int32, (cu_entries,), divisibility=1)
|
||||
|
||||
kernel = PrepMetaKernel(BT)
|
||||
stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
|
||||
return cute.compile(
|
||||
kernel,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
chunk_offsets,
|
||||
stream,
|
||||
options="--enable-tvm-ffi",
|
||||
)
|
||||
|
||||
|
||||
def _upper_bound_chunks(num_seqs: int, total_tokens: int, chunk_size: int) -> int:
|
||||
return (num_seqs - 1) + triton.cdiv(total_tokens - (num_seqs - 1), chunk_size)
|
||||
|
||||
|
||||
def prepare_metadata_cutedsl(
|
||||
cu_seqlens: torch.Tensor,
|
||||
total_tokens: int,
|
||||
chunk_size: int = 64,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
num_seqs = cu_seqlens.numel() - 1
|
||||
upper_bound_chunks = _upper_bound_chunks(num_seqs, total_tokens, chunk_size)
|
||||
chunk_offsets = cu_seqlens.new_empty(num_seqs + 1, dtype=torch.int32)
|
||||
chunk_indices = cu_seqlens.new_empty((upper_bound_chunks, 2), dtype=torch.int32)
|
||||
|
||||
PrepMetaKernel.compile(chunk_size)(cu_seqlens, chunk_indices, chunk_offsets)
|
||||
return chunk_indices, chunk_offsets
|
||||
|
||||
|
||||
def chunk_gated_delta_rule_cutedsl(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
g: torch.Tensor,
|
||||
beta: torch.Tensor,
|
||||
initial_state: torch.Tensor,
|
||||
cu_seqlens: torch.Tensor,
|
||||
chunk_indices: torch.Tensor,
|
||||
chunk_offsets: torch.Tensor,
|
||||
core_attn_out: torch.Tensor | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Run the GDN chunk CuteDSL prefill kernels.
|
||||
|
||||
Args:
|
||||
q: Query tensor with shape ``[1, T, H, K]``.
|
||||
k: Key tensor with shape ``[1, T, H, K]``.
|
||||
v: Value tensor with shape ``[1, T, Hv, V]``.
|
||||
g: Log-space decay tensor with shape ``[1, T, Hv]``.
|
||||
beta: Delta-rule beta tensor with shape ``[1, T, Hv]``.
|
||||
initial_state: Recurrent state with shape ``[N, Hv, V, K]``.
|
||||
cu_seqlens: Cumulative sequence lengths with shape ``[N + 1]``.
|
||||
chunk_indices: Chunk index metadata with shape ``[NT, 2]``.
|
||||
chunk_offsets: Cumulative chunk offsets with shape ``[N + 1]``.
|
||||
core_attn_out: Optional output buffer with shape ``[T, Hv, V]``.
|
||||
|
||||
Returns:
|
||||
A tuple ``(output, final_state)`` where ``output`` has shape
|
||||
``[1, T, Hv, V]`` and ``final_state`` has shape ``[N, Hv, V, K]``.
|
||||
When ``core_attn_out`` is provided, ``output`` is an unsqueezed view of
|
||||
that buffer.
|
||||
"""
|
||||
q_3d = q.squeeze(0)
|
||||
k_3d = k.squeeze(0)
|
||||
v_3d = v.squeeze(0)
|
||||
g_2d = g.squeeze(0)
|
||||
beta_2d = beta.squeeze(0)
|
||||
|
||||
_, _, head_k_dim = k_3d.shape
|
||||
_, num_v_heads, head_v_dim = v_3d.shape
|
||||
chunk_size = 64
|
||||
upper_bound_chunks = chunk_indices.shape[0]
|
||||
pad_t = upper_bound_chunks * chunk_size
|
||||
total_chunks_ptr = chunk_offsets[-1:]
|
||||
|
||||
g_cu = torch.empty_like(g_2d, dtype=torch.float32)
|
||||
u = q_3d.new_empty(pad_t, num_v_heads, head_v_dim)
|
||||
w = q_3d.new_empty(pad_t, num_v_heads, head_k_dim)
|
||||
|
||||
num_sms = torch.cuda.get_device_properties(q.device).multi_processor_count
|
||||
kkt_inv_uw_cutedsl(
|
||||
k_3d,
|
||||
v_3d,
|
||||
u,
|
||||
w,
|
||||
g_2d,
|
||||
beta_2d,
|
||||
g_cu,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
total_chunks_ptr,
|
||||
num_sms=num_sms,
|
||||
)
|
||||
|
||||
h = k_3d.new_empty(
|
||||
upper_bound_chunks,
|
||||
num_v_heads,
|
||||
head_v_dim,
|
||||
head_k_dim,
|
||||
)
|
||||
v_new = q_3d.new_empty(pad_t, num_v_heads, head_v_dim)
|
||||
final_state = torch.empty_like(initial_state)
|
||||
h_cutedsl(
|
||||
k_3d,
|
||||
u,
|
||||
w,
|
||||
v_new,
|
||||
g_cu,
|
||||
h,
|
||||
initial_state,
|
||||
final_state,
|
||||
cu_seqlens,
|
||||
chunk_offsets,
|
||||
)
|
||||
|
||||
output = core_attn_out if core_attn_out is not None else torch.empty_like(v_3d)
|
||||
scale = head_k_dim**-0.5
|
||||
o_cutedsl(
|
||||
q_3d,
|
||||
k_3d,
|
||||
v_new.view(upper_bound_chunks, chunk_size, num_v_heads, head_v_dim),
|
||||
h,
|
||||
g_cu,
|
||||
output,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
total_chunks_ptr,
|
||||
scale,
|
||||
num_sms=num_sms,
|
||||
)
|
||||
return output.unsqueeze(0), final_state
|
||||
|
||||
|
||||
__all__ = [
|
||||
"chunk_gated_delta_rule_cutedsl",
|
||||
"prepare_metadata_cutedsl",
|
||||
]
|
||||
@@ -0,0 +1,754 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adapted from https://github.com/vllm-project/vllm/blob/4868b542c9dfd166662eecc4bb8be3a36a3feaa2/vllm/model_executor/layers/mamba/ops/gdn_chunk_cutedsl/kernel_h.py
|
||||
from functools import cache
|
||||
|
||||
import cutlass
|
||||
import torch
|
||||
from cuda.bindings.driver import CUstream
|
||||
from cutlass import BFloat16, Float32, Int32, Int64, Uint32, cute
|
||||
from cutlass.cute.nvgpu import cpasync, warp
|
||||
from quack.compile_utils import make_fake_tensor
|
||||
|
||||
from sglang.srt.layers.attention.cute_utils import (
|
||||
EVICT_FIRST,
|
||||
_tcgen05,
|
||||
cvt,
|
||||
fence_before_tma_store,
|
||||
simple_tma_copy,
|
||||
)
|
||||
|
||||
|
||||
class Sm100ChunkHKernel:
|
||||
"""For each sequence, compute the chunk recurrent update.
|
||||
|
||||
The input V tile is the U output from the KKT/UW kernel. For each chunk:
|
||||
V_new = U - W @ H.T
|
||||
(we actually do V_new.T = U.T - H @ W.T instead)
|
||||
|
||||
H_scaled = H * exp(g_last)
|
||||
V_scaled = V_new * exp(g_last - g)
|
||||
H_new = H_scaled + V_scaled.T @ K
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
H: int,
|
||||
Hv: int,
|
||||
K_dim: int,
|
||||
V_dim: int,
|
||||
h_dtype: cutlass.Numeric = Float32,
|
||||
BT: int = 64,
|
||||
num_stages: int = 2,
|
||||
) -> None:
|
||||
assert Hv % H == 0
|
||||
assert K_dim == V_dim == 128
|
||||
assert BT == 64
|
||||
self.H = H
|
||||
self.Hv = Hv
|
||||
self.K_dim = K_dim
|
||||
self.V_dim = V_dim
|
||||
self.h_dtype = h_dtype
|
||||
self.BT = BT
|
||||
self.num_stages = num_stages
|
||||
self.num_warps = 10
|
||||
|
||||
@cute.jit
|
||||
def _make_bf16_tma_args(
|
||||
self,
|
||||
tensor: cute.Tensor,
|
||||
dim: cutlass.Constexpr[int],
|
||||
op: cpasync.TmaCopyOp,
|
||||
stages: cutlass.Constexpr[int],
|
||||
):
|
||||
swizzle_128B = cute.make_swizzle(3, 4, 3)
|
||||
slayout = cute.make_layout(
|
||||
(self.BT, 1, (64, dim // 64), stages),
|
||||
stride=(64, 0, (1, self.BT * 64), self.BT * dim),
|
||||
)
|
||||
slayout = cute.make_composed_layout(swizzle_128B, 0, slayout)
|
||||
atom, tma_tensor = cpasync.make_tiled_tma_atom(
|
||||
op,
|
||||
cute.logical_divide(tensor, (None, None, 64)),
|
||||
slayout,
|
||||
cta_tiler=(self.BT, 1, dim),
|
||||
)
|
||||
return atom, tma_tensor, slayout
|
||||
|
||||
@cute.jit
|
||||
def _make_h_tma_args(self, tensor: cute.Tensor, op: cpasync.TmaCopyOp):
|
||||
# number of elements to fill 128B
|
||||
num_elems = 128 // (tensor.element_type.width // 8)
|
||||
swizzle_128B = cute.make_swizzle(3, 4, 3)
|
||||
slayout = cute.make_layout(
|
||||
(1, 1, self.V_dim, (num_elems, self.K_dim // num_elems)),
|
||||
stride=(0, 0, num_elems, (1, self.V_dim * num_elems)),
|
||||
)
|
||||
slayout = cute.make_composed_layout(swizzle_128B, 0, slayout)
|
||||
atom, tma_tensor = cpasync.make_tiled_tma_atom(
|
||||
op,
|
||||
cute.logical_divide(tensor, (None, None, None, num_elems)),
|
||||
slayout,
|
||||
cta_tiler=(1, 1, self.V_dim, self.K_dim),
|
||||
)
|
||||
return atom, tma_tensor, slayout
|
||||
|
||||
@cute.jit
|
||||
def __call__(
|
||||
self,
|
||||
K: cute.Tensor,
|
||||
V: cute.Tensor,
|
||||
W: cute.Tensor,
|
||||
V_new: cute.Tensor,
|
||||
g_cu: cute.Tensor,
|
||||
h: cute.Tensor,
|
||||
h0: cute.Tensor,
|
||||
ht: cute.Tensor,
|
||||
cu_seqlens: cute.Tensor,
|
||||
chunk_offsets: cute.Tensor,
|
||||
stream: CUstream,
|
||||
):
|
||||
tma_g2s = cpasync.CopyBulkTensorTileG2SOp()
|
||||
tma_s2g = cpasync.CopyBulkTensorTileS2GOp()
|
||||
|
||||
K_args = self._make_bf16_tma_args(K, self.K_dim, tma_g2s, self.num_stages)
|
||||
V_args = self._make_bf16_tma_args(V, self.V_dim, tma_g2s, self.num_stages)
|
||||
W_args = self._make_bf16_tma_args(W, self.K_dim, tma_g2s, self.num_stages)
|
||||
V_new_args = self._make_bf16_tma_args(V_new, self.V_dim, tma_s2g, 1)
|
||||
H0_args = self._make_h_tma_args(h0, tma_g2s)
|
||||
HT_args = self._make_h_tma_args(ht, tma_s2g)
|
||||
H_args = self._make_h_tma_args(h, tma_s2g)
|
||||
|
||||
grid = (self.Hv, h0.shape[0], 1)
|
||||
block = (self.num_warps * 32, 1, 1)
|
||||
self.kernel(
|
||||
K_args,
|
||||
V_args,
|
||||
W_args,
|
||||
V_new_args,
|
||||
H0_args,
|
||||
HT_args,
|
||||
H_args,
|
||||
g_cu,
|
||||
cu_seqlens,
|
||||
chunk_offsets,
|
||||
).launch(grid=grid, block=block, stream=stream)
|
||||
|
||||
@cute.kernel
|
||||
def kernel(
|
||||
self,
|
||||
K_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
V_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
W_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
V_new_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
H0_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
HT_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
H_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
g_cu: cute.Tensor,
|
||||
cu_seqlens: cute.Tensor,
|
||||
chunk_offsets: cute.Tensor,
|
||||
):
|
||||
tid, _, _ = cute.arch.thread_idx()
|
||||
head_id, seq_id, _ = cute.arch.block_idx()
|
||||
warp_id = cute.arch.make_warp_uniform(tid // 32)
|
||||
lane_id = tid % 32
|
||||
|
||||
BT = self.BT
|
||||
V_dim = self.V_dim
|
||||
K_dim = self.K_dim
|
||||
num_stages = self.num_stages
|
||||
is_f32 = self.h_dtype == Float32
|
||||
|
||||
K_tma_atom, tmaK, sK_layout = K_args
|
||||
V_tma_atom, tmaV, sV_layout = V_args
|
||||
W_tma_atom, tmaW, sW_layout = W_args
|
||||
V_new_tma_atom, tmaV_new, sV_new_layout = V_new_args
|
||||
H0_tma_atom, tmaH0, sH0_layout = H0_args
|
||||
HT_tma_atom, tmaHT, _ = HT_args
|
||||
H_tma_atom, tmaH, sH_layout = H_args
|
||||
|
||||
def allocate_tensor(smem, dtype, layout):
|
||||
return smem.allocate_tensor(
|
||||
dtype, layout.outer, byte_alignment=128, swizzle=layout.inner
|
||||
)
|
||||
|
||||
smem = cutlass.utils.SmemAllocator()
|
||||
|
||||
# remove size=1 modes
|
||||
sW = allocate_tensor(smem, BFloat16, sW_layout)[None, 0, None, None]
|
||||
sV = allocate_tensor(smem, BFloat16, sV_layout)[None, 0, None, None]
|
||||
sK = allocate_tensor(smem, BFloat16, sK_layout)[None, 0, None, None]
|
||||
sH0 = allocate_tensor(smem, self.h_dtype, sH0_layout)[0, 0, None, None]
|
||||
sH = allocate_tensor(smem, BFloat16, sH_layout)[0, 0, None, None]
|
||||
sV_new = allocate_tensor(smem, BFloat16, sV_new_layout)[None, 0, None, 0]
|
||||
|
||||
s_v_scale = smem.allocate_array(Float32, BT)
|
||||
tma_mbar = smem.allocate_array(Int64, num_stages)
|
||||
wh_in_mbar = smem.allocate_array(Int64, num_stages)
|
||||
wh_done_mbar = smem.allocate_array(Int64, num_stages)
|
||||
vk_in_mbar = smem.allocate_array(Int64, num_stages)
|
||||
vk_done_mbar = smem.allocate_array(Int64, num_stages)
|
||||
h0_mbar = smem.allocate_array(Int64, 1)
|
||||
taddr = smem.allocate(Int32, 4)
|
||||
|
||||
wh_tmem = 0
|
||||
vk_tmem = wh_tmem + BT
|
||||
h_tmem_base = vk_tmem + K_dim
|
||||
v_tmem_base = h_tmem_base + K_dim // 2
|
||||
|
||||
if warp_id == 0:
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(num_stages):
|
||||
cute.arch.mbarrier_init(tma_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(wh_in_mbar + i, 256)
|
||||
cute.arch.mbarrier_init(wh_done_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(vk_in_mbar + i, 256)
|
||||
cute.arch.mbarrier_init(vk_done_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(h0_mbar, 1)
|
||||
cute.arch.mbarrier_init_fence()
|
||||
elif warp_id == 1:
|
||||
cpasync.prefetch_descriptor(H0_tma_atom)
|
||||
cpasync.prefetch_descriptor(W_tma_atom)
|
||||
cpasync.prefetch_descriptor(V_tma_atom)
|
||||
cpasync.prefetch_descriptor(K_tma_atom)
|
||||
cpasync.prefetch_descriptor(HT_tma_atom)
|
||||
cpasync.prefetch_descriptor(H_tma_atom)
|
||||
cpasync.prefetch_descriptor(V_new_tma_atom)
|
||||
cute.arch.sync_threads()
|
||||
|
||||
bos = cu_seqlens[seq_id]
|
||||
eos = cu_seqlens[seq_id + 1]
|
||||
seqlen = eos - bos
|
||||
num_chunks = cute.ceil_div(seqlen, BT)
|
||||
|
||||
if warp_id == 9:
|
||||
# TMA warp
|
||||
stage_id = 0
|
||||
parity = 1
|
||||
|
||||
k_head_id = head_id // (self.Hv // self.H)
|
||||
chunk_offset = chunk_offsets[seq_id]
|
||||
|
||||
# load H0
|
||||
with cute.arch.elect_one():
|
||||
H0_size = V_dim * K_dim * self.h_dtype.width // 8
|
||||
cute.arch.mbarrier_arrive_and_expect_tx(h0_mbar, H0_size)
|
||||
simple_tma_copy(
|
||||
H0_tma_atom, tmaH0[seq_id, head_id, None, None], sH0, h0_mbar
|
||||
)
|
||||
|
||||
# shape: ((BT, num_BT_tiles), (64, 2))
|
||||
gW_tiles = cute.logical_divide(tmaW[None, head_id, None], (BT, None))
|
||||
gV_tiles = cute.logical_divide(tmaV[None, head_id, None], (BT, None))
|
||||
gK_tiles = cute.logical_divide(
|
||||
cute.domain_offset((bos, 0), tmaK[None, k_head_id, None]),
|
||||
(BT, None),
|
||||
)
|
||||
|
||||
for chunk_id in range(num_chunks):
|
||||
mbar = tma_mbar + stage_id
|
||||
gW = gW_tiles[(None, chunk_offset + chunk_id), None]
|
||||
gV = gV_tiles[(None, chunk_offset + chunk_id), None]
|
||||
gK = gK_tiles[(None, chunk_id), None]
|
||||
|
||||
# wait for MMA to release the buffer
|
||||
cute.arch.mbarrier_wait(vk_done_mbar + stage_id, parity)
|
||||
|
||||
# load W, V (i.e. U), and K
|
||||
with cute.arch.elect_one():
|
||||
STAGE_SIZE = BT * (K_dim + V_dim + K_dim) * 2
|
||||
cute.arch.mbarrier_arrive_and_expect_tx(mbar, STAGE_SIZE)
|
||||
simple_tma_copy(
|
||||
W_tma_atom, gW, sW[None, None, stage_id], mbar, EVICT_FIRST
|
||||
)
|
||||
simple_tma_copy(
|
||||
V_tma_atom, gV, sV[None, None, stage_id], mbar, EVICT_FIRST
|
||||
)
|
||||
simple_tma_copy(K_tma_atom, gK, sK[None, None, stage_id], mbar)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
elif warp_id == 8:
|
||||
# MMA warp
|
||||
_tcgen05.alloc(taddr)
|
||||
stage_id = 0
|
||||
parity = 0
|
||||
|
||||
wh_idesc = _tcgen05.make_bf16_idesc(V_dim, BT, negate_A=True)
|
||||
vk_idesc = _tcgen05.make_bf16_idesc(V_dim, K_dim, transpose_B=True)
|
||||
|
||||
# LBO=BT*128 is ignored for K-major
|
||||
sdesc_template = _tcgen05.make_sdesc_128B_swizzle(BT * 128)
|
||||
|
||||
# when using BF16 state, H is read from smem for the 1st iteration
|
||||
# variable names in this conditional branch can't be the same as those
|
||||
# in the mainloop below due to CuteDSL restrictions.
|
||||
if cutlass.const_expr(not is_f32):
|
||||
##### 1st MMA: V_new.T = V.T - H @ W.T #####
|
||||
Haddr0 = sH0[None, None].iterator.toint()
|
||||
Waddr0 = sW[None, None, stage_id].iterator.toint()
|
||||
hdesc0_base = sdesc_template | (Haddr0 >> 4)
|
||||
wdesc0_base = sdesc_template | (Waddr0 >> 4)
|
||||
|
||||
cute.arch.mbarrier_wait(tma_mbar + stage_id, parity)
|
||||
cute.arch.mbarrier_wait(wh_in_mbar + stage_id, parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(K_dim // 64):
|
||||
for j in cutlass.range_constexpr(64 // 16):
|
||||
hdesc0 = hdesc0_base | ((i * V_dim * 128 + j * 32) >> 4)
|
||||
wdesc0 = wdesc0_base | ((i * BT * 128 + j * 32) >> 4)
|
||||
_tcgen05.mma_f16(wh_tmem, hdesc0, wdesc0, wh_idesc, True)
|
||||
_tcgen05.commit(wh_done_mbar + stage_id)
|
||||
|
||||
##### 2nd MMA: H_new = H + V_new.T @ K #####
|
||||
Kaddr0 = sK[None, None, stage_id].iterator.toint()
|
||||
kdesc0_base = sdesc_template | (Kaddr0 >> 4)
|
||||
|
||||
cute.arch.mbarrier_wait(vk_in_mbar + stage_id, parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
with cute.arch.elect_one():
|
||||
for k in cutlass.range_constexpr(BT // 16):
|
||||
vtmem0 = v_tmem_base + k * 8
|
||||
kdesc0 = kdesc0_base | ((k * 16 * 128) >> 4)
|
||||
_tcgen05.mma_ts_f16(vk_tmem, vtmem0, kdesc0, vk_idesc, True)
|
||||
_tcgen05.commit(vk_done_mbar + stage_id)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
num_iters = num_chunks - int(not is_f32)
|
||||
for _ in range(num_iters):
|
||||
##### 1st MMA: V_new.T = V.T - H @ W.T #####
|
||||
Waddr = sW[None, None, stage_id].iterator.toint()
|
||||
wdesc_base = sdesc_template | (Waddr >> 4)
|
||||
|
||||
cute.arch.mbarrier_wait(tma_mbar + stage_id, parity)
|
||||
cute.arch.mbarrier_wait(wh_in_mbar + stage_id, parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(K_dim // 64):
|
||||
for j in cutlass.range_constexpr(64 // 16):
|
||||
htmem = h_tmem_base + i * 32 + j * 8
|
||||
wdesc = wdesc_base | ((i * BT * 128 + j * 32) >> 4)
|
||||
_tcgen05.mma_ts_f16(wh_tmem, htmem, wdesc, wh_idesc, True)
|
||||
_tcgen05.commit(wh_done_mbar + stage_id)
|
||||
|
||||
##### 2nd MMA: H_new = H + V_new.T @ K #####
|
||||
Kaddr = sK[None, None, stage_id].iterator.toint()
|
||||
kdesc_base = sdesc_template | (Kaddr >> 4)
|
||||
|
||||
cute.arch.mbarrier_wait(vk_in_mbar + stage_id, parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
with cute.arch.elect_one():
|
||||
for k in cutlass.range_constexpr(BT // 16):
|
||||
vtmem = v_tmem_base + k * 8
|
||||
kdesc = kdesc_base | ((k * 16 * 128) >> 4)
|
||||
_tcgen05.mma_ts_f16(vk_tmem, vtmem, kdesc, vk_idesc, True)
|
||||
_tcgen05.commit(vk_done_mbar + stage_id)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
elif warp_id >= 4:
|
||||
# H warps
|
||||
tid_ = tid % 128
|
||||
warp_id_ = warp_id % 4
|
||||
chunk_offset = chunk_offsets[seq_id]
|
||||
|
||||
stage_id = 0
|
||||
vk_stage_id = 0
|
||||
vk_parity = 0
|
||||
|
||||
op = cute.nvgpu.CopyUniversalOp()
|
||||
cp_16B = cute.make_copy_atom(op, Float32, num_bits_per_copy=128)
|
||||
|
||||
##### chunk_id = 0 #####
|
||||
if True:
|
||||
chunk_id = 0
|
||||
end_t = min(bos + (chunk_id + 1) * BT, eos)
|
||||
last_idx = end_t - 1
|
||||
h_scale = cute.math.exp(g_cu[last_idx, head_id], fastmath=True)
|
||||
|
||||
# for 1st chunk, wait for H0 transfer from gmem
|
||||
if warp_id_ == 0:
|
||||
cute.arch.mbarrier_wait(h0_mbar, 0)
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
|
||||
# when H0 is FP32, we need to pack it to BF16
|
||||
# also store to smem for TMA store later.
|
||||
if cutlass.const_expr(is_f32):
|
||||
for i in cutlass.range_constexpr(K_dim // 32):
|
||||
# H0 smem layout: (V_dim, (32, K_dim/32))
|
||||
h_f32 = cute.make_rmem_tensor(32, Float32)
|
||||
cute.copy(cp_16B, sH0[tid_, (None, i)], h_f32)
|
||||
|
||||
h_bf16 = cute.make_rmem_tensor(32, BFloat16)
|
||||
h_bf16.store(h_f32.load().to(BFloat16))
|
||||
_tcgen05.st(
|
||||
warp_id_ * 32, h_tmem_base + i * 16, "32x32b", 16, h_bf16
|
||||
)
|
||||
|
||||
# H smem layout: (V_dim, (64, K_dim/64))
|
||||
dst = cute.local_tile(sH[tid_, None], (32,), (i,))
|
||||
cute.copy(cp_16B, h_bf16, dst)
|
||||
|
||||
_tcgen05.wait_st()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(wh_in_mbar + stage_id)
|
||||
|
||||
# scale H for 2nd MMA
|
||||
for i in cutlass.range_constexpr(K_dim // 32):
|
||||
h_f32 = cute.make_rmem_tensor(32, Float32)
|
||||
|
||||
if cutlass.const_expr(is_f32):
|
||||
cute.copy(cp_16B, sH0[tid_, (None, i)], h_f32)
|
||||
|
||||
else:
|
||||
h_bf16 = cute.make_rmem_tensor(32, BFloat16)
|
||||
sH_src = cute.local_tile(sH0[tid_, None], (32,), (i,))
|
||||
cute.copy(cp_16B, sH_src, h_bf16)
|
||||
h_f32.store(
|
||||
cvt.bf16x2_to_fp32x2(
|
||||
cute.recast_tensor(h_bf16, Uint32)
|
||||
).load()
|
||||
)
|
||||
|
||||
for j in cutlass.range_constexpr(32):
|
||||
h_f32[j] *= h_scale
|
||||
_tcgen05.st(warp_id_ * 32, vk_tmem + i * 32, "32x32b", 32, h_f32)
|
||||
|
||||
_tcgen05.wait_st()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(vk_in_mbar + stage_id)
|
||||
|
||||
# for BF16 H0, we issue TMA store from H0 smem
|
||||
# for FP32 H0, we issue TMA store from H smem (after packing)
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
fence_before_tma_store()
|
||||
if warp_id_ == 3:
|
||||
h_src = sH if cutlass.const_expr(is_f32) else sH0
|
||||
h_dst = tmaH[chunk_offset + chunk_id, head_id, None, None]
|
||||
simple_tma_copy(H_tma_atom, h_src, h_dst)
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_commit_group()
|
||||
|
||||
# When H0 is BF16, and there is only 1 chunk, storing
|
||||
# the final state to sH0 can race before this store
|
||||
# has finished. hence, we need to wait here.
|
||||
if cutlass.const_expr(not is_f32):
|
||||
cute.arch.cp_async_bulk_wait_group(0, read=True)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
|
||||
##### subsequent chunks #####
|
||||
for chunk_id in range(1, num_chunks):
|
||||
end_t = min(bos + (chunk_id + 1) * BT, eos)
|
||||
last_idx = end_t - 1
|
||||
h_scale = cute.math.exp(g_cu[last_idx, head_id], fastmath=True)
|
||||
|
||||
# wait for H from previous vk MMA
|
||||
if warp_id_ == 0:
|
||||
cute.arch.mbarrier_wait(vk_done_mbar + vk_stage_id, vk_parity)
|
||||
vk_stage_id = (vk_stage_id + 1) % num_stages
|
||||
if vk_stage_id == 0:
|
||||
vk_parity ^= 1
|
||||
elif warp_id_ == 3:
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_wait_group(0, read=True)
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
# load FP32 H from tmem, convert to BF16, store to tmem for 1st MMA,
|
||||
# store to smem for TMA store later.
|
||||
for i in cutlass.range_constexpr(K_dim // 32):
|
||||
h_f32 = _tcgen05.ld(warp_id_ * 32, vk_tmem + i * 32, "32x32b", 32)
|
||||
h_bf16 = cute.make_rmem_tensor(32, BFloat16)
|
||||
h_bf16.store(h_f32.to(BFloat16))
|
||||
_tcgen05.st(
|
||||
warp_id_ * 32, h_tmem_base + i * 16, "32x32b", 16, h_bf16
|
||||
)
|
||||
|
||||
# H smem layout: (V_dim, (64, K_dim/64))
|
||||
dst = cute.local_tile(sH[tid_, None], (32,), (i,))
|
||||
cute.copy(cp_16B, h_bf16, dst)
|
||||
|
||||
_tcgen05.wait_st()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(wh_in_mbar + stage_id)
|
||||
|
||||
# scale H for 2nd MMA
|
||||
for i in cutlass.range_constexpr(K_dim // 32):
|
||||
h_f32 = cute.make_rmem_tensor(32, Float32)
|
||||
h_f32.store(
|
||||
_tcgen05.ld(warp_id_ * 32, vk_tmem + i * 32, "32x32b", 32)
|
||||
)
|
||||
for j in cutlass.range_constexpr(32):
|
||||
h_f32[j] *= h_scale
|
||||
_tcgen05.st(warp_id_ * 32, vk_tmem + i * 32, "32x32b", 32, h_f32)
|
||||
_tcgen05.wait_st()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(vk_in_mbar + stage_id)
|
||||
|
||||
# issue TMA store for O kernel
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
fence_before_tma_store()
|
||||
if warp_id_ == 3:
|
||||
h_dst = tmaH[chunk_offset + chunk_id, head_id, None, None]
|
||||
simple_tma_copy(H_tma_atom, sH, h_dst)
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_commit_group()
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
|
||||
# handle final state. reuse H0 smem.
|
||||
if warp_id_ == 0:
|
||||
cute.arch.mbarrier_wait(vk_done_mbar + vk_stage_id, vk_parity)
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
for i in cutlass.range_constexpr(K_dim // 32):
|
||||
h_f32 = cute.make_rmem_tensor(32, Float32)
|
||||
h_f32.store(_tcgen05.ld(warp_id_ * 32, vk_tmem + i * 32, "32x32b", 32))
|
||||
|
||||
if cutlass.const_expr(is_f32):
|
||||
cute.copy(cp_16B, h_f32, sH0[tid_, (None, i)])
|
||||
|
||||
else:
|
||||
h_bf16 = cute.make_rmem_tensor(32, BFloat16)
|
||||
h_bf16.store(h_f32.load().to(BFloat16))
|
||||
sH0_dst = cute.local_tile(sH0[tid_, None], (32,), (i,))
|
||||
cute.copy(cp_16B, h_bf16, sH0_dst)
|
||||
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
|
||||
if warp_id_ == 0:
|
||||
ht_dst = tmaHT[seq_id, head_id, None, None]
|
||||
simple_tma_copy(HT_tma_atom, sH0, ht_dst)
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_commit_group()
|
||||
if warp_id_ == 1:
|
||||
_tcgen05.dealloc()
|
||||
|
||||
else:
|
||||
# V warps
|
||||
stage_id = 0
|
||||
parity = 0
|
||||
|
||||
chunk_offset = chunk_offsets[seq_id]
|
||||
|
||||
ldsm_trans_op = warp.LdMatrix8x8x16bOp(num_matrices=4, transpose=True)
|
||||
stsm_trans_op = warp.StMatrix8x8x16bOp(num_matrices=4, transpose=True)
|
||||
ldsm_trans_atom = cute.make_copy_atom(ldsm_trans_op, BFloat16)
|
||||
stsm_trans_atom = cute.make_copy_atom(stsm_trans_op, BFloat16)
|
||||
|
||||
# ((BT, num_BT_tiles), V_dim)
|
||||
gV_new_tiles = cute.logical_divide(
|
||||
tmaV_new[None, head_id, None], (BT, None)
|
||||
)
|
||||
|
||||
# sV shape: [BT, (64, V_dim/64), num_stages]
|
||||
# sV_view shape: [BT, (8, (8,2)), num_stages]
|
||||
sV_view = cute.logical_divide(sV, (None, 8, None))
|
||||
sV_new_view = cute.logical_divide(sV_new, (None, 8))
|
||||
|
||||
# [BT, 8, num_stages]
|
||||
s_col = warp_id * 4 + (lane_id // 8)
|
||||
sV_view = sV_view[None, (None, s_col), None]
|
||||
sV_new_view = sV_new_view[None, (None, s_col)]
|
||||
|
||||
for chunk_id in range(num_chunks):
|
||||
# wait for V to arrive
|
||||
if warp_id == 0:
|
||||
cute.arch.mbarrier_wait(tma_mbar + stage_id, parity)
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
|
||||
# unpack V BF16->FP32, then store to tmem for 1st MMA
|
||||
# V smem layout: [BT, (64, V_dim/64)] / [BT, V_dim]
|
||||
# each iteration, CTA loads [8, V_dim] tile
|
||||
# (warp loads [8, 32] tile)
|
||||
for i in cutlass.range_constexpr(BT // 8):
|
||||
s_row = i * 8 + (lane_id % 8)
|
||||
v_bf16 = cute.make_rmem_tensor(8, BFloat16)
|
||||
cute.copy(ldsm_trans_atom, sV_view[s_row, None, stage_id], v_bf16)
|
||||
v_fp32 = cvt.bf16x2_to_fp32x2(cute.recast_tensor(v_bf16, Uint32))
|
||||
v_fp32 = cute.logical_divide(v_fp32, 4) # (4, 2)
|
||||
|
||||
tcol = wh_tmem + i * 8
|
||||
_tcgen05.st(warp_id * 32 + 0, tcol, "16x256b", 1, v_fp32[None, 0])
|
||||
_tcgen05.st(warp_id * 32 + 16, tcol, "16x256b", 1, v_fp32[None, 1])
|
||||
|
||||
_tcgen05.wait_st()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(wh_in_mbar + stage_id)
|
||||
|
||||
# load g_cu for scaling
|
||||
if tid < BT:
|
||||
end_t = min(bos + (chunk_id + 1) * BT, eos)
|
||||
last_idx = end_t - 1
|
||||
t = bos + chunk_id * BT + tid
|
||||
val = Float32(0.0)
|
||||
if t < eos:
|
||||
val = cute.math.exp(
|
||||
g_cu[last_idx, head_id] - g_cu[t, head_id],
|
||||
fastmath=True,
|
||||
)
|
||||
s_v_scale[tid] = val
|
||||
|
||||
# wait for 1st MMA to finish
|
||||
if warp_id == 2:
|
||||
cute.arch.mbarrier_wait(wh_done_mbar + stage_id, parity)
|
||||
elif warp_id == 3:
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_wait_group(0, read=True)
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
for i in cutlass.range_constexpr(BT // 8):
|
||||
v_new = cute.make_rmem_tensor((4, 2), Float32)
|
||||
tcol = wh_tmem + i * 8
|
||||
v_new[None, 0].store(
|
||||
_tcgen05.ld(warp_id * 32 + 0, tcol, "16x256b", 1)
|
||||
)
|
||||
v_new[None, 1].store(
|
||||
_tcgen05.ld(warp_id * 32 + 16, tcol, "16x256b", 1)
|
||||
)
|
||||
v_new_bf16 = cute.make_rmem_tensor(8, BFloat16)
|
||||
v_new_bf16.store(v_new.load().to(BFloat16))
|
||||
|
||||
# scale V_new for 2nd MMA
|
||||
scale0 = s_v_scale[i * 8 + (lane_id % 4) * 2 + 0]
|
||||
scale1 = s_v_scale[i * 8 + (lane_id % 4) * 2 + 1]
|
||||
v_scaled = cute.make_rmem_tensor(8, Float32)
|
||||
for k in cutlass.range_constexpr(4):
|
||||
v_scaled[k * 2] = v_new[k * 2] * scale0
|
||||
v_scaled[k * 2 + 1] = v_new[k * 2 + 1] * scale1
|
||||
v_scaled_bf16 = v_scaled.load().to(BFloat16).reshape((4, 2))
|
||||
|
||||
# store V_new BF16 for O kernel
|
||||
s_row = i * 8 + (lane_id % 8)
|
||||
cute.copy(stsm_trans_atom, v_new_bf16, sV_new_view[s_row, None])
|
||||
|
||||
# store to tmem
|
||||
tcol = v_tmem_base + i * 4
|
||||
_tcgen05.st(
|
||||
warp_id * 32 + 0, tcol, "16x128b", 1, v_scaled_bf16[None, 0]
|
||||
)
|
||||
_tcgen05.st(
|
||||
warp_id * 32 + 16, tcol, "16x128b", 1, v_scaled_bf16[None, 1]
|
||||
)
|
||||
_tcgen05.wait_st()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(vk_in_mbar + stage_id)
|
||||
|
||||
# issue TMA store for V_new
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
fence_before_tma_store()
|
||||
if warp_id == 3:
|
||||
gV = gV_new_tiles[(None, chunk_offset + chunk_id), None]
|
||||
simple_tma_copy(V_new_tma_atom, sV_new, gV)
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_commit_group()
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
@cache
|
||||
@staticmethod
|
||||
def compile(
|
||||
H: int,
|
||||
Hv: int,
|
||||
K_dim: int,
|
||||
V_dim: int,
|
||||
h_dtype: cutlass.Numeric = Float32,
|
||||
BT: int = 64,
|
||||
num_stages: int = 2,
|
||||
):
|
||||
total_t = cute.sym_int()
|
||||
pad_t = cute.sym_int()
|
||||
total_chunks_n = cute.sym_int()
|
||||
num_sequences = cute.sym_int()
|
||||
cu_entries = cute.sym_int()
|
||||
|
||||
K = make_fake_tensor(BFloat16, (total_t, H, K_dim), divisibility=16)
|
||||
V = make_fake_tensor(BFloat16, (pad_t, Hv, V_dim), divisibility=16)
|
||||
W = make_fake_tensor(BFloat16, (pad_t, Hv, K_dim), divisibility=16)
|
||||
V_new = make_fake_tensor(BFloat16, (pad_t, Hv, V_dim), divisibility=16)
|
||||
g_cu = make_fake_tensor(Float32, (total_t, Hv), divisibility=4)
|
||||
h = make_fake_tensor(
|
||||
BFloat16, (total_chunks_n, Hv, V_dim, K_dim), divisibility=16
|
||||
)
|
||||
h0 = make_fake_tensor(
|
||||
h_dtype, (num_sequences, Hv, V_dim, K_dim), divisibility=16
|
||||
)
|
||||
ht = make_fake_tensor(
|
||||
h_dtype, (num_sequences, Hv, V_dim, K_dim), divisibility=16
|
||||
)
|
||||
cu_seqlens = make_fake_tensor(Int32, (cu_entries,), divisibility=1)
|
||||
chunk_offsets = make_fake_tensor(Int32, (cu_entries,), divisibility=1)
|
||||
|
||||
kernel = Sm100ChunkHKernel(H, Hv, K_dim, V_dim, h_dtype, BT, num_stages)
|
||||
stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
|
||||
return cute.compile(
|
||||
kernel,
|
||||
K,
|
||||
V,
|
||||
W,
|
||||
V_new,
|
||||
g_cu,
|
||||
h,
|
||||
h0,
|
||||
ht,
|
||||
cu_seqlens,
|
||||
chunk_offsets,
|
||||
stream,
|
||||
options="--enable-tvm-ffi",
|
||||
)
|
||||
|
||||
|
||||
def h_cutedsl(
|
||||
K: torch.Tensor,
|
||||
V: torch.Tensor,
|
||||
W: torch.Tensor,
|
||||
V_new: torch.Tensor,
|
||||
g_cu: torch.Tensor,
|
||||
h: torch.Tensor,
|
||||
h0: torch.Tensor,
|
||||
ht: torch.Tensor,
|
||||
cu_seqlens: torch.Tensor,
|
||||
chunk_offsets: torch.Tensor,
|
||||
BT: int = 64,
|
||||
num_stages: int = 2,
|
||||
) -> None:
|
||||
"""Compute H/V_new with the same argument order as the CUDA wrapper."""
|
||||
|
||||
_, H, K_dim = K.shape
|
||||
_, Hv, V_dim = V.shape
|
||||
h_dtype = {
|
||||
torch.bfloat16: BFloat16,
|
||||
torch.float32: Float32,
|
||||
}[h0.dtype]
|
||||
Sm100ChunkHKernel.compile(H, Hv, K_dim, V_dim, h_dtype, BT, num_stages)(
|
||||
K,
|
||||
V,
|
||||
W,
|
||||
V_new,
|
||||
g_cu,
|
||||
h,
|
||||
h0,
|
||||
ht,
|
||||
cu_seqlens,
|
||||
chunk_offsets,
|
||||
)
|
||||
|
||||
|
||||
h_v2b_cutedsl = h_cutedsl
|
||||
@@ -0,0 +1,823 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adapted from https://github.com/vllm-project/vllm/blob/4868b542c9dfd166662eecc4bb8be3a36a3feaa2/vllm/model_executor/layers/mamba/ops/gdn_chunk_cutedsl/kernel_kkt_inv_uw.py
|
||||
from functools import cache
|
||||
|
||||
import cutlass
|
||||
import torch
|
||||
from cuda.bindings.driver import CUstream
|
||||
from cutlass import BFloat16, Float32, Int32, Int64, Uint32, cute
|
||||
from cutlass.cute.nvgpu import cpasync, warp
|
||||
from quack.compile_utils import make_fake_tensor
|
||||
|
||||
from sglang.srt.layers.attention.cute_utils import (
|
||||
EVICT_FIRST,
|
||||
_tcgen05,
|
||||
cvt,
|
||||
fence_before_tma_store,
|
||||
mma_bf16,
|
||||
simple_tma_copy,
|
||||
)
|
||||
|
||||
|
||||
class Sm100ChunkUWKernel:
|
||||
"""Compute per-chunk KKT inverse preprocessing and U/W tiles.
|
||||
|
||||
Gamma[i,j] = exp(g_cu[i] - g_cu[j])
|
||||
A = strictLower(beta * (K @ K.T) * Gamma)
|
||||
Ai = inverse(I + A)
|
||||
U = (Ai * beta) @ V
|
||||
W = (Ai * beta * exp(g_cu)) @ K
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
H: int,
|
||||
Hv: int,
|
||||
K_dim: int,
|
||||
V_dim: int,
|
||||
num_stages: int = 2,
|
||||
) -> None:
|
||||
assert Hv % H == 0
|
||||
assert K_dim == V_dim == 128
|
||||
self.H = H
|
||||
self.Hv = Hv
|
||||
self.K_dim = K_dim
|
||||
self.V_dim = V_dim
|
||||
self.num_stages = num_stages
|
||||
|
||||
# hard-code
|
||||
self.BT = 64
|
||||
self.num_warps = 2 + 4 + 4
|
||||
|
||||
@cute.jit
|
||||
def _make_tma_args(
|
||||
self,
|
||||
tensor: cute.Tensor,
|
||||
dim: cutlass.Constexpr[int],
|
||||
num_stages: int,
|
||||
op: cpasync.TmaCopyOp,
|
||||
):
|
||||
# logical layout: [BT, dim]
|
||||
# permute for TMA: [dim/64, BT, 64] with swizzling
|
||||
swizzle_128B = cute.make_swizzle(3, 4, 3)
|
||||
slayout = cute.make_layout(
|
||||
(self.BT, 1, (64, dim // 64), num_stages),
|
||||
stride=(64, 0, (1, self.BT * 64), self.BT * dim),
|
||||
)
|
||||
slayout = cute.make_composed_layout(swizzle_128B, 0, slayout)
|
||||
|
||||
# we need to convert gmem layout to (T, H, (64, D/64)) for make_tiled_tma_atom()
|
||||
# to emit a single 4D TMA. otherwise, it will emit (D/64)x 3D TMA.
|
||||
atom, tma_tensor = cpasync.make_tiled_tma_atom(
|
||||
op,
|
||||
cute.logical_divide(tensor, (None, None, 64)),
|
||||
slayout,
|
||||
cta_tiler=(self.BT, 1, dim),
|
||||
)
|
||||
return atom, tma_tensor, slayout
|
||||
|
||||
@cute.jit
|
||||
def __call__(
|
||||
self,
|
||||
K: cute.Tensor,
|
||||
V: cute.Tensor,
|
||||
U: cute.Tensor,
|
||||
W: cute.Tensor,
|
||||
g: cute.Tensor,
|
||||
beta: cute.Tensor,
|
||||
g_cu: cute.Tensor,
|
||||
cu_seqlens: cute.Tensor,
|
||||
chunk_indices: cute.Tensor,
|
||||
total_chunks: cute.Tensor,
|
||||
num_sms: Int32,
|
||||
stream: CUstream,
|
||||
):
|
||||
tma_g2s = cpasync.CopyBulkTensorTileG2SOp()
|
||||
tma_s2g = cpasync.CopyBulkTensorTileS2GOp()
|
||||
|
||||
K_args = self._make_tma_args(K, self.K_dim, self.num_stages, tma_g2s)
|
||||
V_args = self._make_tma_args(V, self.V_dim, self.num_stages, tma_g2s)
|
||||
U_args = self._make_tma_args(U, self.V_dim, 1, tma_s2g)
|
||||
W_args = self._make_tma_args(W, self.K_dim, 1, tma_s2g)
|
||||
|
||||
grid = (num_sms // self.Hv, self.Hv, 1)
|
||||
block = (self.num_warps * 32, 1, 1)
|
||||
self.kernel(
|
||||
K_args,
|
||||
V_args,
|
||||
U_args,
|
||||
W_args,
|
||||
g,
|
||||
beta,
|
||||
g_cu,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
total_chunks,
|
||||
).launch(grid=grid, block=block, stream=stream)
|
||||
|
||||
@cute.kernel
|
||||
def kernel(
|
||||
self,
|
||||
K_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
V_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
U_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
W_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
g: cute.Tensor,
|
||||
beta: cute.Tensor,
|
||||
g_cu: cute.Tensor,
|
||||
cu_seqlens: cute.Tensor,
|
||||
chunk_indices: cute.Tensor,
|
||||
total_chunks: cute.Tensor,
|
||||
):
|
||||
tid, _, _ = cute.arch.thread_idx()
|
||||
bid, head_id, _ = cute.arch.block_idx()
|
||||
grid_x, _, _ = cute.arch.grid_dim()
|
||||
|
||||
warp_id = cute.arch.make_warp_uniform(tid // 32)
|
||||
lane_id = tid % 32
|
||||
k_head_id = head_id // (self.Hv // self.H)
|
||||
|
||||
BT = self.BT
|
||||
K_dim = self.K_dim
|
||||
V_dim = self.V_dim
|
||||
num_stages = self.num_stages
|
||||
|
||||
K_tma_atom, tmaK, sK_layout = K_args
|
||||
V_tma_atom, tmaV, sV_layout = V_args
|
||||
U_tma_atom, tmaU, sU_layout = U_args
|
||||
W_tma_atom, tmaW, sW_layout = W_args
|
||||
|
||||
def allocate_tensor(smem, dtype, layout):
|
||||
return smem.allocate_tensor(
|
||||
dtype, layout.outer, byte_alignment=128, swizzle=layout.inner
|
||||
)
|
||||
|
||||
smem = cutlass.utils.SmemAllocator()
|
||||
sK = allocate_tensor(smem, BFloat16, sK_layout)[None, 0, None, None]
|
||||
sV = allocate_tensor(smem, BFloat16, sV_layout)[None, 0, None, None]
|
||||
sU = allocate_tensor(smem, BFloat16, sU_layout)[None, 0, None, 0]
|
||||
sW = allocate_tensor(smem, BFloat16, sW_layout)[None, 0, None, 0]
|
||||
|
||||
swizzle_128B = cute.make_swizzle(3, 4, 3)
|
||||
sA_layout = cute.make_layout((BT, (64, 1)), stride=(64, (1, BT * 64)))
|
||||
sA_layout = cute.make_composed_layout(swizzle_128B, 0, sA_layout)
|
||||
sA = allocate_tensor(smem, BFloat16, sA_layout)
|
||||
sAi = allocate_tensor(smem, BFloat16, sA_layout)
|
||||
|
||||
s_beta = smem.allocate_array(Float32, BT)
|
||||
s_g_cu_exp = smem.allocate_array(Float32, BT)
|
||||
s_g_cu = smem.allocate_array(Float32, BT)
|
||||
|
||||
tma_mbar = smem.allocate_array(Int64, num_stages)
|
||||
mma_kkt_mbar = smem.allocate_array(Int64, num_stages)
|
||||
inv_mbar = smem.allocate_array(Int64, num_stages)
|
||||
mma_u_mbar = smem.allocate_array(Int64, num_stages)
|
||||
mma_w_mbar = smem.allocate_array(Int64, num_stages)
|
||||
epi_mbar = smem.allocate_array(Int64, num_stages)
|
||||
taddr = smem.allocate(Int32, 4)
|
||||
|
||||
kkt_tmem = 0
|
||||
U_tmem_base = kkt_tmem + BT
|
||||
Ab_tmem_base = U_tmem_base + V_dim * num_stages
|
||||
assert Ab_tmem_base + (BT // 2) * num_stages <= 512
|
||||
|
||||
# prepare ldmatrix/stmatrix ops
|
||||
ldsm_op = warp.LdMatrix8x8x16bOp(num_matrices=4)
|
||||
stsm_op = warp.StMatrix8x8x16bOp(num_matrices=4)
|
||||
ldsm_trans_op = warp.LdMatrix8x8x16bOp(num_matrices=4, transpose=True)
|
||||
ldsm_atom = cute.make_copy_atom(ldsm_op, BFloat16)
|
||||
stsm_atom = cute.make_copy_atom(stsm_op, BFloat16)
|
||||
ldsm_trans_atom = cute.make_copy_atom(ldsm_trans_op, BFloat16)
|
||||
|
||||
if warp_id == 0:
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(num_stages):
|
||||
cute.arch.mbarrier_init(tma_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(mma_kkt_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(inv_mbar + i, 128)
|
||||
cute.arch.mbarrier_init(mma_u_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(mma_w_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(epi_mbar + i, 128)
|
||||
cute.arch.mbarrier_init_fence()
|
||||
elif warp_id == 1:
|
||||
cpasync.prefetch_descriptor(K_tma_atom)
|
||||
cpasync.prefetch_descriptor(V_tma_atom)
|
||||
cpasync.prefetch_descriptor(U_tma_atom)
|
||||
cpasync.prefetch_descriptor(W_tma_atom)
|
||||
cute.arch.sync_threads()
|
||||
|
||||
num_global_chunks = total_chunks[0]
|
||||
if warp_id == 9:
|
||||
# TMA warp
|
||||
stage_id = 0
|
||||
parity = 1
|
||||
|
||||
for global_chunk_id in range(bid, num_global_chunks, grid_x):
|
||||
seq_id = chunk_indices[global_chunk_id, 0]
|
||||
chunk_id = chunk_indices[global_chunk_id, 1]
|
||||
bos = cu_seqlens[seq_id]
|
||||
|
||||
# since off_t is not a multiple of BT, we need to use
|
||||
# domain_offset() to shift the pointer first.
|
||||
mbar = tma_mbar + stage_id
|
||||
gK = cute.local_tile(
|
||||
cute.domain_offset((bos, 0), tmaK[None, k_head_id, None]),
|
||||
tiler=(BT, K_dim),
|
||||
coord=(chunk_id, 0),
|
||||
)
|
||||
gV = cute.local_tile(
|
||||
cute.domain_offset((bos, 0), tmaV[None, head_id, None]),
|
||||
tiler=(BT, V_dim),
|
||||
coord=(chunk_id, 0),
|
||||
)
|
||||
|
||||
# when UW MMA is done, K and V TMA buffers are released
|
||||
cute.arch.mbarrier_wait(mma_u_mbar + stage_id, parity)
|
||||
|
||||
with cute.arch.elect_one():
|
||||
STAGE_SIZE = BT * (K_dim + V_dim) * 2
|
||||
cute.arch.mbarrier_arrive_and_expect_tx(mbar, STAGE_SIZE)
|
||||
simple_tma_copy(K_tma_atom, gK, sK[None, None, stage_id], mbar)
|
||||
simple_tma_copy(
|
||||
V_tma_atom, gV, sV[None, None, stage_id], mbar, EVICT_FIRST
|
||||
)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
elif warp_id == 8:
|
||||
# MMA warp
|
||||
_tcgen05.alloc(taddr)
|
||||
|
||||
stage_id = 0
|
||||
parity = 0
|
||||
|
||||
kkt_idesc = _tcgen05.make_bf16_idesc(BT, BT)
|
||||
u_idesc = _tcgen05.make_bf16_idesc(BT, V_dim, transpose_B=True)
|
||||
w_idesc = _tcgen05.make_bf16_idesc(BT, K_dim, transpose_B=True)
|
||||
|
||||
# LBO=BT*128 is ignored for K-major
|
||||
sdesc_template = _tcgen05.make_sdesc_128B_swizzle(BT * 128)
|
||||
|
||||
for global_chunk_id in range(bid, num_global_chunks, grid_x):
|
||||
U_tmem = U_tmem_base + V_dim * stage_id
|
||||
W_tmem = U_tmem | (16 << 16)
|
||||
Ab_tmem = Ab_tmem_base + (BT // 2) * stage_id
|
||||
Abg_tmem = Ab_tmem | (16 << 16)
|
||||
|
||||
##### KKT MMA: KKT = K @ K.T #####
|
||||
kaddr = sK[None, None, stage_id].iterator.toint()
|
||||
kdesc_base = sdesc_template | (kaddr >> 4)
|
||||
|
||||
# wait for TMA data to arrive
|
||||
# kkt tmem is guaranteed to be free as this is issued
|
||||
# after the previous kkt's consumer (inv warps)
|
||||
cute.arch.mbarrier_wait(tma_mbar + stage_id, parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(K_dim // 64):
|
||||
for j in cutlass.range_constexpr(64 // 16):
|
||||
kdesc = kdesc_base | ((i * BT * 128 + j * 32) >> 4)
|
||||
_tcgen05.mma_f16(
|
||||
kkt_tmem,
|
||||
kdesc,
|
||||
kdesc,
|
||||
kkt_idesc,
|
||||
(i > 0) or (j > 0),
|
||||
)
|
||||
_tcgen05.commit(mma_kkt_mbar + stage_id)
|
||||
|
||||
##### U/W MMA: U = Ab @ V, W = Abg @ K #####
|
||||
vaddr = sV[None, None, stage_id].iterator.toint()
|
||||
vdesc = sdesc_template | (vaddr >> 4)
|
||||
kdesc = sdesc_template | (kaddr >> 4)
|
||||
|
||||
# wait for epilogue to release tmem buffer
|
||||
cute.arch.mbarrier_wait(epi_mbar + stage_id, parity ^ 1)
|
||||
cute.arch.mbarrier_wait(inv_mbar + stage_id, parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(BT // 16):
|
||||
_tcgen05.mma_ts_f16(
|
||||
W_tmem, Abg_tmem + i * 8, kdesc, w_idesc, i > 0
|
||||
)
|
||||
kdesc += (16 * 128) >> 4
|
||||
_tcgen05.commit(mma_w_mbar + stage_id)
|
||||
|
||||
for i in cutlass.range_constexpr(BT // 16):
|
||||
_tcgen05.mma_ts_f16(
|
||||
U_tmem, Ab_tmem + i * 8, vdesc, u_idesc, i > 0
|
||||
)
|
||||
vdesc += (16 * 128) >> 4
|
||||
_tcgen05.commit(mma_u_mbar + stage_id)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
cute.arch.mbarrier_wait(epi_mbar + stage_id, parity ^ 1)
|
||||
_tcgen05.dealloc()
|
||||
|
||||
elif warp_id >= 4:
|
||||
# inv warps
|
||||
tid_ = tid % 128
|
||||
warp_id_ = warp_id % 4
|
||||
|
||||
stage_id = 0
|
||||
parity = 0
|
||||
|
||||
# view into (16,16) sub-tiles, then ldmatrix layout
|
||||
sA_ldsm = cute.logical_divide(sA, (16, cute.make_layout((8, 2))))
|
||||
sAi_ldsm = cute.logical_divide(sAi, (16, cute.make_layout((8, 2))))
|
||||
sA_ldsm = sA_ldsm[(lane_id % 16, None), ((None, lane_id // 16), None)]
|
||||
sAi_ldsm = sAi_ldsm[(lane_id % 16, None), ((None, lane_id // 16), None)]
|
||||
|
||||
# init Ai smem buffer with zeros (only the first 48 rows)
|
||||
for i in cutlass.range_constexpr((BT // 4 * 3) * BT // 128):
|
||||
idx = i * 128 + tid_
|
||||
sAi[idx // BT, idx % BT] = BFloat16(0.0)
|
||||
|
||||
# indices for ldmatrix layout later
|
||||
row_indices = cute.make_rmem_tensor((1, 2, 1), Int32)
|
||||
row_indices[0, 0, 0] = warp_id_ * 16 + (lane_id // 4)
|
||||
row_indices[0, 1, 0] = warp_id_ * 16 + (lane_id // 4) + 8
|
||||
row_indices = row_indices.load()
|
||||
|
||||
col_indices = cute.make_rmem_tensor((2, 1, 2), Int32)
|
||||
col_indices[0, 0, 0] = (lane_id % 4) * 2 + 0
|
||||
col_indices[1, 0, 0] = (lane_id % 4) * 2 + 1
|
||||
col_indices[0, 0, 1] = (lane_id % 4) * 2 + 8
|
||||
col_indices[1, 0, 1] = (lane_id % 4) * 2 + 9
|
||||
col_indices = col_indices.load()
|
||||
|
||||
for global_chunk_id in range(bid, num_global_chunks, grid_x):
|
||||
seq_id = chunk_indices[global_chunk_id, 0]
|
||||
chunk_id = chunk_indices[global_chunk_id, 1]
|
||||
bos = cu_seqlens[seq_id]
|
||||
eos = cu_seqlens[seq_id + 1]
|
||||
off_t = bos + chunk_id * BT
|
||||
|
||||
t = off_t + tid_
|
||||
|
||||
##### Phase 1: load g and beta #####
|
||||
if tid_ < BT:
|
||||
in_bounds = t < eos
|
||||
beta_val = beta[t, head_id] if in_bounds else Float32(0.0)
|
||||
g_val = g[t, head_id] if in_bounds else Float32(0.0)
|
||||
|
||||
s_beta[tid_] = beta_val
|
||||
|
||||
# compute cumsum(g)
|
||||
# parallel scan within a warp
|
||||
for i in cutlass.range_constexpr(5):
|
||||
offset = cutlass.const_expr(1 << i)
|
||||
lower = cute.arch.shuffle_sync_up(
|
||||
g_val, offset, mask_and_clamp=0
|
||||
)
|
||||
if lane_id >= offset:
|
||||
g_val += lower
|
||||
|
||||
# store warp sum
|
||||
if lane_id == 31:
|
||||
s_g_cu[warp_id_] = g_val
|
||||
cute.arch.barrier(barrier_id=3, number_of_threads=BT)
|
||||
|
||||
# add warp sum from lower warps
|
||||
for i in cutlass.range_constexpr(1, BT // 32):
|
||||
if warp_id_ >= i:
|
||||
g_val += s_g_cu[i - 1]
|
||||
cute.arch.barrier(barrier_id=3, number_of_threads=BT)
|
||||
|
||||
# store g_cu to gmem for H and O kernels
|
||||
if in_bounds:
|
||||
g_cu[t, head_id] = g_val
|
||||
|
||||
# store g and g_cu to smem for later
|
||||
s_g_cu[tid_] = g_val
|
||||
s_g_cu_exp[tid_] = cute.math.exp(g_val) if in_bounds else 0.0
|
||||
|
||||
##### Phase 2: A = strictLower(beta * kkt * Gamma) #####
|
||||
if warp_id_ == 0:
|
||||
cute.arch.mbarrier_wait(mma_kkt_mbar + stage_id, parity)
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
# tmem 16x256b layout / ldmatrix layout
|
||||
# mode0 is 8 rows together
|
||||
# mode1 is top and bottom 8 rows
|
||||
# mode2 is groups of 16 rows
|
||||
row_coord = (lane_id // 4, None, warp_id_)
|
||||
s_beta_view = cute.make_tensor(s_beta, (8, 2, 4))
|
||||
beta_row = s_beta_view[row_coord].load().reshape((1, 2, 1))
|
||||
|
||||
s_g_cu_view = cute.make_tensor(s_g_cu, (8, 2, 4))
|
||||
g_cu_row = s_g_cu_view[row_coord].load().reshape((1, 2, 1))
|
||||
|
||||
# mode0 is 2 consecutive elems
|
||||
# mode1 is top and bottom 8 rows
|
||||
# mode2 is next 8 columns
|
||||
# mode3 is repeating that 16x16 tile pattern
|
||||
kkt = _tcgen05.ld(kkt_tmem, 0, "16x256b", BT // 8)
|
||||
kkt = kkt.reshape((2, 2, 2, BT // 16))
|
||||
|
||||
for i in cutlass.range_constexpr(BT // 16):
|
||||
# mode0 is 2 elems next to each other
|
||||
# mode1 is 4 pairs of elems on 1 row
|
||||
# mode2 is top and bottom 8 rows
|
||||
# mode3 is next 16 columns
|
||||
col_coord = (None, lane_id % 4, None, i)
|
||||
s_g_cu_view = cute.make_tensor(s_g_cu, (2, 4, 2, BT // 16))
|
||||
g_cu_col = s_g_cu_view[col_coord].load().reshape((2, 1, 2))
|
||||
|
||||
Gamma = cute.math.exp(g_cu_row - g_cu_col, fastmath=True)
|
||||
A = kkt[None, None, None, i] * beta_row * Gamma
|
||||
|
||||
# strict lower mask
|
||||
# NOTE: for OOB t position, s_beta is filled with zeros.
|
||||
# hence, we don't need to apply bounds check for columns.
|
||||
A_masked = cute.where(row_indices > col_indices + i * 16, A, 0.0)
|
||||
|
||||
# pack to BF16
|
||||
# CuteDSL doesn't generate cvt.bf16x2.f32 here for some reasons
|
||||
packed = cute.make_rmem_tensor(4, Uint32)
|
||||
packed[0] = cvt.fp32x2_to_bf16x2(
|
||||
A_masked[0, 0, 0], A_masked[1, 0, 0]
|
||||
)
|
||||
packed[1] = cvt.fp32x2_to_bf16x2(
|
||||
A_masked[0, 1, 0], A_masked[1, 1, 0]
|
||||
)
|
||||
packed[2] = cvt.fp32x2_to_bf16x2(
|
||||
A_masked[0, 0, 1], A_masked[1, 0, 1]
|
||||
)
|
||||
packed[3] = cvt.fp32x2_to_bf16x2(
|
||||
A_masked[0, 1, 1], A_masked[1, 1, 1]
|
||||
)
|
||||
|
||||
# store to smem
|
||||
cute.copy(
|
||||
stsm_atom,
|
||||
cute.recast_tensor(packed, BFloat16),
|
||||
sA_ldsm[warp_id_, None, i],
|
||||
)
|
||||
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
|
||||
##### Phase 3: matrix inverse #####
|
||||
# we use Newton-Schulz iterations to compute the inverse
|
||||
# of the four 16x16 diagonal blocks.
|
||||
# Ai_new = 2 Ai - Ai @ M @ Ai
|
||||
# where M = I + A
|
||||
#
|
||||
# we do this with 2 MMAs:
|
||||
# 1. -AiM = Ai @ (-M)
|
||||
# 2. Ai_new = 2 Ai + (-AiM) @ Ai
|
||||
zeros_f32 = cute.make_rmem_tensor(4, Float32)
|
||||
zeros_f32.fill(0.0)
|
||||
|
||||
def set_diagonal(A: cute.Tensor, lane_id: Int32):
|
||||
"Set the diagonal to 1s"
|
||||
if lane_id % 9 == 0:
|
||||
A[0] = (A[0] & Uint32(0xFFFF0000)) | Uint32(0x00003F80)
|
||||
A[3] = (A[3] & Uint32(0xFFFF0000)) | Uint32(0x00003F80)
|
||||
elif lane_id % 9 == 4:
|
||||
A[0] = (A[0] & Uint32(0x0000FFFF)) | Uint32(0x3F800000)
|
||||
A[3] = (A[3] & Uint32(0x0000FFFF)) | Uint32(0x3F800000)
|
||||
|
||||
Ai_bf16 = cute.make_rmem_tensor(8, BFloat16)
|
||||
mma_B_bf16 = cute.make_rmem_tensor(8, BFloat16)
|
||||
M_bf16 = cute.make_rmem_tensor(8, BFloat16)
|
||||
acc = cute.make_rmem_tensor((4, 2), Float32)
|
||||
|
||||
# share the same storage
|
||||
Ai = cute.recast_tensor(Ai_bf16, Uint32)
|
||||
mma_B = cute.logical_divide(cute.recast_tensor(mma_B_bf16, Uint32), 2)
|
||||
M = cute.logical_divide(cute.recast_tensor(M_bf16, Uint32), 2)
|
||||
|
||||
# initial guess: Ai = I-A
|
||||
cute.copy(ldsm_atom, sA_ldsm[warp_id_, None, warp_id_], Ai_bf16)
|
||||
for i in cutlass.range_constexpr(4):
|
||||
Ai[i] ^= Uint32(0x80008000) # negate A
|
||||
set_diagonal(Ai, lane_id)
|
||||
|
||||
# (4, 2)
|
||||
Ai_f32 = cute.logical_divide(cvt.bf16x2_to_fp32x2(Ai), 4)
|
||||
|
||||
# M is holding -(I+A), stay constant throughout the iterations
|
||||
cute.copy(ldsm_trans_atom, sA_ldsm[warp_id_, None, warp_id_], M_bf16)
|
||||
set_diagonal(M, lane_id)
|
||||
for i in cutlass.range_constexpr(4):
|
||||
M[i] ^= Uint32(0x80008000)
|
||||
|
||||
# 3 rounds of Newton-Schulz
|
||||
for _ in cutlass.range_constexpr(3):
|
||||
# First MMA: -AiM = Ai @ (-M)
|
||||
cute.copy(stsm_atom, Ai_bf16, sA_ldsm[warp_id_, None, warp_id_])
|
||||
cute.arch.sync_warp()
|
||||
acc[None, 0] = mma_bf16(Ai, M[None, 0], zeros_f32)
|
||||
acc[None, 1] = mma_bf16(Ai, M[None, 1], zeros_f32)
|
||||
Ai_bf16.store(acc.load().to(BFloat16))
|
||||
|
||||
# Second MMA: Ai_new = 2Ai + (-AiM) @ Ai
|
||||
for j in cutlass.range_constexpr(8):
|
||||
Ai_f32[j] *= 2.0
|
||||
cute.copy(
|
||||
ldsm_trans_atom,
|
||||
sA_ldsm[warp_id_, None, warp_id_],
|
||||
mma_B_bf16,
|
||||
)
|
||||
Ai_f32[None, 0] = mma_bf16(Ai, mma_B[None, 0], Ai_f32[None, 0])
|
||||
Ai_f32[None, 1] = mma_bf16(Ai, mma_B[None, 1], Ai_f32[None, 1])
|
||||
Ai_bf16.store(Ai_f32.load().to(BFloat16))
|
||||
|
||||
cute.copy(stsm_atom, Ai_bf16, sAi_ldsm[warp_id_, None, warp_id_])
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
|
||||
# off-diagonal by 1
|
||||
# Ai[i,i-1] = -Ai[i,i] @ A[i,i-1] @ Ai[i-1,i-1].
|
||||
if warp_id_ > 0:
|
||||
neg_Ai = cute.make_rmem_tensor(4, Uint32)
|
||||
for i in cutlass.range_constexpr(4):
|
||||
neg_Ai[i] = Ai[i] ^ Uint32(0x80008000)
|
||||
|
||||
cute.copy(
|
||||
ldsm_trans_atom,
|
||||
sA_ldsm[warp_id_, None, warp_id_ - 1],
|
||||
mma_B_bf16,
|
||||
)
|
||||
acc[None, 0] = mma_bf16(neg_Ai, mma_B[None, 0], zeros_f32)
|
||||
acc[None, 1] = mma_bf16(neg_Ai, mma_B[None, 1], zeros_f32)
|
||||
Ai_bf16.store(acc.load().to(BFloat16))
|
||||
|
||||
cute.copy(
|
||||
ldsm_trans_atom,
|
||||
sAi_ldsm[warp_id_ - 1, None, warp_id_ - 1],
|
||||
mma_B_bf16,
|
||||
)
|
||||
acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], zeros_f32)
|
||||
acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], zeros_f32)
|
||||
Ai_bf16.store(acc.load().to(BFloat16))
|
||||
cute.copy(
|
||||
stsm_atom,
|
||||
Ai_bf16,
|
||||
sAi_ldsm[warp_id_, None, warp_id_ - 1],
|
||||
)
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
|
||||
# off-diagonal by 2
|
||||
if warp_id_ < 2:
|
||||
cute.copy(
|
||||
ldsm_atom,
|
||||
sA_ldsm[warp_id_ + 2, None, warp_id_],
|
||||
Ai_bf16,
|
||||
)
|
||||
cute.copy(
|
||||
ldsm_trans_atom,
|
||||
sAi_ldsm[warp_id_, None, warp_id_],
|
||||
mma_B_bf16,
|
||||
)
|
||||
acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], zeros_f32)
|
||||
acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], zeros_f32)
|
||||
|
||||
cute.copy(
|
||||
ldsm_atom,
|
||||
sA_ldsm[warp_id_ + 2, None, warp_id_ + 1],
|
||||
Ai_bf16,
|
||||
)
|
||||
cute.copy(
|
||||
ldsm_trans_atom,
|
||||
sAi_ldsm[warp_id_ + 1, None, warp_id_],
|
||||
mma_B_bf16,
|
||||
)
|
||||
acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], acc[None, 0])
|
||||
acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], acc[None, 1])
|
||||
|
||||
tmp = cute.make_rmem_tensor(8, BFloat16)
|
||||
tmp.store(acc.load().to(BFloat16))
|
||||
cute.copy(stsm_atom, tmp, sAi_ldsm[warp_id_ + 2, None, warp_id_])
|
||||
cute.arch.sync_warp()
|
||||
|
||||
cute.copy(
|
||||
ldsm_atom, sAi_ldsm[warp_id_ + 2, None, warp_id_ + 2], Ai_bf16
|
||||
)
|
||||
for i in cutlass.range_constexpr(4):
|
||||
Ai[i] ^= Uint32(0x80008000)
|
||||
cute.copy(
|
||||
ldsm_trans_atom,
|
||||
sAi_ldsm[warp_id_ + 2, None, warp_id_],
|
||||
mma_B_bf16,
|
||||
)
|
||||
acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], zeros_f32)
|
||||
acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], zeros_f32)
|
||||
tmp.store(acc.load().to(BFloat16))
|
||||
cute.copy(stsm_atom, tmp, sAi_ldsm[warp_id_ + 2, None, warp_id_])
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
|
||||
# off-diagonal by 3
|
||||
if warp_id_ == 0:
|
||||
cute.copy(ldsm_atom, sA_ldsm[3, None, 0], Ai_bf16)
|
||||
cute.copy(ldsm_trans_atom, sAi_ldsm[0, None, 0], mma_B_bf16)
|
||||
acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], zeros_f32)
|
||||
acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], zeros_f32)
|
||||
|
||||
for i in cutlass.range_constexpr(1, 3):
|
||||
cute.copy(ldsm_atom, sA_ldsm[3, None, i], Ai_bf16)
|
||||
cute.copy(ldsm_trans_atom, sAi_ldsm[i, None, 0], mma_B_bf16)
|
||||
acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], acc[None, 0])
|
||||
acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], acc[None, 1])
|
||||
|
||||
tmp = cute.make_rmem_tensor(8, BFloat16)
|
||||
tmp.store(acc.load().to(BFloat16))
|
||||
cute.copy(stsm_atom, tmp, sAi_ldsm[3, None, 0])
|
||||
cute.arch.sync_warp()
|
||||
|
||||
cute.copy(ldsm_atom, sAi_ldsm[3, None, 3], Ai_bf16)
|
||||
for i in cutlass.range_constexpr(4):
|
||||
Ai[i] ^= Uint32(0x80008000)
|
||||
cute.copy(ldsm_trans_atom, sAi_ldsm[3, None, 0], mma_B_bf16)
|
||||
acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], zeros_f32)
|
||||
acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], zeros_f32)
|
||||
tmp.store(acc.load().to(BFloat16))
|
||||
cute.copy(stsm_atom, tmp, sAi_ldsm[3, None, 0])
|
||||
|
||||
##### Phase 4: compute Ab, Abg #####
|
||||
if warp_id_ == 3:
|
||||
cute.arch.mbarrier_wait(mma_u_mbar + stage_id, parity ^ 1)
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
|
||||
for i in cutlass.range_constexpr(BT // 16):
|
||||
cute.copy(ldsm_atom, sAi_ldsm[warp_id_, None, i], Ai_bf16)
|
||||
|
||||
col_coord = (None, lane_id % 4, None, i)
|
||||
s_beta_view = cute.make_tensor(s_beta, (2, 4, 2, BT // 16))
|
||||
beta_col = s_beta_view[col_coord].load().reshape((2, 1, 2))
|
||||
|
||||
s_g_cu_view = cute.make_tensor(s_g_cu_exp, (2, 4, 2, BT // 16))
|
||||
g_cu_col = s_g_cu_view[col_coord].load().reshape((2, 1, 2))
|
||||
|
||||
Ai_f32 = cvt.bf16x2_to_fp32x2(Ai).load().reshape((2, 2, 2))
|
||||
|
||||
Ab_f32 = Ai_f32 * beta_col
|
||||
Ab = Ab_f32.to(BFloat16)
|
||||
Ab_tmem = Ab_tmem_base + (BT // 2) * stage_id + i * 8
|
||||
_tcgen05.st(warp_id_ * 32, Ab_tmem, "16x128b", 2, Ab)
|
||||
|
||||
Abg_f32 = Ab_f32 * g_cu_col
|
||||
Abg = Abg_f32.to(BFloat16)
|
||||
_tcgen05.st(warp_id_ * 32 + 16, Ab_tmem, "16x128b", 2, Abg)
|
||||
|
||||
_tcgen05.wait_st()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(inv_mbar + stage_id)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
elif warp_id < 4:
|
||||
# epi warps
|
||||
stage_id = 0
|
||||
parity = 0
|
||||
|
||||
# ((BT, num_global_chunks), V_dim)
|
||||
gU_tiles = cute.logical_divide(tmaU[None, head_id, None], (BT, None))
|
||||
gW_tiles = cute.logical_divide(tmaW[None, head_id, None], (BT, None))
|
||||
|
||||
# sW shape: [BT, (64, K_dim/64)]
|
||||
# sW_view shape: [(8, 2), (4, K_dim/64)]
|
||||
s_row = warp_id * 16 + lane_id % 16 # select the rows of [16,16] tile
|
||||
sW_view = cute.zipped_divide(
|
||||
sW[s_row, None],
|
||||
tiler=cute.make_layout((8, 2)),
|
||||
)
|
||||
sU_view = cute.zipped_divide(
|
||||
sU[s_row, None],
|
||||
tiler=cute.make_layout((8, 2)),
|
||||
)
|
||||
|
||||
# select the 8 columns within [16,16] tile
|
||||
sW_view = sW_view[(None, lane_id // 16), None]
|
||||
sU_view = sU_view[(None, lane_id // 16), None]
|
||||
|
||||
for global_chunk_id in range(bid, num_global_chunks, grid_x):
|
||||
# wait for W MMA + previous TMA store to finish
|
||||
U_tmem = U_tmem_base + V_dim * stage_id
|
||||
if warp_id == 0:
|
||||
cute.arch.mbarrier_wait(mma_w_mbar + stage_id, parity)
|
||||
elif warp_id == 1:
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_wait_group(0, read=True)
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
w_f32 = _tcgen05.ld(warp_id * 32 + 16, U_tmem, "16x256b", K_dim // 8)
|
||||
_tcgen05.wait_ld()
|
||||
w_bf16 = cute.make_rmem_tensor((8, K_dim // 16), BFloat16)
|
||||
w_bf16.store(w_f32.to(BFloat16))
|
||||
cute.copy(stsm_atom, w_bf16, sW_view)
|
||||
|
||||
# wait for U MMA + issue W TMA store
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
fence_before_tma_store()
|
||||
if warp_id == 0:
|
||||
cute.arch.mbarrier_wait(mma_u_mbar + stage_id, parity)
|
||||
elif warp_id == 1:
|
||||
# don't need to commit
|
||||
simple_tma_copy(
|
||||
W_tma_atom, sW, gW_tiles[(None, global_chunk_id), None]
|
||||
)
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
u_f32 = _tcgen05.ld(warp_id * 32, U_tmem, "16x256b", V_dim // 8)
|
||||
_tcgen05.wait_ld()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(epi_mbar + stage_id)
|
||||
u_bf16 = cute.make_rmem_tensor((8, V_dim // 16), BFloat16)
|
||||
u_bf16.store(u_f32.to(BFloat16))
|
||||
cute.copy(stsm_atom, u_bf16, sU_view)
|
||||
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
fence_before_tma_store()
|
||||
if warp_id == 1:
|
||||
simple_tma_copy(
|
||||
U_tma_atom, sU, gU_tiles[(None, global_chunk_id), None]
|
||||
)
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_commit_group()
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
@cache
|
||||
@staticmethod
|
||||
def compile(H: int, Hv: int, K_dim: int, V_dim: int, num_stages: int = 2):
|
||||
total_t = cute.sym_int()
|
||||
pad_t = cute.sym_int()
|
||||
total_chunks_n = cute.sym_int()
|
||||
num_sequences = cute.sym_int()
|
||||
|
||||
K = make_fake_tensor(BFloat16, (total_t, H, K_dim), divisibility=16)
|
||||
V = make_fake_tensor(BFloat16, (total_t, Hv, V_dim), divisibility=16)
|
||||
U = make_fake_tensor(BFloat16, (pad_t, Hv, V_dim), divisibility=16)
|
||||
W = make_fake_tensor(BFloat16, (pad_t, Hv, K_dim), divisibility=16)
|
||||
g = make_fake_tensor(Float32, (total_t, Hv), divisibility=4)
|
||||
beta = make_fake_tensor(Float32, (total_t, Hv), divisibility=4)
|
||||
g_cu = make_fake_tensor(Float32, (total_t, Hv), divisibility=4)
|
||||
cu_seqlens = make_fake_tensor(Int32, (num_sequences,), divisibility=1)
|
||||
chunk_indices = make_fake_tensor(Int32, (total_chunks_n, 2), divisibility=2)
|
||||
total_chunks = make_fake_tensor(Int32, (1,), divisibility=1)
|
||||
|
||||
kernel = Sm100ChunkUWKernel(H, Hv, K_dim, V_dim, num_stages)
|
||||
stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
|
||||
return cute.compile(
|
||||
kernel,
|
||||
K,
|
||||
V,
|
||||
U,
|
||||
W,
|
||||
g,
|
||||
beta,
|
||||
g_cu,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
total_chunks,
|
||||
Int32(148),
|
||||
stream,
|
||||
options="--enable-tvm-ffi",
|
||||
)
|
||||
|
||||
|
||||
def kkt_inv_uw_cutedsl(
|
||||
K: torch.Tensor,
|
||||
V: torch.Tensor,
|
||||
U: torch.Tensor,
|
||||
W: torch.Tensor,
|
||||
g: torch.Tensor,
|
||||
beta: torch.Tensor,
|
||||
g_cu: torch.Tensor,
|
||||
cu_seqlens: torch.Tensor,
|
||||
chunk_indices: torch.Tensor,
|
||||
total_chunks: torch.Tensor,
|
||||
num_sms: int = 148,
|
||||
) -> None:
|
||||
_, Hv, V_dim = V.shape
|
||||
_, H, K_dim = K.shape
|
||||
|
||||
Sm100ChunkUWKernel.compile(H, Hv, K_dim, V_dim)(
|
||||
K,
|
||||
V,
|
||||
U,
|
||||
W,
|
||||
g,
|
||||
beta,
|
||||
g_cu,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
total_chunks,
|
||||
num_sms,
|
||||
)
|
||||
@@ -0,0 +1,631 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adapted from https://github.com/vllm-project/vllm/blob/4868b542c9dfd166662eecc4bb8be3a36a3feaa2/vllm/model_executor/layers/mamba/ops/gdn_chunk_cutedsl/kernel_o.py
|
||||
from functools import cache
|
||||
|
||||
import cutlass
|
||||
import torch
|
||||
from cuda.bindings.driver import CUstream
|
||||
from cutlass import BFloat16, Float32, Int32, Int64, Uint32, cute
|
||||
from cutlass.cute.nvgpu import cpasync, warp
|
||||
from quack.compile_utils import make_fake_tensor
|
||||
|
||||
from sglang.srt.layers.attention.cute_utils import (
|
||||
EVICT_FIRST,
|
||||
_tcgen05,
|
||||
cvt,
|
||||
fence_before_tma_store,
|
||||
simple_tma_copy,
|
||||
)
|
||||
|
||||
|
||||
class Sm100ChunkOKernel:
|
||||
"""Compute per-token output from recurrent and intra-chunk terms.
|
||||
|
||||
Gamma[i,j] = exp(g_cu[i] - g_cu[j])
|
||||
P = mask((Q @ K.T) * Gamma)
|
||||
O = scale * (exp(g_cu) * (Q @ H.T) + P @ V)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
H: int,
|
||||
Hv: int,
|
||||
K_dim: int,
|
||||
V_dim: int,
|
||||
BT: int = 64,
|
||||
num_stages: int = 2,
|
||||
) -> None:
|
||||
assert Hv % H == 0
|
||||
assert K_dim == 128
|
||||
assert V_dim == 128
|
||||
assert BT == 64
|
||||
self.H = H
|
||||
self.Hv = Hv
|
||||
self.K_dim = K_dim
|
||||
self.V_dim = V_dim
|
||||
self.BT = BT
|
||||
self.num_stages = num_stages
|
||||
self.num_warps = 10
|
||||
|
||||
@cute.jit
|
||||
def _make_bf16_tma_args(
|
||||
self,
|
||||
tensor: cute.Tensor,
|
||||
dim: cutlass.Constexpr[int],
|
||||
op: cpasync.TmaCopyOp,
|
||||
stages: cutlass.Constexpr[int],
|
||||
):
|
||||
swizzle_128B = cute.make_swizzle(3, 4, 3)
|
||||
slayout = cute.make_layout(
|
||||
(self.BT, 1, (64, dim // 64), stages),
|
||||
stride=(64, 0, (1, self.BT * 64), self.BT * dim),
|
||||
)
|
||||
slayout = cute.make_composed_layout(swizzle_128B, 0, slayout)
|
||||
atom, tma_tensor = cpasync.make_tiled_tma_atom(
|
||||
op,
|
||||
cute.logical_divide(tensor, (None, None, 64)),
|
||||
slayout,
|
||||
cta_tiler=(self.BT, 1, dim),
|
||||
)
|
||||
return atom, tma_tensor, slayout
|
||||
|
||||
@cute.jit
|
||||
def _make_h_tma_args(
|
||||
self,
|
||||
tensor: cute.Tensor,
|
||||
op: cpasync.TmaCopyOp,
|
||||
stages: cutlass.Constexpr[int],
|
||||
):
|
||||
num_elems = 128 // (tensor.element_type.width // 8)
|
||||
swizzle_128B = cute.make_swizzle(3, 4, 3)
|
||||
slayout = cute.make_layout(
|
||||
(1, self.V_dim, (num_elems, self.K_dim // num_elems), stages),
|
||||
stride=(0, num_elems, (1, self.V_dim * num_elems), self.V_dim * self.K_dim),
|
||||
)
|
||||
slayout = cute.make_composed_layout(swizzle_128B, 0, slayout)
|
||||
atom, tma_tensor = cpasync.make_tiled_tma_atom(
|
||||
op,
|
||||
cute.logical_divide(tensor, (None, None, num_elems)),
|
||||
slayout,
|
||||
cta_tiler=(1, self.V_dim, self.K_dim),
|
||||
)
|
||||
return atom, tma_tensor, slayout
|
||||
|
||||
@cute.jit
|
||||
def __call__(
|
||||
self,
|
||||
q: cute.Tensor,
|
||||
k: cute.Tensor,
|
||||
v_new_chunks: cute.Tensor,
|
||||
h: cute.Tensor,
|
||||
g_cu: cute.Tensor,
|
||||
o: cute.Tensor,
|
||||
cu_seqlens: cute.Tensor,
|
||||
chunk_indices: cute.Tensor,
|
||||
total_chunks: cute.Tensor,
|
||||
scale: Float32,
|
||||
num_sms: Int32,
|
||||
stream: CUstream,
|
||||
):
|
||||
grid = (num_sms // self.Hv, self.Hv, 1)
|
||||
block = (self.num_warps * 32, 1, 1)
|
||||
tma_g2s = cpasync.CopyBulkTensorTileG2SOp()
|
||||
tma_s2g = cpasync.CopyBulkTensorTileS2GOp()
|
||||
Q_args = self._make_bf16_tma_args(q, self.K_dim, tma_g2s, self.num_stages)
|
||||
K_args = self._make_bf16_tma_args(k, self.K_dim, tma_g2s, self.num_stages)
|
||||
V_args = self._make_bf16_tma_args(
|
||||
v_new_chunks, self.V_dim, tma_g2s, self.num_stages
|
||||
)
|
||||
H_args = self._make_h_tma_args(h, tma_g2s, self.num_stages)
|
||||
O_args = self._make_bf16_tma_args(o, self.V_dim, tma_s2g, 1)
|
||||
self.kernel(
|
||||
Q_args,
|
||||
K_args,
|
||||
V_args,
|
||||
H_args,
|
||||
O_args,
|
||||
g_cu,
|
||||
o,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
total_chunks,
|
||||
scale,
|
||||
).launch(grid=grid, block=block, stream=stream)
|
||||
|
||||
@cute.kernel
|
||||
def kernel(
|
||||
self,
|
||||
Q_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
K_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
V_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
H_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
O_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
g_cu: cute.Tensor,
|
||||
o: cute.Tensor,
|
||||
cu_seqlens: cute.Tensor,
|
||||
chunk_indices: cute.Tensor,
|
||||
total_chunks: cute.Tensor,
|
||||
scale: Float32,
|
||||
):
|
||||
tid, _, _ = cute.arch.thread_idx()
|
||||
bid, v_head_id, _ = cute.arch.block_idx()
|
||||
grid_x, _, _ = cute.arch.grid_dim()
|
||||
warp_id = cute.arch.make_warp_uniform(tid // 32)
|
||||
lane_id = tid % 32
|
||||
|
||||
BT = self.BT
|
||||
K_dim = self.K_dim
|
||||
V_dim = self.V_dim
|
||||
num_stages = self.num_stages
|
||||
|
||||
heads_per_qk = self.Hv // self.H
|
||||
k_head_id = v_head_id // heads_per_qk
|
||||
num_global_chunks = total_chunks[0]
|
||||
|
||||
Q_tma_atom, tmaQ, sQ_layout = Q_args
|
||||
K_tma_atom, tmaK, sK_layout = K_args
|
||||
V_tma_atom, tmaV, sV_layout = V_args
|
||||
H_tma_atom, tmaH, sH_layout = H_args
|
||||
O_tma_atom, tmaO, sO_layout = O_args
|
||||
|
||||
def allocate_tensor(smem, dtype, layout):
|
||||
return smem.allocate_tensor(
|
||||
dtype, layout.outer, byte_alignment=128, swizzle=layout.inner
|
||||
)
|
||||
|
||||
smem = cutlass.utils.SmemAllocator()
|
||||
sQ = allocate_tensor(smem, BFloat16, sQ_layout)[None, 0, None, None]
|
||||
sK = allocate_tensor(smem, BFloat16, sK_layout)[None, 0, None, None]
|
||||
sV = allocate_tensor(smem, BFloat16, sV_layout)[None, 0, None, None]
|
||||
sH = allocate_tensor(smem, BFloat16, sH_layout)[0, None, None, None]
|
||||
sO = allocate_tensor(smem, BFloat16, sO_layout)[None, 0, None, 0]
|
||||
|
||||
s_g_cu = smem.allocate_array(Float32, BT)
|
||||
qk_full_mbar = smem.allocate_array(Int64, num_stages)
|
||||
hv_full_mbar = smem.allocate_array(Int64, num_stages)
|
||||
qk_empty_mbar = smem.allocate_array(Int64, num_stages)
|
||||
pv_mma_mbar = smem.allocate_array(Int64, num_stages)
|
||||
qk_mbar = smem.allocate_array(Int64, 1)
|
||||
mask_mbar = smem.allocate_array(Int64, 1)
|
||||
epi_mbar = smem.allocate_array(Int64, 1)
|
||||
taddr = smem.allocate(Int32, 4)
|
||||
|
||||
qk_tmem = 0
|
||||
p_tmem = 64
|
||||
out_tmem = 128
|
||||
qh_tmem = 256
|
||||
|
||||
if warp_id == 0:
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(num_stages):
|
||||
cute.arch.mbarrier_init(qk_full_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(qk_empty_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(hv_full_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(pv_mma_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(qk_mbar, 1)
|
||||
cute.arch.mbarrier_init(mask_mbar, 128)
|
||||
cute.arch.mbarrier_init(epi_mbar, 128)
|
||||
cute.arch.mbarrier_init_fence()
|
||||
elif warp_id == 9:
|
||||
cpasync.prefetch_descriptor(Q_tma_atom)
|
||||
cpasync.prefetch_descriptor(K_tma_atom)
|
||||
cpasync.prefetch_descriptor(V_tma_atom)
|
||||
cpasync.prefetch_descriptor(H_tma_atom)
|
||||
cute.arch.sync_threads()
|
||||
|
||||
if warp_id == 9:
|
||||
# TMA warp
|
||||
stage_id = 0
|
||||
parity = 1
|
||||
|
||||
for global_chunk_id in range(bid, num_global_chunks, grid_x):
|
||||
seq_id = chunk_indices[global_chunk_id, 0]
|
||||
chunk_id = chunk_indices[global_chunk_id, 1]
|
||||
bos = cu_seqlens[seq_id]
|
||||
|
||||
# copy Q and K
|
||||
q_tile = cute.local_tile(
|
||||
cute.domain_offset((bos, 0), tmaQ[None, k_head_id, None]),
|
||||
tiler=(BT, K_dim),
|
||||
coord=(chunk_id, 0),
|
||||
)
|
||||
k_tile = cute.local_tile(
|
||||
cute.domain_offset((bos, 0), tmaK[None, k_head_id, None]),
|
||||
tiler=(BT, K_dim),
|
||||
coord=(chunk_id, 0),
|
||||
)
|
||||
mbar = qk_full_mbar + stage_id
|
||||
|
||||
cute.arch.mbarrier_wait(qk_empty_mbar + stage_id, parity)
|
||||
|
||||
with cute.arch.elect_one():
|
||||
STAGE_SIZE = BT * (K_dim + K_dim) * 2
|
||||
cute.arch.mbarrier_arrive_and_expect_tx(mbar, STAGE_SIZE)
|
||||
simple_tma_copy(Q_tma_atom, q_tile, sQ[None, None, stage_id], mbar)
|
||||
simple_tma_copy(K_tma_atom, k_tile, sK[None, None, stage_id], mbar)
|
||||
|
||||
# copy H and V
|
||||
gH = tmaH[global_chunk_id * self.Hv + v_head_id, None, None]
|
||||
gV = cute.local_tile(
|
||||
tmaV[None, v_head_id, None],
|
||||
tiler=(BT, V_dim),
|
||||
coord=(global_chunk_id, 0),
|
||||
)
|
||||
mbar = hv_full_mbar + stage_id
|
||||
|
||||
cute.arch.mbarrier_wait(pv_mma_mbar + stage_id, parity)
|
||||
|
||||
with cute.arch.elect_one():
|
||||
H_STAGE_SIZE = V_dim * K_dim * 2
|
||||
V_STAGE_SIZE = BT * V_dim * 2
|
||||
cute.arch.mbarrier_arrive_and_expect_tx(
|
||||
mbar, H_STAGE_SIZE + V_STAGE_SIZE
|
||||
)
|
||||
simple_tma_copy(
|
||||
H_tma_atom, gH, sH[None, None, stage_id], mbar, EVICT_FIRST
|
||||
)
|
||||
simple_tma_copy(
|
||||
V_tma_atom, gV, sV[None, None, stage_id], mbar, EVICT_FIRST
|
||||
)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
elif warp_id == 8:
|
||||
# MMA warp
|
||||
_tcgen05.alloc(taddr)
|
||||
|
||||
# LBO=BT*128 is ignored for K-major
|
||||
sdesc_template = _tcgen05.make_sdesc_128B_swizzle(BT * 128)
|
||||
qk_idesc = _tcgen05.make_bf16_idesc(BT, BT)
|
||||
qh_idesc = _tcgen05.make_bf16_idesc(BT, V_dim)
|
||||
pv_idesc = _tcgen05.make_bf16_idesc(BT, V_dim, transpose_B=True)
|
||||
|
||||
stage_id = 0
|
||||
tma_parity = 0
|
||||
mask_parity = 0
|
||||
|
||||
for global_chunk_id in range(bid, num_global_chunks, grid_x):
|
||||
qaddr = sQ[None, None, stage_id].iterator.toint()
|
||||
kaddr = sK[None, None, stage_id].iterator.toint()
|
||||
haddr = sH[None, None, stage_id].iterator.toint()
|
||||
vaddr = sV[None, None, stage_id].iterator.toint()
|
||||
qdesc_base = sdesc_template | (qaddr >> 4)
|
||||
kdesc_base = sdesc_template | (kaddr >> 4)
|
||||
hdesc_base = sdesc_template | (haddr >> 4)
|
||||
vdesc_base = sdesc_template | (vaddr >> 4)
|
||||
|
||||
##### 1st MMA: Q @ K.T #####
|
||||
# do this first to unblock mask(QK)
|
||||
cute.arch.mbarrier_wait(epi_mbar, mask_parity ^ 1)
|
||||
cute.arch.mbarrier_wait(qk_full_mbar + stage_id, tma_parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(K_dim // BT):
|
||||
for j in cutlass.range_constexpr(BT // 16):
|
||||
qdesc = qdesc_base | ((i * BT * 128 + j * 32) >> 4)
|
||||
kdesc = kdesc_base | ((i * BT * 128 + j * 32) >> 4)
|
||||
_tcgen05.mma_f16(
|
||||
qk_tmem, qdesc, kdesc, qk_idesc, (i > 0) or (j > 0)
|
||||
)
|
||||
_tcgen05.commit(qk_mbar)
|
||||
|
||||
##### 2nd MMA: Q @ H.T #####
|
||||
cute.arch.mbarrier_wait(hv_full_mbar + stage_id, tma_parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(K_dim // BT):
|
||||
for j in cutlass.range_constexpr(BT // 16):
|
||||
qdesc = qdesc_base | ((i * BT * 128 + j * 32) >> 4)
|
||||
hdesc = hdesc_base | ((i * V_dim * 128 + j * 32) >> 4)
|
||||
_tcgen05.mma_f16(
|
||||
qh_tmem, qdesc, hdesc, qh_idesc, (i > 0) or (j > 0)
|
||||
)
|
||||
_tcgen05.commit(qk_empty_mbar + stage_id)
|
||||
|
||||
##### 3rd MMA: P @ V #####
|
||||
# stalled by mask(QK)
|
||||
cute.arch.mbarrier_wait(mask_mbar, mask_parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(BT // 16):
|
||||
vdesc = vdesc_base | ((i * 16 * 128) >> 4)
|
||||
_tcgen05.mma_ts_f16(
|
||||
out_tmem, p_tmem + i * 8, vdesc, pv_idesc, i > 0
|
||||
)
|
||||
_tcgen05.commit(pv_mma_mbar + stage_id)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
tma_parity ^= 1
|
||||
mask_parity ^= 1
|
||||
|
||||
# wait for epilogue to finish for deallocation
|
||||
cute.arch.mbarrier_wait(epi_mbar, mask_parity ^ 1)
|
||||
_tcgen05.dealloc()
|
||||
|
||||
elif warp_id >= 4:
|
||||
# masking warps
|
||||
warp_id_ = warp_id % 4
|
||||
tid_ = tid % 128
|
||||
row0 = warp_id_ * 16 + lane_id // 4
|
||||
row1 = row0 + 8
|
||||
|
||||
parity = 0
|
||||
|
||||
# for ldmatrix layout later
|
||||
row_indices = cute.make_rmem_tensor(2, Int32)
|
||||
row_indices[0] = warp_id_ * 16 + lane_id // 4
|
||||
row_indices[1] = warp_id_ * 16 + lane_id // 4 + 8
|
||||
row_indices = row_indices.load().reshape((1, 2))
|
||||
|
||||
col_indices = cute.make_rmem_tensor(2, Int32)
|
||||
col_indices[0] = (lane_id % 4) * 2
|
||||
col_indices[1] = (lane_id % 4) * 2 + 1
|
||||
col_indices = col_indices.load().reshape((2, 1))
|
||||
|
||||
for global_chunk_id in range(bid, num_global_chunks, grid_x):
|
||||
if tid_ < BT:
|
||||
seq_id = chunk_indices[global_chunk_id, 0]
|
||||
chunk_id = chunk_indices[global_chunk_id, 1]
|
||||
bos = cu_seqlens[seq_id]
|
||||
eos = cu_seqlens[seq_id + 1]
|
||||
|
||||
t_ = bos + chunk_id * BT + tid_
|
||||
s_g_cu[tid_] = g_cu[t_, v_head_id] if t_ < eos else Float32(0.0)
|
||||
|
||||
# wait for QK MMA
|
||||
if warp_id_ == 0:
|
||||
cute.arch.mbarrier_wait(qk_mbar, parity)
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
qk = _tcgen05.ld(warp_id_ * 32, qk_tmem, "16x256b", BT // 8)
|
||||
qk = qk.reshape((2, 2, BT // 8))
|
||||
_tcgen05.wait_ld()
|
||||
|
||||
g_cu_rows = cute.make_rmem_tensor(2, Float32)
|
||||
g_cu_rows[0] = s_g_cu[row0]
|
||||
g_cu_rows[1] = s_g_cu[row1]
|
||||
g_cu_rows = g_cu_rows.load().reshape((1, 2))
|
||||
|
||||
for i in cutlass.range_constexpr(BT // 8):
|
||||
col = i * 8 + (lane_id % 4) * 2
|
||||
g_cu_cols = cute.make_rmem_tensor(2, Float32)
|
||||
g_cu_cols[0] = s_g_cu[col]
|
||||
g_cu_cols[1] = s_g_cu[col + 1]
|
||||
g_cu_cols = g_cu_cols.load().reshape((2, 1))
|
||||
|
||||
# apply gamma and causal mask
|
||||
Gamma = cute.math.exp(g_cu_rows - g_cu_cols, fastmath=True)
|
||||
tmp = qk[None, None, i] * Gamma
|
||||
tmp = cute.where(row_indices >= col_indices + i * 8, tmp, 0.0)
|
||||
|
||||
# CuteDSL can't emit cvt.bf16x2.f32 here
|
||||
attn_lo = cute.make_rmem_tensor(2, Uint32)
|
||||
attn_lo[0] = cvt.fp32x2_to_bf16x2(tmp[0, 0], tmp[1, 0])
|
||||
attn_lo[1] = cvt.fp32x2_to_bf16x2(tmp[0, 1], tmp[1, 1])
|
||||
_tcgen05.st(warp_id_ * 32, p_tmem + i * 4, "16x128b", 1, attn_lo)
|
||||
|
||||
_tcgen05.wait_st()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(mask_mbar)
|
||||
|
||||
parity ^= 1
|
||||
|
||||
else:
|
||||
# epilogue warps
|
||||
# for ldmatrix layout later
|
||||
row0 = warp_id * 16 + lane_id // 4
|
||||
row1 = row0 + 8
|
||||
|
||||
stage_id = 0
|
||||
mma_parity = 0
|
||||
|
||||
op = cute.nvgpu.CopyUniversalOp()
|
||||
cp_4B = cute.make_copy_atom(op, BFloat16, num_bits_per_copy=32)
|
||||
stsm_op = warp.StMatrix8x8x16bOp(num_matrices=4, transpose=False)
|
||||
stsm_atom = cute.make_copy_atom(stsm_op, BFloat16)
|
||||
|
||||
# ldmatrix layout
|
||||
# [total_seq_len, ((2, 4, WIDTH/8), V_DIM/WIDTH)]
|
||||
WIDTH = 64
|
||||
o_view = cute.logical_divide(
|
||||
o[None, v_head_id, None],
|
||||
(None, cute.make_layout((2, 4, WIDTH // 8))),
|
||||
)
|
||||
# select lane: [total_seq_len, 2, WIDTH/8, V_DIM/WIDTH]
|
||||
o_view = o_view[None, ((None, lane_id % 4, None), None)]
|
||||
|
||||
for global_chunk_id in range(bid, num_global_chunks, grid_x):
|
||||
seq_id = chunk_indices[global_chunk_id, 0]
|
||||
chunk_id = chunk_indices[global_chunk_id, 1]
|
||||
bos = cu_seqlens[seq_id]
|
||||
eos = cu_seqlens[seq_id + 1]
|
||||
chunk_start = bos + chunk_id * BT
|
||||
full_chunk = chunk_start + BT <= eos
|
||||
|
||||
g_cu_rows = cute.make_rmem_tensor(2, Float32)
|
||||
g_cu_rows.fill(0.0)
|
||||
|
||||
# load g_cu
|
||||
if chunk_start + row0 < eos:
|
||||
g_cu_rows[0] = cute.math.exp(
|
||||
g_cu[chunk_start + row0, v_head_id], fastmath=True
|
||||
)
|
||||
if chunk_start + row1 < eos:
|
||||
g_cu_rows[1] = cute.math.exp(
|
||||
g_cu[chunk_start + row1, v_head_id], fastmath=True
|
||||
)
|
||||
g_cu_rows = g_cu_rows.load().reshape((1, 2, 1))
|
||||
|
||||
if warp_id == 0:
|
||||
cute.arch.mbarrier_wait(pv_mma_mbar + stage_id, mma_parity)
|
||||
elif warp_id == 3 and full_chunk:
|
||||
cute.arch.cp_async_bulk_wait_group(0, read=True)
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
if full_chunk:
|
||||
# use TMA store: tmem->rmem->smem->gmem
|
||||
for i in cutlass.range_constexpr(V_dim // WIDTH):
|
||||
qh = _tcgen05.ld(
|
||||
warp_id * 32, qh_tmem + i * WIDTH, "16x256b", WIDTH // 8
|
||||
)
|
||||
pv = _tcgen05.ld(
|
||||
warp_id * 32, out_tmem + i * WIDTH, "16x256b", WIDTH // 8
|
||||
)
|
||||
_tcgen05.wait_ld()
|
||||
if i == V_dim // WIDTH - 1:
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(epi_mbar)
|
||||
|
||||
qh = qh.reshape((2, 2, WIDTH // 8))
|
||||
pv = pv.reshape((2, 2, WIDTH // 8))
|
||||
|
||||
out_f32 = scale * (g_cu_rows * qh + pv)
|
||||
out_bf16 = cute.make_rmem_tensor((8, WIDTH // 16), BFloat16)
|
||||
out_bf16.store(out_f32.to(BFloat16).reshape((8, WIDTH // 16)))
|
||||
|
||||
# TODO: issue single cute.copy()
|
||||
for j in cutlass.range_constexpr(WIDTH // 16):
|
||||
s_row = warp_id * 16 + lane_id % 16
|
||||
s_col = i * (WIDTH // 8) + j * 2 + lane_id // 16
|
||||
sO_tile = cute.local_tile(sO[s_row, None], (8,), (s_col,))
|
||||
cute.copy(stsm_atom, out_bf16[None, j], sO_tile)
|
||||
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
fence_before_tma_store()
|
||||
if warp_id == 3:
|
||||
gO = cute.local_tile(
|
||||
cute.domain_offset((bos, 0), tmaO[None, v_head_id, None]),
|
||||
tiler=(BT, V_dim),
|
||||
coord=(chunk_id, 0),
|
||||
)
|
||||
simple_tma_copy(O_tma_atom, sO, gO)
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_commit_group()
|
||||
|
||||
else:
|
||||
# direct gmem store
|
||||
# TODO: explore doing multiple 1D TMAs
|
||||
for i in cutlass.range_constexpr(V_dim // WIDTH):
|
||||
qh = _tcgen05.ld(
|
||||
warp_id * 32, qh_tmem + i * WIDTH, "16x256b", WIDTH // 8
|
||||
)
|
||||
pv = _tcgen05.ld(
|
||||
warp_id * 32, out_tmem + i * WIDTH, "16x256b", WIDTH // 8
|
||||
)
|
||||
_tcgen05.wait_ld()
|
||||
if i == V_dim // WIDTH - 1:
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(epi_mbar)
|
||||
|
||||
qh = qh.reshape((2, 2, WIDTH // 8))
|
||||
pv = pv.reshape((2, 2, WIDTH // 8))
|
||||
|
||||
out_f32 = scale * (g_cu_rows * qh + pv)
|
||||
out_bf16 = cute.make_rmem_tensor((2, 2, WIDTH // 8), BFloat16)
|
||||
out_bf16.store(out_f32.to(BFloat16))
|
||||
|
||||
if chunk_start + row0 < eos:
|
||||
cute.copy(
|
||||
cp_4B,
|
||||
out_bf16[None, 0, None],
|
||||
o_view[chunk_start + row0, None, None, i],
|
||||
)
|
||||
if chunk_start + row1 < eos:
|
||||
cute.copy(
|
||||
cp_4B,
|
||||
out_bf16[None, 1, None],
|
||||
o_view[chunk_start + row1, None, None, i],
|
||||
)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
mma_parity ^= 1
|
||||
|
||||
@cache
|
||||
@staticmethod
|
||||
def compile(
|
||||
H: int,
|
||||
Hv: int,
|
||||
K_dim: int,
|
||||
V_dim: int,
|
||||
BT: int = 64,
|
||||
num_stages: int = 2,
|
||||
):
|
||||
total_t = cute.sym_int()
|
||||
pad_t = cute.sym_int()
|
||||
total_chunks_n = cute.sym_int()
|
||||
h_outer_n = cute.sym_int()
|
||||
cu_entries = cute.sym_int()
|
||||
|
||||
q = make_fake_tensor(BFloat16, (total_t, H, K_dim), divisibility=16)
|
||||
k = make_fake_tensor(BFloat16, (total_t, H, K_dim), divisibility=16)
|
||||
v_new = make_fake_tensor(BFloat16, (pad_t, Hv, V_dim), divisibility=16)
|
||||
h_flat = make_fake_tensor(BFloat16, (h_outer_n, V_dim, K_dim), divisibility=16)
|
||||
g_cu = make_fake_tensor(Float32, (total_t, Hv), divisibility=4)
|
||||
o = make_fake_tensor(BFloat16, (total_t, Hv, V_dim), divisibility=16)
|
||||
cu_seqlens = make_fake_tensor(Int32, (cu_entries,), divisibility=1)
|
||||
chunk_indices = make_fake_tensor(Int32, (total_chunks_n, 2), divisibility=2)
|
||||
total_chunks = make_fake_tensor(Int32, (1,), divisibility=1)
|
||||
|
||||
kernel = Sm100ChunkOKernel(
|
||||
H,
|
||||
Hv,
|
||||
K_dim,
|
||||
V_dim,
|
||||
BT,
|
||||
num_stages,
|
||||
)
|
||||
stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
|
||||
return cute.compile(
|
||||
kernel,
|
||||
q,
|
||||
k,
|
||||
v_new,
|
||||
h_flat,
|
||||
g_cu,
|
||||
o,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
total_chunks,
|
||||
Float32(1.0),
|
||||
Int32(148),
|
||||
stream,
|
||||
options="--enable-tvm-ffi",
|
||||
)
|
||||
|
||||
|
||||
def o_cutedsl(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v_new_chunks: torch.Tensor,
|
||||
h: torch.Tensor,
|
||||
g_cu: torch.Tensor,
|
||||
o: torch.Tensor,
|
||||
cu_seqlens: torch.Tensor,
|
||||
chunk_indices: torch.Tensor,
|
||||
total_chunks: torch.Tensor,
|
||||
scale: float,
|
||||
num_sms: int = 148,
|
||||
) -> None:
|
||||
_, H, K_dim = q.shape
|
||||
_, Hv, V_dim = o.shape
|
||||
|
||||
Sm100ChunkOKernel.compile(H, Hv, K_dim, V_dim)(
|
||||
q,
|
||||
k,
|
||||
v_new_chunks.view(-1, Hv, V_dim),
|
||||
h.view(-1, V_dim, K_dim),
|
||||
g_cu,
|
||||
o,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
total_chunks,
|
||||
float(scale),
|
||||
num_sms,
|
||||
)
|
||||
@@ -0,0 +1,175 @@
|
||||
"""CuTe DSL kernels for GDN (Gated Delta Network) linear attention.
|
||||
|
||||
Decode path uses the existing ``cutedsl_fused_sigmoid_gating_delta_rule_update``
|
||||
(works on SM90+).
|
||||
|
||||
Prefill (extend) path uses the ported vLLM SM100 chunkwise kernel
|
||||
(``chunk_gated_delta_rule_cutedsl``). Requires SM100+ and ``head_k_dim == 128``.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.jit_kernel.cutedsl_gdn import cutedsl_fused_sigmoid_gating_delta_rule_update
|
||||
from sglang.srt.layers.attention.linear.kernels.kernel_backend import (
|
||||
LinearAttnKernelBase,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_blackwell() -> bool:
|
||||
"""True iff running on SM100+ (Blackwell) where the ported kernel is valid."""
|
||||
if not torch.cuda.is_available():
|
||||
return False
|
||||
major, _ = torch.cuda.get_device_capability()
|
||||
return major >= 10
|
||||
|
||||
|
||||
class CuteDSLGDNKernel(LinearAttnKernelBase):
|
||||
"""CuTe DSL kernel for GDN.
|
||||
|
||||
Decode: ``cutedsl_fused_sigmoid_gating_delta_rule_update`` (SM90+).
|
||||
Extend (prefill): chunkwise ``chunk_gated_delta_rule_cutedsl``
|
||||
(SM100+ only, ``head_k_dim`` must be 128). On SM90 the prefill path is
|
||||
unsupported; callers should query :attr:`supports_prefill` and fall back
|
||||
to another backend (e.g. Triton).
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# The Blackwell extend kernel uses tcgen05/TMA-bulk-swizzle features
|
||||
# that don't exist on SM90. The decode kernel does work on SM90+.
|
||||
self.supports_prefill = _is_blackwell()
|
||||
|
||||
# Heavy CuteDSL imports are deferred to extend() so SM90 boxes can
|
||||
# still construct the kernel just for decode.
|
||||
self._extend_fn: Optional[callable] = None
|
||||
self._prepare_meta_fn: Optional[callable] = None
|
||||
self._l2norm_fn: Optional[callable] = None
|
||||
|
||||
def _ensure_extend_loaded(self, head_k_dim: int) -> None:
|
||||
if self._extend_fn is not None:
|
||||
return
|
||||
if not self.supports_prefill:
|
||||
major = (
|
||||
torch.cuda.get_device_capability()[0]
|
||||
if torch.cuda.is_available()
|
||||
else -1
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"CuTe DSL GDN prefill requires SM100+ (Blackwell); got SM{major}."
|
||||
)
|
||||
if head_k_dim != 128:
|
||||
raise RuntimeError(
|
||||
f"CuTe DSL GDN prefill requires head_k_dim=128, got {head_k_dim}."
|
||||
)
|
||||
from sglang.srt.layers.attention.fla.l2norm import l2norm_fwd
|
||||
from sglang.srt.layers.attention.linear.kernels.gdn_blackwell import (
|
||||
chunk_gated_delta_rule_cutedsl,
|
||||
prepare_metadata_cutedsl,
|
||||
)
|
||||
|
||||
self._extend_fn = chunk_gated_delta_rule_cutedsl
|
||||
self._prepare_meta_fn = prepare_metadata_cutedsl
|
||||
self._l2norm_fn = l2norm_fwd
|
||||
logger.info("Using CuTe DSL GDN prefill (Blackwell)")
|
||||
|
||||
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_delta_rule_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,
|
||||
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:
|
||||
head_k_dim = k.shape[-1]
|
||||
self._ensure_extend_loaded(head_k_dim)
|
||||
|
||||
total_seq_len = q.shape[1]
|
||||
num_v_heads = v.shape[2]
|
||||
head_v_dim = v.shape[3]
|
||||
|
||||
# L2 norm Q/K outside the kernel (same as flashinfer path).
|
||||
q_norm = self._l2norm_fn(q[0].contiguous()).unsqueeze(0)
|
||||
k_norm = self._l2norm_fn(k[0].contiguous()).unsqueeze(0)
|
||||
v_in = v[0].contiguous().unsqueeze(0)
|
||||
# Kernel expects log-space float32 gate per (token, v-head).
|
||||
g_in = g[0].to(torch.float32).unsqueeze(0)
|
||||
beta_in = beta[0].to(torch.float32).unsqueeze(0)
|
||||
|
||||
cu_seqlens = query_start_loc.to(torch.int32)
|
||||
|
||||
# Pool gather: remap padding (-1) to the last (sentinel) slot.
|
||||
ssm_cache_indices = torch.where(
|
||||
cache_indices >= 0,
|
||||
cache_indices,
|
||||
ssm_states.shape[0] - 1,
|
||||
).to(torch.long)
|
||||
initial_state = ssm_states[ssm_cache_indices].contiguous()
|
||||
|
||||
chunk_indices, chunk_offsets = self._prepare_meta_fn(
|
||||
cu_seqlens, total_seq_len, chunk_size=64
|
||||
)
|
||||
|
||||
output, final_state = self._extend_fn(
|
||||
q=q_norm,
|
||||
k=k_norm,
|
||||
v=v_in,
|
||||
g=g_in,
|
||||
beta=beta_in,
|
||||
initial_state=initial_state,
|
||||
cu_seqlens=cu_seqlens,
|
||||
chunk_indices=chunk_indices,
|
||||
chunk_offsets=chunk_offsets,
|
||||
)
|
||||
|
||||
ssm_states.index_copy_(
|
||||
0,
|
||||
ssm_cache_indices,
|
||||
final_state.to(ssm_states.dtype),
|
||||
)
|
||||
|
||||
# Match Triton extend interface: (output, last_recurrent_state, h).
|
||||
# We've already written state back, so no need to return it.
|
||||
return output, None, None
|
||||
|
||||
def target_verify(self, *args, **kwargs):
|
||||
raise NotImplementedError("CuteDSLGDNKernel does not support target_verify")
|
||||
@@ -0,0 +1,382 @@
|
||||
"""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)
|
||||
@@ -0,0 +1,241 @@
|
||||
import torch
|
||||
|
||||
from sglang.srt.layers.attention.linear.kernels.kernel_backend import (
|
||||
LinearAttnKernelBase,
|
||||
)
|
||||
from sglang.srt.utils import is_cpu, is_npu, is_xpu
|
||||
|
||||
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_recurrent_linear_replayssm import (
|
||||
fused_recurrent_gdn_replayssm_decode,
|
||||
)
|
||||
from sglang.srt.layers.attention.fla.fused_sigmoid_gating_recurrent import (
|
||||
fused_sigmoid_gating_delta_rule_update,
|
||||
)
|
||||
|
||||
if is_npu():
|
||||
from sgl_kernel_npu.fla.chunk import chunk_gated_delta_rule_npu
|
||||
from sgl_kernel_npu.fla.fused_sigmoid_gating_recurrent import (
|
||||
fused_sigmoid_gating_delta_rule_update_npu,
|
||||
)
|
||||
|
||||
chunk_gated_delta_rule = chunk_gated_delta_rule_npu
|
||||
fused_sigmoid_gating_delta_rule_update = fused_sigmoid_gating_delta_rule_update_npu
|
||||
elif is_cpu():
|
||||
from sgl_kernel.mamba import chunk_gated_delta_rule_cpu
|
||||
|
||||
chunk_gated_delta_rule = chunk_gated_delta_rule_cpu
|
||||
fused_sigmoid_gating_delta_rule_update = (
|
||||
torch.ops.sgl_kernel.fused_sigmoid_gating_delta_rule_update_cpu
|
||||
)
|
||||
elif is_xpu():
|
||||
from sglang.srt.hardware_backend.xpu.kernels.fla.fused_sigmoid_gating_recurrent import (
|
||||
fused_sigmoid_gating_delta_rule_update,
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
# GDN ReplaySSM buffered decode (slice 1a). Drop-in for the packed
|
||||
# decode: same args plus the three per-layer ring caches and the
|
||||
# per-row write cursor. When any ring tensor / cursor is None (flag
|
||||
# off) we fall through to the byte-identical legacy path below.
|
||||
replayssm_d = kwargs.get("replayssm_d")
|
||||
replayssm_k = kwargs.get("replayssm_k")
|
||||
replayssm_g = kwargs.get("replayssm_g")
|
||||
replayssm_write_pos = kwargs.get("replayssm_write_pos")
|
||||
# GDN ReplaySSM (slice 2b): optional per-row force-flush (radix track
|
||||
# boundary). None when radix tracking is off / flag off; the kernel
|
||||
# treats None as "no forced flush" (byte-identical to slice 1a/1b).
|
||||
replayssm_force_flush = kwargs.get("replayssm_force_flush")
|
||||
if (
|
||||
replayssm_d is not None
|
||||
and replayssm_k is not None
|
||||
and replayssm_g is not None
|
||||
and replayssm_write_pos is not None
|
||||
):
|
||||
fused_recurrent_gdn_replayssm_decode(
|
||||
mixed_qkv=mixed_qkv,
|
||||
a=a,
|
||||
b=b,
|
||||
A_log=A_log,
|
||||
dt_bias=dt_bias,
|
||||
scale=scale,
|
||||
initial_state=ssm_states,
|
||||
d_cache=replayssm_d,
|
||||
k_cache=replayssm_k,
|
||||
g_cache=replayssm_g,
|
||||
out=out,
|
||||
ssm_state_indices=cache_indices,
|
||||
write_pos=replayssm_write_pos,
|
||||
force_flush=replayssm_force_flush,
|
||||
use_qk_l2norm_in_kernel=True,
|
||||
)
|
||||
return out.transpose(0, 1)
|
||||
|
||||
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,
|
||||
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 fused_sigmoid_gating_delta_rule_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,
|
||||
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:
|
||||
recurrent_state = ssm_states
|
||||
recurrent_state_indices_args = {"initial_state_indices": cache_indices}
|
||||
if is_npu():
|
||||
recurrent_state = ssm_states[cache_indices]
|
||||
recurrent_state_indices_args = {}
|
||||
|
||||
return chunk_gated_delta_rule(
|
||||
q=q,
|
||||
k=k,
|
||||
v=v,
|
||||
g=g,
|
||||
beta=beta,
|
||||
initial_state=recurrent_state,
|
||||
cu_seqlens=query_start_loc,
|
||||
head_first=False,
|
||||
use_qk_l2norm_in_kernel=True,
|
||||
**recurrent_state_indices_args,
|
||||
)
|
||||
|
||||
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:
|
||||
return fused_sigmoid_gating_delta_rule_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,
|
||||
is_kda=False,
|
||||
# target_verify specific parameters
|
||||
disable_state_update=True,
|
||||
intermediate_states_buffer=intermediate_states_buffer,
|
||||
intermediate_state_indices=intermediate_state_indices,
|
||||
cache_steps=cache_steps,
|
||||
retrieve_parent_token=retrieve_parent_token,
|
||||
)
|
||||
@@ -0,0 +1,221 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# KDA (Kimi Delta Attention) SM100/Blackwell CuteDSL prefill pipeline.
|
||||
#
|
||||
# Mirrors gdn_blackwell but for KDA's PER-CHANNEL decay gate. A fused Triton
|
||||
# prologue computes the per-chunk cumsum g_cu and five pre-scaled key/query
|
||||
# tensors; three cutedsl kernels then run the chunked gated delta rule:
|
||||
# prologue -> kkt_inv_uw (U,W) -> h (V_new, per-chunk state, final state) -> o
|
||||
import torch
|
||||
|
||||
from .kernel_h import kda_h_cutedsl
|
||||
from .kernel_kkt_inv_uw import kkt_inv_uw_cutedsl
|
||||
from .kernel_o import kda_o_cutedsl
|
||||
from .prologue import kda_prologue
|
||||
|
||||
__all__ = ["chunk_kda_cutedsl", "prepare_metadata"]
|
||||
|
||||
|
||||
def prepare_metadata(cu_seqlens: torch.Tensor, chunk_size: int = 64):
|
||||
"""Build (chunk_indices [NT,2], chunk_offsets [N+1], total_chunks [1]).
|
||||
|
||||
chunk_indices[g] = (seq_id, local_chunk_id) for global chunk g.
|
||||
chunk_offsets[s] = number of chunks before sequence s.
|
||||
"""
|
||||
dev = cu_seqlens.device
|
||||
cs = cu_seqlens.to(torch.int64)
|
||||
seqlens = cs[1:] - cs[:-1]
|
||||
nchunks = (seqlens + chunk_size - 1) // chunk_size # [N]
|
||||
n = seqlens.numel()
|
||||
chunk_offsets = torch.zeros(n + 1, dtype=torch.int32, device=dev)
|
||||
chunk_offsets[1:] = nchunks.cumsum(0).to(torch.int32)
|
||||
total = int(chunk_offsets[-1].item())
|
||||
seq_id = torch.repeat_interleave(torch.arange(n, device=dev), nchunks)
|
||||
local = torch.arange(total, device=dev) - chunk_offsets[seq_id].to(torch.int64)
|
||||
chunk_indices = torch.stack(
|
||||
[seq_id.to(torch.int32), local.to(torch.int32)], dim=1
|
||||
).contiguous()
|
||||
total_chunks = torch.tensor([total], dtype=torch.int32, device=dev)
|
||||
return chunk_indices, chunk_offsets, total_chunks, total
|
||||
|
||||
|
||||
# Per-(Hv,K,V,device) grow-only scratch workspace. The cutedsl KKT/h/o kernels
|
||||
# are fast; the per-call PyTorch overhead (re-allocating + re-zeroing the eye and
|
||||
# the two pack buffers ~200MB/call, metadata recompute, a `.item()` sync) was what
|
||||
# dragged the full function below Triton. Reusing scratch across calls removes it.
|
||||
# Safe because KDA layers run sequentially on one CUDA stream (the next call's
|
||||
# kernels are ordered after this call's), and only the returned o/ht are fresh.
|
||||
_KDA_WS: dict = {}
|
||||
|
||||
|
||||
def _kda_workspace(q, T, Hv, K, V, cu_seqlens):
|
||||
import torch as _t
|
||||
|
||||
dev = q.device
|
||||
# Key by the current CUDA stream too: the scratch is process-global and
|
||||
# mutable, so two KDA forwards running concurrently on different streams
|
||||
# (e.g. two-batch overlap) must not share buffers. Within one forward all
|
||||
# KDA layers run on the same stream -> same key -> the reuse benefit holds.
|
||||
stream = _t.cuda.current_stream(device=dev).cuda_stream
|
||||
key = (Hv, K, V, dev, q.dtype, stream)
|
||||
ws = _KDA_WS.get(key)
|
||||
|
||||
# metadata: recompute only when cu_seqlens changes (object identity -> no
|
||||
# sync; within one forward all KDA layers share the same cu_seqlens object).
|
||||
if ws is None or ws["cu"] is not cu_seqlens:
|
||||
ci, co, tcs, total = prepare_metadata(cu_seqlens)
|
||||
else:
|
||||
ci, co, tcs, total = ws["ci"], ws["co"], ws["tcs"], ws["total"]
|
||||
pad_t = total * 64
|
||||
|
||||
if ws is None or ws["Tcap"] < T or ws["padcap"] < pad_t or ws["totalcap"] < total:
|
||||
Tcap = T if ws is None else max(T, ws["Tcap"])
|
||||
padcap = pad_t if ws is None else max(pad_t, ws["padcap"])
|
||||
totalcap = total if ws is None else max(total, ws["totalcap"])
|
||||
ws = {
|
||||
"kL": q.new_zeros(Tcap, Hv, K, dtype=_t.bfloat16),
|
||||
"qg2": q.new_zeros(Tcap, Hv, K, dtype=_t.bfloat16),
|
||||
"eye": q.new_zeros(Tcap, Hv, K, dtype=_t.bfloat16),
|
||||
"U": q.new_empty(padcap, Hv, V, dtype=_t.bfloat16),
|
||||
"W": q.new_empty(padcap, Hv, K, dtype=_t.bfloat16),
|
||||
"Vn": q.new_empty(padcap, Hv, V, dtype=_t.bfloat16),
|
||||
"hc": q.new_empty(totalcap, Hv, V, K, dtype=_t.bfloat16),
|
||||
"Tcap": Tcap,
|
||||
"padcap": padcap,
|
||||
"totalcap": totalcap,
|
||||
"cu": None,
|
||||
"eye_hw": 0,
|
||||
}
|
||||
_KDA_WS[key] = ws
|
||||
|
||||
ws["ci"], ws["co"], ws["tcs"], ws["total"] = ci, co, tcs, total
|
||||
|
||||
# eye is the one-hot(chunk-position) identity injection: recompute only on a
|
||||
# cu_seqlens change. Clear the prior high-water region then scatter the new 1s.
|
||||
if ws["cu"] is not cu_seqlens:
|
||||
eye = ws["eye"]
|
||||
hw = max(ws["eye_hw"], T)
|
||||
eye[:hw].zero_()
|
||||
# Match cu_seqlens' dtype (typically int32) so searchsorted/indexing avoid
|
||||
# the int64 casts, while staying correct if cu_seqlens is passed as int64.
|
||||
tok = _t.arange(T, device=dev, dtype=cu_seqlens.dtype)
|
||||
seq_of = _t.searchsorted(cu_seqlens, tok, right=True) - 1
|
||||
pos = (tok - cu_seqlens[seq_of]) % 64
|
||||
eye[tok, :, pos] = 1.0
|
||||
ws["eye_hw"] = T
|
||||
ws["cu"] = cu_seqlens
|
||||
return ws, ci, co, tcs, total, pad_t
|
||||
|
||||
|
||||
def chunk_kda_cutedsl(
|
||||
q: torch.Tensor, # [T, Hv, K] bf16, L2-normed
|
||||
k: torch.Tensor, # [T, Hv, K] bf16, L2-normed
|
||||
v: torch.Tensor, # [T, Hv, V] bf16
|
||||
g: torch.Tensor, # [T, Hv, K] log-decay. RAW if A_log given, else pre-activated
|
||||
beta: torch.Tensor, # [T, Hv] fp32, post-sigmoid
|
||||
h0: torch.Tensor, # [N, Hv, V, K] (initial recurrent state, [V,K] layout)
|
||||
cu_seqlens: torch.Tensor,
|
||||
scale: float | None = None,
|
||||
num_sms: int | None = None,
|
||||
A_log: torch.Tensor | None = None, # [Hv]; if set, activate g internally
|
||||
dt_bias: torch.Tensor | None = None, # [Hv, K] or [Hv*K]
|
||||
lower_bound: float | None = None,
|
||||
):
|
||||
"""Run the KDA chunk gated-delta-rule prefill. Returns (o [T,Hv,V], ht [N,Hv,V,K])."""
|
||||
import torch.nn.functional as F
|
||||
|
||||
T, Hv, K = q.shape
|
||||
V = v.shape[-1]
|
||||
if scale is None:
|
||||
scale = K**-0.5
|
||||
if num_sms is None:
|
||||
num_sms = torch.cuda.get_device_properties(q.device).multi_processor_count
|
||||
|
||||
# Gate activation (standard KDA gate). Fused into the prologue is a B2 TODO;
|
||||
# for now a small PyTorch pass, matching chunk_kda's kda_gate_chunk_cumsum.
|
||||
if A_log is not None:
|
||||
if lower_bound is not None:
|
||||
raise NotImplementedError(
|
||||
"KDA cutedsl: safe_gate (lower_bound) not yet supported"
|
||||
)
|
||||
x = g.float()
|
||||
if dt_bias is not None:
|
||||
x = x + dt_bias.float().view(1, Hv, K)
|
||||
g_act = -torch.exp(A_log.float()).view(1, Hv, 1) * F.softplus(x)
|
||||
else:
|
||||
g_act = g.float()
|
||||
|
||||
# Reusable scratch (eye/pack/U/W/V_new/h_chunks) + cached metadata; only the
|
||||
# returned o/ht are freshly allocated. This removes the ~0.2-0.6ms/call host
|
||||
# overhead (re-alloc + re-zero of ~200MB + metadata sync) that otherwise drags
|
||||
# the (fast) cutedsl kernels below Triton.
|
||||
ws, chunk_indices, chunk_offsets, total_chunks, total, pad_t = _kda_workspace(
|
||||
q, T, Hv, K, V, cu_seqlens
|
||||
)
|
||||
|
||||
# KL/qg2 from the prologue fold the decay with a chunk-global g_last reference
|
||||
# (exp(g_cu - g_last)), which overflows fp32 for real per-channel gates. They
|
||||
# are recomputed below; the prologue still gives the bounded KR/KG/qg/g_cu.
|
||||
_, KR, KG, qg, _, g_cu = kda_prologue(
|
||||
q, k, g_act, float(scale), cu_seqlens, chunk_indices, total
|
||||
)
|
||||
|
||||
# Sub-chunk-normalized intra-chunk gated KKT / QK from the FLA kernel (stable),
|
||||
# injected through the cutedsl KKT/Aqk MMAs as an identity-right-operand pass:
|
||||
# with kL'=M (M in the first 64 K-slots) and kR'=onehot(chunk-pos), the MMA
|
||||
# kL'@kR'.T == M, so kkt_inv_uw/kernel_o see the correct matrix without overflow.
|
||||
from sglang.srt.layers.attention.fla.kda import chunk_kda_scaled_dot_kkt_fwd
|
||||
|
||||
ones_beta = q.new_ones(1, T, Hv, dtype=torch.float32)
|
||||
M_kk, M_qk = chunk_kda_scaled_dot_kkt_fwd(
|
||||
q.unsqueeze(0).contiguous(),
|
||||
k.unsqueeze(0).contiguous(),
|
||||
gk=g_cu.unsqueeze(0),
|
||||
beta=ones_beta,
|
||||
scale=float(scale),
|
||||
cu_seqlens=cu_seqlens,
|
||||
chunk_size=64,
|
||||
)
|
||||
|
||||
# Pack M into the first 64 K-slots of the reused buffers; cols [64:128] stay 0
|
||||
# (never written since the one-time zeroed alloc), so the MxI injection is exact.
|
||||
kL_inj = ws["kL"][:T]
|
||||
qg2_inj = ws["qg2"][:T]
|
||||
kL_inj[:, :, :64] = M_kk[0].to(torch.bfloat16)
|
||||
qg2_inj[:, :, :64] = M_qk[0].to(torch.bfloat16)
|
||||
eye = ws["eye"][:T]
|
||||
|
||||
U = ws["U"][:pad_t]
|
||||
W = ws["W"][:pad_t]
|
||||
kkt_inv_uw_cutedsl(
|
||||
kL_inj,
|
||||
eye,
|
||||
KG,
|
||||
v,
|
||||
U,
|
||||
W,
|
||||
beta,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
total_chunks,
|
||||
num_sms=num_sms,
|
||||
)
|
||||
|
||||
V_new = ws["Vn"][:pad_t]
|
||||
h_chunks = ws["hc"][:total]
|
||||
ht = torch.empty_like(h0)
|
||||
kda_h_cutedsl(KR, U, W, V_new, g_cu, h_chunks, h0, ht, cu_seqlens, chunk_offsets)
|
||||
|
||||
o = q.new_empty(T, Hv, V, dtype=torch.bfloat16)
|
||||
kda_o_cutedsl(
|
||||
qg,
|
||||
qg2_inj,
|
||||
eye,
|
||||
V_new,
|
||||
h_chunks,
|
||||
o,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
total_chunks,
|
||||
num_sms=num_sms,
|
||||
)
|
||||
return o, ht
|
||||
@@ -0,0 +1,690 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# KDA (Kimi Delta Attention) SM100 chunk recurrent-state kernel.
|
||||
#
|
||||
# Idea is adopted from GDN blackwell kernel. KDA differs from GDN only in the
|
||||
# decay gate, which is PER-CHANNEL (one decay per key-dim k) instead of a single
|
||||
# scalar per head. The hard cross-token part of the per-channel decay is folded
|
||||
# OUTSIDE this kernel into the pre-scaled key tensor `kg`:
|
||||
#
|
||||
# kg[c, k] = k[c, k] * exp(g_cu_last[k] - g_cu[c, k]) (bounded, <= |k|)
|
||||
#
|
||||
# so the only in-kernel gate logic that remains is:
|
||||
# 1. state decay is PER-COLUMN: H[v, k] *= exp(g_cu_last[k]) (not a scalar)
|
||||
# 2. the H_new MMA consumes `kg` (pre-scaled) instead of raw K, and v_new stays
|
||||
# RAW (GDN instead scales v_new by the scalar exp(g_last - g_t) and uses raw K).
|
||||
#
|
||||
# Math per chunk (state S stored transposed as H = [V, K]):
|
||||
# V_new = U - W @ S (gate-free; W already gated in kkt stage)
|
||||
# H_scaled[v, k] = H[v, k] * exp(g_cu_last[k])
|
||||
# H_new = H_scaled + V_new.T @ kg
|
||||
from functools import cache
|
||||
|
||||
import cutlass
|
||||
import torch
|
||||
from cuda.bindings.driver import CUstream
|
||||
from cutlass import BFloat16, Float32, Int32, Int64, Uint32, cute
|
||||
from cutlass.cute.nvgpu import cpasync, warp
|
||||
from quack.compile_utils import make_fake_tensor
|
||||
|
||||
from sglang.srt.layers.attention.cute_utils import (
|
||||
EVICT_FIRST,
|
||||
_tcgen05,
|
||||
cvt,
|
||||
fence_before_tma_store,
|
||||
simple_tma_copy,
|
||||
)
|
||||
|
||||
|
||||
class Sm100KdaChunkHKernel:
|
||||
"""KDA per-chunk recurrent-state update (see module docstring)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
H: int,
|
||||
Hv: int,
|
||||
K_dim: int,
|
||||
V_dim: int,
|
||||
h_dtype: cutlass.Numeric = Float32,
|
||||
BT: int = 64,
|
||||
num_stages: int = 2,
|
||||
) -> None:
|
||||
assert Hv % H == 0
|
||||
assert K_dim == V_dim == 128
|
||||
assert BT == 64
|
||||
self.H = H
|
||||
self.Hv = Hv
|
||||
self.K_dim = K_dim
|
||||
self.V_dim = V_dim
|
||||
self.h_dtype = h_dtype
|
||||
self.BT = BT
|
||||
self.num_stages = num_stages
|
||||
self.num_warps = 10
|
||||
|
||||
@cute.jit
|
||||
def _make_bf16_tma_args(
|
||||
self,
|
||||
tensor: cute.Tensor,
|
||||
dim: cutlass.Constexpr[int],
|
||||
op: cpasync.TmaCopyOp,
|
||||
stages: cutlass.Constexpr[int],
|
||||
):
|
||||
swizzle_128B = cute.make_swizzle(3, 4, 3)
|
||||
slayout = cute.make_layout(
|
||||
(self.BT, 1, (64, dim // 64), stages),
|
||||
stride=(64, 0, (1, self.BT * 64), self.BT * dim),
|
||||
)
|
||||
slayout = cute.make_composed_layout(swizzle_128B, 0, slayout)
|
||||
atom, tma_tensor = cpasync.make_tiled_tma_atom(
|
||||
op,
|
||||
cute.logical_divide(tensor, (None, None, 64)),
|
||||
slayout,
|
||||
cta_tiler=(self.BT, 1, dim),
|
||||
)
|
||||
return atom, tma_tensor, slayout
|
||||
|
||||
@cute.jit
|
||||
def _make_h_tma_args(self, tensor: cute.Tensor, op: cpasync.TmaCopyOp):
|
||||
num_elems = 128 // (tensor.element_type.width // 8)
|
||||
swizzle_128B = cute.make_swizzle(3, 4, 3)
|
||||
slayout = cute.make_layout(
|
||||
(1, 1, self.V_dim, (num_elems, self.K_dim // num_elems)),
|
||||
stride=(0, 0, num_elems, (1, self.V_dim * num_elems)),
|
||||
)
|
||||
slayout = cute.make_composed_layout(swizzle_128B, 0, slayout)
|
||||
atom, tma_tensor = cpasync.make_tiled_tma_atom(
|
||||
op,
|
||||
cute.logical_divide(tensor, (None, None, None, num_elems)),
|
||||
slayout,
|
||||
cta_tiler=(1, 1, self.V_dim, self.K_dim),
|
||||
)
|
||||
return atom, tma_tensor, slayout
|
||||
|
||||
@cute.jit
|
||||
def __call__(
|
||||
self,
|
||||
K: cute.Tensor, # KDA: this is `kg`, the per-channel pre-scaled key [T, Hv, K]
|
||||
V: cute.Tensor, # = U from kkt stage
|
||||
W: cute.Tensor,
|
||||
V_new: cute.Tensor,
|
||||
g_cu: cute.Tensor, # KDA: [T, Hv, K] per-channel cumsum
|
||||
h: cute.Tensor,
|
||||
h0: cute.Tensor,
|
||||
ht: cute.Tensor,
|
||||
cu_seqlens: cute.Tensor,
|
||||
chunk_offsets: cute.Tensor,
|
||||
stream: CUstream,
|
||||
):
|
||||
tma_g2s = cpasync.CopyBulkTensorTileG2SOp()
|
||||
tma_s2g = cpasync.CopyBulkTensorTileS2GOp()
|
||||
|
||||
K_args = self._make_bf16_tma_args(K, self.K_dim, tma_g2s, self.num_stages)
|
||||
V_args = self._make_bf16_tma_args(V, self.V_dim, tma_g2s, self.num_stages)
|
||||
W_args = self._make_bf16_tma_args(W, self.K_dim, tma_g2s, self.num_stages)
|
||||
V_new_args = self._make_bf16_tma_args(V_new, self.V_dim, tma_s2g, 1)
|
||||
H0_args = self._make_h_tma_args(h0, tma_g2s)
|
||||
HT_args = self._make_h_tma_args(ht, tma_s2g)
|
||||
H_args = self._make_h_tma_args(h, tma_s2g)
|
||||
|
||||
grid = (self.Hv, h0.shape[0], 1)
|
||||
block = (self.num_warps * 32, 1, 1)
|
||||
self.kernel(
|
||||
K_args,
|
||||
V_args,
|
||||
W_args,
|
||||
V_new_args,
|
||||
H0_args,
|
||||
HT_args,
|
||||
H_args,
|
||||
g_cu,
|
||||
cu_seqlens,
|
||||
chunk_offsets,
|
||||
).launch(grid=grid, block=block, stream=stream)
|
||||
|
||||
@cute.kernel
|
||||
def kernel(
|
||||
self,
|
||||
K_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
V_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
W_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
V_new_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
H0_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
HT_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
H_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
g_cu: cute.Tensor,
|
||||
cu_seqlens: cute.Tensor,
|
||||
chunk_offsets: cute.Tensor,
|
||||
):
|
||||
tid, _, _ = cute.arch.thread_idx()
|
||||
head_id, seq_id, _ = cute.arch.block_idx()
|
||||
warp_id = cute.arch.make_warp_uniform(tid // 32)
|
||||
lane_id = tid % 32
|
||||
|
||||
BT = self.BT
|
||||
V_dim = self.V_dim
|
||||
K_dim = self.K_dim
|
||||
num_stages = self.num_stages
|
||||
is_f32 = self.h_dtype == Float32
|
||||
|
||||
K_tma_atom, tmaK, sK_layout = K_args
|
||||
V_tma_atom, tmaV, sV_layout = V_args
|
||||
W_tma_atom, tmaW, sW_layout = W_args
|
||||
V_new_tma_atom, tmaV_new, sV_new_layout = V_new_args
|
||||
H0_tma_atom, tmaH0, sH0_layout = H0_args
|
||||
HT_tma_atom, tmaHT, _ = HT_args
|
||||
H_tma_atom, tmaH, sH_layout = H_args
|
||||
|
||||
def allocate_tensor(smem, dtype, layout):
|
||||
return smem.allocate_tensor(
|
||||
dtype, layout.outer, byte_alignment=128, swizzle=layout.inner
|
||||
)
|
||||
|
||||
smem = cutlass.utils.SmemAllocator()
|
||||
|
||||
sW = allocate_tensor(smem, BFloat16, sW_layout)[None, 0, None, None]
|
||||
sV = allocate_tensor(smem, BFloat16, sV_layout)[None, 0, None, None]
|
||||
sK = allocate_tensor(smem, BFloat16, sK_layout)[None, 0, None, None]
|
||||
sH0 = allocate_tensor(smem, self.h_dtype, sH0_layout)[0, 0, None, None]
|
||||
sH = allocate_tensor(smem, BFloat16, sH_layout)[0, 0, None, None]
|
||||
sV_new = allocate_tensor(smem, BFloat16, sV_new_layout)[None, 0, None, 0]
|
||||
|
||||
# KDA: per-channel end-of-chunk decay exp(g_cu_last[k]); shared by all V-rows.
|
||||
s_gl_exp = smem.allocate_array(Float32, K_dim)
|
||||
tma_mbar = smem.allocate_array(Int64, num_stages)
|
||||
wh_in_mbar = smem.allocate_array(Int64, num_stages)
|
||||
wh_done_mbar = smem.allocate_array(Int64, num_stages)
|
||||
vk_in_mbar = smem.allocate_array(Int64, num_stages)
|
||||
vk_done_mbar = smem.allocate_array(Int64, num_stages)
|
||||
h0_mbar = smem.allocate_array(Int64, 1)
|
||||
taddr = smem.allocate(Int32, 4)
|
||||
|
||||
wh_tmem = 0
|
||||
vk_tmem = wh_tmem + BT
|
||||
h_tmem_base = vk_tmem + K_dim
|
||||
v_tmem_base = h_tmem_base + K_dim // 2
|
||||
|
||||
if warp_id == 0:
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(num_stages):
|
||||
cute.arch.mbarrier_init(tma_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(wh_in_mbar + i, 256)
|
||||
cute.arch.mbarrier_init(wh_done_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(vk_in_mbar + i, 256)
|
||||
cute.arch.mbarrier_init(vk_done_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(h0_mbar, 1)
|
||||
cute.arch.mbarrier_init_fence()
|
||||
elif warp_id == 1:
|
||||
cpasync.prefetch_descriptor(H0_tma_atom)
|
||||
cpasync.prefetch_descriptor(W_tma_atom)
|
||||
cpasync.prefetch_descriptor(V_tma_atom)
|
||||
cpasync.prefetch_descriptor(K_tma_atom)
|
||||
cpasync.prefetch_descriptor(HT_tma_atom)
|
||||
cpasync.prefetch_descriptor(H_tma_atom)
|
||||
cpasync.prefetch_descriptor(V_new_tma_atom)
|
||||
cute.arch.sync_threads()
|
||||
|
||||
bos = cu_seqlens[seq_id]
|
||||
eos = cu_seqlens[seq_id + 1]
|
||||
seqlen = eos - bos
|
||||
num_chunks = cute.ceil_div(seqlen, BT)
|
||||
|
||||
if warp_id == 9:
|
||||
# TMA warp
|
||||
stage_id = 0
|
||||
parity = 1
|
||||
|
||||
chunk_offset = chunk_offsets[seq_id]
|
||||
|
||||
# load H0
|
||||
with cute.arch.elect_one():
|
||||
H0_size = V_dim * K_dim * self.h_dtype.width // 8
|
||||
cute.arch.mbarrier_arrive_and_expect_tx(h0_mbar, H0_size)
|
||||
simple_tma_copy(
|
||||
H0_tma_atom, tmaH0[seq_id, head_id, None, None], sH0, h0_mbar
|
||||
)
|
||||
|
||||
gW_tiles = cute.logical_divide(tmaW[None, head_id, None], (BT, None))
|
||||
gV_tiles = cute.logical_divide(tmaV[None, head_id, None], (BT, None))
|
||||
# KDA: kg is per v-head [T, Hv, K], index by head_id (G=1 => same as k_head_id)
|
||||
gK_tiles = cute.logical_divide(
|
||||
cute.domain_offset((bos, 0), tmaK[None, head_id, None]),
|
||||
(BT, None),
|
||||
)
|
||||
|
||||
for chunk_id in range(num_chunks):
|
||||
mbar = tma_mbar + stage_id
|
||||
gW = gW_tiles[(None, chunk_offset + chunk_id), None]
|
||||
gV = gV_tiles[(None, chunk_offset + chunk_id), None]
|
||||
gK = gK_tiles[(None, chunk_id), None]
|
||||
|
||||
cute.arch.mbarrier_wait(vk_done_mbar + stage_id, parity)
|
||||
|
||||
with cute.arch.elect_one():
|
||||
STAGE_SIZE = BT * (K_dim + V_dim + K_dim) * 2
|
||||
cute.arch.mbarrier_arrive_and_expect_tx(mbar, STAGE_SIZE)
|
||||
simple_tma_copy(
|
||||
W_tma_atom, gW, sW[None, None, stage_id], mbar, EVICT_FIRST
|
||||
)
|
||||
simple_tma_copy(
|
||||
V_tma_atom, gV, sV[None, None, stage_id], mbar, EVICT_FIRST
|
||||
)
|
||||
simple_tma_copy(K_tma_atom, gK, sK[None, None, stage_id], mbar)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
elif warp_id == 8:
|
||||
# MMA warp -- IDENTICAL to GDN: sK now holds kg, so V_new.T@kg falls out.
|
||||
_tcgen05.alloc(taddr)
|
||||
stage_id = 0
|
||||
parity = 0
|
||||
|
||||
wh_idesc = _tcgen05.make_bf16_idesc(V_dim, BT, negate_A=True)
|
||||
vk_idesc = _tcgen05.make_bf16_idesc(V_dim, K_dim, transpose_B=True)
|
||||
sdesc_template = _tcgen05.make_sdesc_128B_swizzle(BT * 128)
|
||||
|
||||
if cutlass.const_expr(not is_f32):
|
||||
Haddr0 = sH0[None, None].iterator.toint()
|
||||
Waddr0 = sW[None, None, stage_id].iterator.toint()
|
||||
hdesc0_base = sdesc_template | (Haddr0 >> 4)
|
||||
wdesc0_base = sdesc_template | (Waddr0 >> 4)
|
||||
|
||||
cute.arch.mbarrier_wait(tma_mbar + stage_id, parity)
|
||||
cute.arch.mbarrier_wait(wh_in_mbar + stage_id, parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(K_dim // 64):
|
||||
for j in cutlass.range_constexpr(64 // 16):
|
||||
hdesc0 = hdesc0_base | ((i * V_dim * 128 + j * 32) >> 4)
|
||||
wdesc0 = wdesc0_base | ((i * BT * 128 + j * 32) >> 4)
|
||||
_tcgen05.mma_f16(wh_tmem, hdesc0, wdesc0, wh_idesc, True)
|
||||
_tcgen05.commit(wh_done_mbar + stage_id)
|
||||
|
||||
Kaddr0 = sK[None, None, stage_id].iterator.toint()
|
||||
kdesc0_base = sdesc_template | (Kaddr0 >> 4)
|
||||
|
||||
cute.arch.mbarrier_wait(vk_in_mbar + stage_id, parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
with cute.arch.elect_one():
|
||||
for k in cutlass.range_constexpr(BT // 16):
|
||||
vtmem0 = v_tmem_base + k * 8
|
||||
kdesc0 = kdesc0_base | ((k * 16 * 128) >> 4)
|
||||
_tcgen05.mma_ts_f16(vk_tmem, vtmem0, kdesc0, vk_idesc, True)
|
||||
_tcgen05.commit(vk_done_mbar + stage_id)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
num_iters = num_chunks - int(not is_f32)
|
||||
for _ in range(num_iters):
|
||||
Waddr = sW[None, None, stage_id].iterator.toint()
|
||||
wdesc_base = sdesc_template | (Waddr >> 4)
|
||||
|
||||
cute.arch.mbarrier_wait(tma_mbar + stage_id, parity)
|
||||
cute.arch.mbarrier_wait(wh_in_mbar + stage_id, parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(K_dim // 64):
|
||||
for j in cutlass.range_constexpr(64 // 16):
|
||||
htmem = h_tmem_base + i * 32 + j * 8
|
||||
wdesc = wdesc_base | ((i * BT * 128 + j * 32) >> 4)
|
||||
_tcgen05.mma_ts_f16(wh_tmem, htmem, wdesc, wh_idesc, True)
|
||||
_tcgen05.commit(wh_done_mbar + stage_id)
|
||||
|
||||
Kaddr = sK[None, None, stage_id].iterator.toint()
|
||||
kdesc_base = sdesc_template | (Kaddr >> 4)
|
||||
|
||||
cute.arch.mbarrier_wait(vk_in_mbar + stage_id, parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
with cute.arch.elect_one():
|
||||
for k in cutlass.range_constexpr(BT // 16):
|
||||
vtmem = v_tmem_base + k * 8
|
||||
kdesc = kdesc_base | ((k * 16 * 128) >> 4)
|
||||
_tcgen05.mma_ts_f16(vk_tmem, vtmem, kdesc, vk_idesc, True)
|
||||
_tcgen05.commit(vk_done_mbar + stage_id)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
elif warp_id >= 4:
|
||||
# H warps
|
||||
tid_ = tid % 128
|
||||
warp_id_ = warp_id % 4
|
||||
chunk_offset = chunk_offsets[seq_id]
|
||||
|
||||
stage_id = 0
|
||||
vk_stage_id = 0
|
||||
vk_parity = 0
|
||||
|
||||
op = cute.nvgpu.CopyUniversalOp()
|
||||
cp_16B = cute.make_copy_atom(op, Float32, num_bits_per_copy=128)
|
||||
|
||||
##### chunk_id = 0 #####
|
||||
if True:
|
||||
chunk_id = 0
|
||||
end_t = min(bos + (chunk_id + 1) * BT, eos)
|
||||
last_idx = end_t - 1
|
||||
|
||||
# KDA: load per-channel end-of-chunk decay into smem (all 128 k-cols)
|
||||
s_gl_exp[tid_] = cute.math.exp(
|
||||
g_cu[last_idx, head_id, tid_], fastmath=True
|
||||
)
|
||||
|
||||
if warp_id_ == 0:
|
||||
cute.arch.mbarrier_wait(h0_mbar, 0)
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
|
||||
if cutlass.const_expr(is_f32):
|
||||
for i in cutlass.range_constexpr(K_dim // 32):
|
||||
h_f32 = cute.make_rmem_tensor(32, Float32)
|
||||
cute.copy(cp_16B, sH0[tid_, (None, i)], h_f32)
|
||||
|
||||
h_bf16 = cute.make_rmem_tensor(32, BFloat16)
|
||||
h_bf16.store(h_f32.load().to(BFloat16))
|
||||
_tcgen05.st(
|
||||
warp_id_ * 32, h_tmem_base + i * 16, "32x32b", 16, h_bf16
|
||||
)
|
||||
|
||||
dst = cute.local_tile(sH[tid_, None], (32,), (i,))
|
||||
cute.copy(cp_16B, h_bf16, dst)
|
||||
|
||||
_tcgen05.wait_st()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(wh_in_mbar + stage_id)
|
||||
|
||||
# scale H for 2nd MMA -- KDA: per-column decay s_gl_exp[k]
|
||||
for i in cutlass.range_constexpr(K_dim // 32):
|
||||
h_f32 = cute.make_rmem_tensor(32, Float32)
|
||||
|
||||
if cutlass.const_expr(is_f32):
|
||||
cute.copy(cp_16B, sH0[tid_, (None, i)], h_f32)
|
||||
else:
|
||||
h_bf16 = cute.make_rmem_tensor(32, BFloat16)
|
||||
sH_src = cute.local_tile(sH0[tid_, None], (32,), (i,))
|
||||
cute.copy(cp_16B, sH_src, h_bf16)
|
||||
h_f32.store(
|
||||
cvt.bf16x2_to_fp32x2(
|
||||
cute.recast_tensor(h_bf16, Uint32)
|
||||
).load()
|
||||
)
|
||||
|
||||
for j in cutlass.range_constexpr(32):
|
||||
h_f32[j] *= s_gl_exp[i * 32 + j]
|
||||
_tcgen05.st(warp_id_ * 32, vk_tmem + i * 32, "32x32b", 32, h_f32)
|
||||
|
||||
_tcgen05.wait_st()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(vk_in_mbar + stage_id)
|
||||
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
fence_before_tma_store()
|
||||
if warp_id_ == 3:
|
||||
h_src = sH if cutlass.const_expr(is_f32) else sH0
|
||||
h_dst = tmaH[chunk_offset + chunk_id, head_id, None, None]
|
||||
simple_tma_copy(H_tma_atom, h_src, h_dst)
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_commit_group()
|
||||
if cutlass.const_expr(not is_f32):
|
||||
cute.arch.cp_async_bulk_wait_group(0, read=True)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
|
||||
##### subsequent chunks #####
|
||||
for chunk_id in range(1, num_chunks):
|
||||
end_t = min(bos + (chunk_id + 1) * BT, eos)
|
||||
last_idx = end_t - 1
|
||||
|
||||
# KDA: refresh per-channel end-of-chunk decay for this chunk
|
||||
s_gl_exp[tid_] = cute.math.exp(
|
||||
g_cu[last_idx, head_id, tid_], fastmath=True
|
||||
)
|
||||
|
||||
if warp_id_ == 0:
|
||||
cute.arch.mbarrier_wait(vk_done_mbar + vk_stage_id, vk_parity)
|
||||
vk_stage_id = (vk_stage_id + 1) % num_stages
|
||||
if vk_stage_id == 0:
|
||||
vk_parity ^= 1
|
||||
elif warp_id_ == 3:
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_wait_group(0, read=True)
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
for i in cutlass.range_constexpr(K_dim // 32):
|
||||
h_f32 = _tcgen05.ld(warp_id_ * 32, vk_tmem + i * 32, "32x32b", 32)
|
||||
h_bf16 = cute.make_rmem_tensor(32, BFloat16)
|
||||
h_bf16.store(h_f32.to(BFloat16))
|
||||
_tcgen05.st(
|
||||
warp_id_ * 32, h_tmem_base + i * 16, "32x32b", 16, h_bf16
|
||||
)
|
||||
dst = cute.local_tile(sH[tid_, None], (32,), (i,))
|
||||
cute.copy(cp_16B, h_bf16, dst)
|
||||
|
||||
_tcgen05.wait_st()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(wh_in_mbar + stage_id)
|
||||
|
||||
# scale H for 2nd MMA -- KDA: per-column decay
|
||||
for i in cutlass.range_constexpr(K_dim // 32):
|
||||
h_f32 = cute.make_rmem_tensor(32, Float32)
|
||||
h_f32.store(
|
||||
_tcgen05.ld(warp_id_ * 32, vk_tmem + i * 32, "32x32b", 32)
|
||||
)
|
||||
for j in cutlass.range_constexpr(32):
|
||||
h_f32[j] *= s_gl_exp[i * 32 + j]
|
||||
_tcgen05.st(warp_id_ * 32, vk_tmem + i * 32, "32x32b", 32, h_f32)
|
||||
_tcgen05.wait_st()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(vk_in_mbar + stage_id)
|
||||
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
fence_before_tma_store()
|
||||
if warp_id_ == 3:
|
||||
h_dst = tmaH[chunk_offset + chunk_id, head_id, None, None]
|
||||
simple_tma_copy(H_tma_atom, sH, h_dst)
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_commit_group()
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
|
||||
# handle final state. reuse H0 smem.
|
||||
if warp_id_ == 0:
|
||||
cute.arch.mbarrier_wait(vk_done_mbar + vk_stage_id, vk_parity)
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
for i in cutlass.range_constexpr(K_dim // 32):
|
||||
h_f32 = cute.make_rmem_tensor(32, Float32)
|
||||
h_f32.store(_tcgen05.ld(warp_id_ * 32, vk_tmem + i * 32, "32x32b", 32))
|
||||
|
||||
if cutlass.const_expr(is_f32):
|
||||
cute.copy(cp_16B, h_f32, sH0[tid_, (None, i)])
|
||||
else:
|
||||
h_bf16 = cute.make_rmem_tensor(32, BFloat16)
|
||||
h_bf16.store(h_f32.load().to(BFloat16))
|
||||
sH0_dst = cute.local_tile(sH0[tid_, None], (32,), (i,))
|
||||
cute.copy(cp_16B, h_bf16, sH0_dst)
|
||||
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
|
||||
if warp_id_ == 0:
|
||||
ht_dst = tmaHT[seq_id, head_id, None, None]
|
||||
simple_tma_copy(HT_tma_atom, sH0, ht_dst)
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_commit_group()
|
||||
if warp_id_ == 1:
|
||||
_tcgen05.dealloc()
|
||||
|
||||
else:
|
||||
# V warps -- KDA: v_new is NOT gate-scaled; store RAW to both tmem & gmem.
|
||||
stage_id = 0
|
||||
parity = 0
|
||||
|
||||
chunk_offset = chunk_offsets[seq_id]
|
||||
|
||||
ldsm_trans_op = warp.LdMatrix8x8x16bOp(num_matrices=4, transpose=True)
|
||||
stsm_trans_op = warp.StMatrix8x8x16bOp(num_matrices=4, transpose=True)
|
||||
ldsm_trans_atom = cute.make_copy_atom(ldsm_trans_op, BFloat16)
|
||||
stsm_trans_atom = cute.make_copy_atom(stsm_trans_op, BFloat16)
|
||||
|
||||
gV_new_tiles = cute.logical_divide(
|
||||
tmaV_new[None, head_id, None], (BT, None)
|
||||
)
|
||||
|
||||
sV_view = cute.logical_divide(sV, (None, 8, None))
|
||||
sV_new_view = cute.logical_divide(sV_new, (None, 8))
|
||||
|
||||
s_col = warp_id * 4 + (lane_id // 8)
|
||||
sV_view = sV_view[None, (None, s_col), None]
|
||||
sV_new_view = sV_new_view[None, (None, s_col)]
|
||||
|
||||
for chunk_id in range(num_chunks):
|
||||
if warp_id == 0:
|
||||
cute.arch.mbarrier_wait(tma_mbar + stage_id, parity)
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
|
||||
# unpack U (sV) BF16->FP32, store to tmem to init the 1st MMA acc
|
||||
for i in cutlass.range_constexpr(BT // 8):
|
||||
s_row = i * 8 + (lane_id % 8)
|
||||
v_bf16 = cute.make_rmem_tensor(8, BFloat16)
|
||||
cute.copy(ldsm_trans_atom, sV_view[s_row, None, stage_id], v_bf16)
|
||||
v_fp32 = cvt.bf16x2_to_fp32x2(cute.recast_tensor(v_bf16, Uint32))
|
||||
v_fp32 = cute.logical_divide(v_fp32, 4)
|
||||
|
||||
tcol = wh_tmem + i * 8
|
||||
_tcgen05.st(warp_id * 32 + 0, tcol, "16x256b", 1, v_fp32[None, 0])
|
||||
_tcgen05.st(warp_id * 32 + 16, tcol, "16x256b", 1, v_fp32[None, 1])
|
||||
|
||||
_tcgen05.wait_st()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(wh_in_mbar + stage_id)
|
||||
|
||||
# wait for 1st MMA (V_new.T) to finish
|
||||
if warp_id == 2:
|
||||
cute.arch.mbarrier_wait(wh_done_mbar + stage_id, parity)
|
||||
elif warp_id == 3:
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_wait_group(0, read=True)
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
for i in cutlass.range_constexpr(BT // 8):
|
||||
v_new = cute.make_rmem_tensor((4, 2), Float32)
|
||||
tcol = wh_tmem + i * 8
|
||||
v_new[None, 0].store(
|
||||
_tcgen05.ld(warp_id * 32 + 0, tcol, "16x256b", 1)
|
||||
)
|
||||
v_new[None, 1].store(
|
||||
_tcgen05.ld(warp_id * 32 + 16, tcol, "16x256b", 1)
|
||||
)
|
||||
v_new_bf16 = cute.make_rmem_tensor(8, BFloat16)
|
||||
v_new_bf16.store(v_new.load().to(BFloat16))
|
||||
|
||||
# KDA: NO per-token scaling. v_new (raw) goes to BOTH gmem and tmem.
|
||||
s_row = i * 8 + (lane_id % 8)
|
||||
cute.copy(stsm_trans_atom, v_new_bf16, sV_new_view[s_row, None])
|
||||
|
||||
v_new_bf16_42 = v_new.load().to(BFloat16).reshape((4, 2))
|
||||
tcol = v_tmem_base + i * 4
|
||||
_tcgen05.st(
|
||||
warp_id * 32 + 0, tcol, "16x128b", 1, v_new_bf16_42[None, 0]
|
||||
)
|
||||
_tcgen05.st(
|
||||
warp_id * 32 + 16, tcol, "16x128b", 1, v_new_bf16_42[None, 1]
|
||||
)
|
||||
_tcgen05.wait_st()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(vk_in_mbar + stage_id)
|
||||
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
fence_before_tma_store()
|
||||
if warp_id == 3:
|
||||
gV = gV_new_tiles[(None, chunk_offset + chunk_id), None]
|
||||
simple_tma_copy(V_new_tma_atom, sV_new, gV)
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_commit_group()
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
@cache
|
||||
@staticmethod
|
||||
def compile(
|
||||
H: int,
|
||||
Hv: int,
|
||||
K_dim: int,
|
||||
V_dim: int,
|
||||
h_dtype: cutlass.Numeric = Float32,
|
||||
BT: int = 64,
|
||||
num_stages: int = 2,
|
||||
):
|
||||
total_t = cute.sym_int()
|
||||
pad_t = cute.sym_int()
|
||||
total_chunks_n = cute.sym_int()
|
||||
num_sequences = cute.sym_int()
|
||||
cu_entries = cute.sym_int()
|
||||
|
||||
K = make_fake_tensor(BFloat16, (total_t, Hv, K_dim), divisibility=16)
|
||||
V = make_fake_tensor(BFloat16, (pad_t, Hv, V_dim), divisibility=16)
|
||||
W = make_fake_tensor(BFloat16, (pad_t, Hv, K_dim), divisibility=16)
|
||||
V_new = make_fake_tensor(BFloat16, (pad_t, Hv, V_dim), divisibility=16)
|
||||
g_cu = make_fake_tensor(Float32, (total_t, Hv, K_dim), divisibility=4)
|
||||
h = make_fake_tensor(
|
||||
BFloat16, (total_chunks_n, Hv, V_dim, K_dim), divisibility=16
|
||||
)
|
||||
h0 = make_fake_tensor(
|
||||
h_dtype, (num_sequences, Hv, V_dim, K_dim), divisibility=16
|
||||
)
|
||||
ht = make_fake_tensor(
|
||||
h_dtype, (num_sequences, Hv, V_dim, K_dim), divisibility=16
|
||||
)
|
||||
cu_seqlens = make_fake_tensor(Int32, (cu_entries,), divisibility=1)
|
||||
chunk_offsets = make_fake_tensor(Int32, (cu_entries,), divisibility=1)
|
||||
|
||||
kernel = Sm100KdaChunkHKernel(H, Hv, K_dim, V_dim, h_dtype, BT, num_stages)
|
||||
stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
|
||||
return cute.compile(
|
||||
kernel,
|
||||
K,
|
||||
V,
|
||||
W,
|
||||
V_new,
|
||||
g_cu,
|
||||
h,
|
||||
h0,
|
||||
ht,
|
||||
cu_seqlens,
|
||||
chunk_offsets,
|
||||
stream,
|
||||
options="--enable-tvm-ffi",
|
||||
)
|
||||
|
||||
|
||||
def kda_h_cutedsl(
|
||||
kg: torch.Tensor,
|
||||
V: torch.Tensor,
|
||||
W: torch.Tensor,
|
||||
V_new: torch.Tensor,
|
||||
g_cu: torch.Tensor,
|
||||
h: torch.Tensor,
|
||||
h0: torch.Tensor,
|
||||
ht: torch.Tensor,
|
||||
cu_seqlens: torch.Tensor,
|
||||
chunk_offsets: torch.Tensor,
|
||||
BT: int = 64,
|
||||
num_stages: int = 2,
|
||||
) -> None:
|
||||
"""KDA chunk-state kernel. `kg` = per-channel pre-scaled key [T, Hv, K]."""
|
||||
_, Hv, K_dim = kg.shape
|
||||
_, _, V_dim = V.shape
|
||||
h_dtype = {torch.bfloat16: BFloat16, torch.float32: Float32}[h0.dtype]
|
||||
Sm100KdaChunkHKernel.compile(Hv, Hv, K_dim, V_dim, h_dtype, BT, num_stages)(
|
||||
kg, V, W, V_new, g_cu, h, h0, ht, cu_seqlens, chunk_offsets
|
||||
)
|
||||
@@ -0,0 +1,741 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# KDA (Kimi Delta Attention) SM100 KKT-inverse + U/W kernel.
|
||||
#
|
||||
# Adapted from gdn_blackwell/kernel_kkt_inv_uw.py. KDA's decay is PER-CHANNEL, so
|
||||
# (as with kernel_h/o) the gate is folded OUTSIDE this kernel into pre-scaled keys:
|
||||
#
|
||||
# kL [c,d] = k[c,d] * exp(g_cu[c,d] - g_cu_last[d]) (KKT left operand)
|
||||
# kR [j,d] = k[j,d] * exp(g_cu_last[d] - g_cu[j,d]) (KKT right operand, bounded)
|
||||
# kg [j,d] = k[j,d] * exp(g_cu[j,d]) (W operand, bounded)
|
||||
#
|
||||
# Then KKT[c,j] = sum_d kL[c,d]*kR[j,d] = sum_d k[c,d]*k[j,d]*exp(g_cu[c,d]-g_cu[j,d])
|
||||
# carries the per-channel decay, so:
|
||||
# A = strictLower(beta * KKT) (NO post-MMA Gamma; decay already inside)
|
||||
# Ai = inverse(I + A) (Newton-Schulz, gate-independent -> verbatim)
|
||||
# U = (Ai * beta) @ V
|
||||
# W = (Ai * beta) @ kg (NO Abg; the exp(g_cu) lives in kg)
|
||||
#
|
||||
# Net: this kernel has NO cumsum and NO g_cu — only beta survives, exactly like GDN.
|
||||
from functools import cache
|
||||
|
||||
import cutlass
|
||||
import torch
|
||||
from cuda.bindings.driver import CUstream
|
||||
from cutlass import BFloat16, Float32, Int32, Int64, Uint32, cute
|
||||
from cutlass.cute.nvgpu import cpasync, warp
|
||||
from quack.compile_utils import make_fake_tensor
|
||||
|
||||
from sglang.srt.layers.attention.cute_utils import (
|
||||
EVICT_FIRST,
|
||||
_tcgen05,
|
||||
cvt,
|
||||
fence_before_tma_store,
|
||||
mma_bf16,
|
||||
simple_tma_copy,
|
||||
)
|
||||
|
||||
|
||||
class Sm100KdaChunkUWKernel:
|
||||
"""KDA per-chunk KKT-inverse + U/W (see module docstring)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
H: int,
|
||||
Hv: int,
|
||||
K_dim: int,
|
||||
V_dim: int,
|
||||
num_stages: int = 2,
|
||||
) -> None:
|
||||
assert Hv % H == 0
|
||||
assert K_dim == V_dim == 128
|
||||
self.H = H
|
||||
self.Hv = Hv
|
||||
self.K_dim = K_dim
|
||||
self.V_dim = V_dim
|
||||
self.num_stages = num_stages
|
||||
|
||||
self.BT = 64
|
||||
self.num_warps = 2 + 4 + 4
|
||||
|
||||
@cute.jit
|
||||
def _make_tma_args(
|
||||
self,
|
||||
tensor: cute.Tensor,
|
||||
dim: cutlass.Constexpr[int],
|
||||
num_stages: int,
|
||||
op: cpasync.TmaCopyOp,
|
||||
):
|
||||
swizzle_128B = cute.make_swizzle(3, 4, 3)
|
||||
slayout = cute.make_layout(
|
||||
(self.BT, 1, (64, dim // 64), num_stages),
|
||||
stride=(64, 0, (1, self.BT * 64), self.BT * dim),
|
||||
)
|
||||
slayout = cute.make_composed_layout(swizzle_128B, 0, slayout)
|
||||
atom, tma_tensor = cpasync.make_tiled_tma_atom(
|
||||
op,
|
||||
cute.logical_divide(tensor, (None, None, 64)),
|
||||
slayout,
|
||||
cta_tiler=(self.BT, 1, dim),
|
||||
)
|
||||
return atom, tma_tensor, slayout
|
||||
|
||||
@cute.jit
|
||||
def __call__(
|
||||
self,
|
||||
KL: cute.Tensor, # k*exp(g_cu - g_cu_last) [T, Hv, K]
|
||||
KR: cute.Tensor, # k*exp(g_cu_last - g_cu) [T, Hv, K]
|
||||
KG: cute.Tensor, # k*exp(g_cu) [T, Hv, K]
|
||||
V: cute.Tensor,
|
||||
U: cute.Tensor,
|
||||
W: cute.Tensor,
|
||||
beta: cute.Tensor,
|
||||
cu_seqlens: cute.Tensor,
|
||||
chunk_indices: cute.Tensor,
|
||||
total_chunks: cute.Tensor,
|
||||
num_sms: Int32,
|
||||
stream: CUstream,
|
||||
):
|
||||
tma_g2s = cpasync.CopyBulkTensorTileG2SOp()
|
||||
tma_s2g = cpasync.CopyBulkTensorTileS2GOp()
|
||||
|
||||
KL_args = self._make_tma_args(KL, self.K_dim, self.num_stages, tma_g2s)
|
||||
KR_args = self._make_tma_args(KR, self.K_dim, self.num_stages, tma_g2s)
|
||||
KG_args = self._make_tma_args(KG, self.K_dim, self.num_stages, tma_g2s)
|
||||
V_args = self._make_tma_args(V, self.V_dim, self.num_stages, tma_g2s)
|
||||
U_args = self._make_tma_args(U, self.V_dim, 1, tma_s2g)
|
||||
W_args = self._make_tma_args(W, self.K_dim, 1, tma_s2g)
|
||||
|
||||
grid = (num_sms // self.Hv, self.Hv, 1)
|
||||
block = (self.num_warps * 32, 1, 1)
|
||||
self.kernel(
|
||||
KL_args,
|
||||
KR_args,
|
||||
KG_args,
|
||||
V_args,
|
||||
U_args,
|
||||
W_args,
|
||||
beta,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
total_chunks,
|
||||
).launch(grid=grid, block=block, stream=stream)
|
||||
|
||||
@cute.kernel
|
||||
def kernel(
|
||||
self,
|
||||
KL_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
KR_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
KG_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
V_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
U_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
W_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
beta: cute.Tensor,
|
||||
cu_seqlens: cute.Tensor,
|
||||
chunk_indices: cute.Tensor,
|
||||
total_chunks: cute.Tensor,
|
||||
):
|
||||
tid, _, _ = cute.arch.thread_idx()
|
||||
bid, head_id, _ = cute.arch.block_idx()
|
||||
grid_x, _, _ = cute.arch.grid_dim()
|
||||
|
||||
warp_id = cute.arch.make_warp_uniform(tid // 32)
|
||||
lane_id = tid % 32
|
||||
|
||||
BT = self.BT
|
||||
K_dim = self.K_dim
|
||||
V_dim = self.V_dim
|
||||
num_stages = self.num_stages
|
||||
|
||||
KL_tma_atom, tmaKL, sKL_layout = KL_args
|
||||
KR_tma_atom, tmaKR, sKR_layout = KR_args
|
||||
KG_tma_atom, tmaKG, sKG_layout = KG_args
|
||||
V_tma_atom, tmaV, sV_layout = V_args
|
||||
U_tma_atom, tmaU, sU_layout = U_args
|
||||
W_tma_atom, tmaW, sW_layout = W_args
|
||||
|
||||
def allocate_tensor(smem, dtype, layout):
|
||||
return smem.allocate_tensor(
|
||||
dtype, layout.outer, byte_alignment=128, swizzle=layout.inner
|
||||
)
|
||||
|
||||
smem = cutlass.utils.SmemAllocator()
|
||||
sKL = allocate_tensor(smem, BFloat16, sKL_layout)[None, 0, None, None]
|
||||
sKR = allocate_tensor(smem, BFloat16, sKR_layout)[None, 0, None, None]
|
||||
sKG = allocate_tensor(smem, BFloat16, sKG_layout)[None, 0, None, None]
|
||||
sV = allocate_tensor(smem, BFloat16, sV_layout)[None, 0, None, None]
|
||||
sU = allocate_tensor(smem, BFloat16, sU_layout)[None, 0, None, 0]
|
||||
sW = allocate_tensor(smem, BFloat16, sW_layout)[None, 0, None, 0]
|
||||
|
||||
swizzle_128B = cute.make_swizzle(3, 4, 3)
|
||||
sA_layout = cute.make_layout((BT, (64, 1)), stride=(64, (1, BT * 64)))
|
||||
sA_layout = cute.make_composed_layout(swizzle_128B, 0, sA_layout)
|
||||
sA = allocate_tensor(smem, BFloat16, sA_layout)
|
||||
sAi = allocate_tensor(smem, BFloat16, sA_layout)
|
||||
|
||||
s_beta = smem.allocate_array(Float32, BT)
|
||||
|
||||
tma_mbar = smem.allocate_array(Int64, num_stages)
|
||||
mma_kkt_mbar = smem.allocate_array(Int64, num_stages)
|
||||
inv_mbar = smem.allocate_array(Int64, num_stages)
|
||||
mma_u_mbar = smem.allocate_array(Int64, num_stages)
|
||||
mma_w_mbar = smem.allocate_array(Int64, num_stages)
|
||||
epi_mbar = smem.allocate_array(Int64, num_stages)
|
||||
taddr = smem.allocate(Int32, 4)
|
||||
|
||||
kkt_tmem = 0
|
||||
U_tmem_base = kkt_tmem + BT
|
||||
Ab_tmem_base = U_tmem_base + V_dim * num_stages
|
||||
assert Ab_tmem_base + (BT // 2) * num_stages <= 512
|
||||
|
||||
ldsm_op = warp.LdMatrix8x8x16bOp(num_matrices=4)
|
||||
stsm_op = warp.StMatrix8x8x16bOp(num_matrices=4)
|
||||
ldsm_trans_op = warp.LdMatrix8x8x16bOp(num_matrices=4, transpose=True)
|
||||
ldsm_atom = cute.make_copy_atom(ldsm_op, BFloat16)
|
||||
stsm_atom = cute.make_copy_atom(stsm_op, BFloat16)
|
||||
ldsm_trans_atom = cute.make_copy_atom(ldsm_trans_op, BFloat16)
|
||||
|
||||
if warp_id == 0:
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(num_stages):
|
||||
cute.arch.mbarrier_init(tma_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(mma_kkt_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(inv_mbar + i, 128)
|
||||
cute.arch.mbarrier_init(mma_u_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(mma_w_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(epi_mbar + i, 128)
|
||||
cute.arch.mbarrier_init_fence()
|
||||
elif warp_id == 1:
|
||||
cpasync.prefetch_descriptor(KL_tma_atom)
|
||||
cpasync.prefetch_descriptor(KR_tma_atom)
|
||||
cpasync.prefetch_descriptor(KG_tma_atom)
|
||||
cpasync.prefetch_descriptor(V_tma_atom)
|
||||
cpasync.prefetch_descriptor(U_tma_atom)
|
||||
cpasync.prefetch_descriptor(W_tma_atom)
|
||||
cute.arch.sync_threads()
|
||||
|
||||
num_global_chunks = total_chunks[0]
|
||||
if warp_id == 9:
|
||||
# TMA warp
|
||||
stage_id = 0
|
||||
parity = 1
|
||||
|
||||
for global_chunk_id in range(bid, num_global_chunks, grid_x):
|
||||
seq_id = chunk_indices[global_chunk_id, 0]
|
||||
chunk_id = chunk_indices[global_chunk_id, 1]
|
||||
bos = cu_seqlens[seq_id]
|
||||
|
||||
mbar = tma_mbar + stage_id
|
||||
# KDA: all keys are per v-head [T, Hv, K], index by head_id.
|
||||
gKL = cute.local_tile(
|
||||
cute.domain_offset((bos, 0), tmaKL[None, head_id, None]),
|
||||
tiler=(BT, K_dim),
|
||||
coord=(chunk_id, 0),
|
||||
)
|
||||
gKR = cute.local_tile(
|
||||
cute.domain_offset((bos, 0), tmaKR[None, head_id, None]),
|
||||
tiler=(BT, K_dim),
|
||||
coord=(chunk_id, 0),
|
||||
)
|
||||
gKG = cute.local_tile(
|
||||
cute.domain_offset((bos, 0), tmaKG[None, head_id, None]),
|
||||
tiler=(BT, K_dim),
|
||||
coord=(chunk_id, 0),
|
||||
)
|
||||
gV = cute.local_tile(
|
||||
cute.domain_offset((bos, 0), tmaV[None, head_id, None]),
|
||||
tiler=(BT, V_dim),
|
||||
coord=(chunk_id, 0),
|
||||
)
|
||||
|
||||
cute.arch.mbarrier_wait(mma_u_mbar + stage_id, parity)
|
||||
|
||||
with cute.arch.elect_one():
|
||||
STAGE_SIZE = BT * (K_dim + K_dim + K_dim + V_dim) * 2
|
||||
cute.arch.mbarrier_arrive_and_expect_tx(mbar, STAGE_SIZE)
|
||||
simple_tma_copy(KL_tma_atom, gKL, sKL[None, None, stage_id], mbar)
|
||||
simple_tma_copy(KR_tma_atom, gKR, sKR[None, None, stage_id], mbar)
|
||||
simple_tma_copy(
|
||||
KG_tma_atom, gKG, sKG[None, None, stage_id], mbar, EVICT_FIRST
|
||||
)
|
||||
simple_tma_copy(
|
||||
V_tma_atom, gV, sV[None, None, stage_id], mbar, EVICT_FIRST
|
||||
)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
elif warp_id == 8:
|
||||
# MMA warp
|
||||
_tcgen05.alloc(taddr)
|
||||
|
||||
stage_id = 0
|
||||
parity = 0
|
||||
|
||||
kkt_idesc = _tcgen05.make_bf16_idesc(BT, BT)
|
||||
u_idesc = _tcgen05.make_bf16_idesc(BT, V_dim, transpose_B=True)
|
||||
w_idesc = _tcgen05.make_bf16_idesc(BT, K_dim, transpose_B=True)
|
||||
|
||||
sdesc_template = _tcgen05.make_sdesc_128B_swizzle(BT * 128)
|
||||
|
||||
for global_chunk_id in range(bid, num_global_chunks, grid_x):
|
||||
U_tmem = U_tmem_base + V_dim * stage_id
|
||||
W_tmem = U_tmem | (16 << 16)
|
||||
Ab_tmem = Ab_tmem_base + (BT // 2) * stage_id
|
||||
Abg_tmem = Ab_tmem | (16 << 16)
|
||||
|
||||
##### KKT MMA: KKT = kL @ kR.T #####
|
||||
klraddr = sKL[None, None, stage_id].iterator.toint()
|
||||
krraddr = sKR[None, None, stage_id].iterator.toint()
|
||||
kldesc_base = sdesc_template | (klraddr >> 4)
|
||||
krdesc_base = sdesc_template | (krraddr >> 4)
|
||||
|
||||
cute.arch.mbarrier_wait(tma_mbar + stage_id, parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(K_dim // 64):
|
||||
for j in cutlass.range_constexpr(64 // 16):
|
||||
off = (i * BT * 128 + j * 32) >> 4
|
||||
_tcgen05.mma_f16(
|
||||
kkt_tmem,
|
||||
kldesc_base | off,
|
||||
krdesc_base | off,
|
||||
kkt_idesc,
|
||||
(i > 0) or (j > 0),
|
||||
)
|
||||
_tcgen05.commit(mma_kkt_mbar + stage_id)
|
||||
|
||||
##### U/W MMA: U = Ab @ V, W = Ab @ kg #####
|
||||
vaddr = sV[None, None, stage_id].iterator.toint()
|
||||
kgaddr = sKG[None, None, stage_id].iterator.toint()
|
||||
vdesc = sdesc_template | (vaddr >> 4)
|
||||
kgdesc = sdesc_template | (kgaddr >> 4)
|
||||
|
||||
cute.arch.mbarrier_wait(epi_mbar + stage_id, parity ^ 1)
|
||||
cute.arch.mbarrier_wait(inv_mbar + stage_id, parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(BT // 16):
|
||||
_tcgen05.mma_ts_f16(
|
||||
W_tmem, Abg_tmem + i * 8, kgdesc, w_idesc, i > 0
|
||||
)
|
||||
kgdesc += (16 * 128) >> 4
|
||||
_tcgen05.commit(mma_w_mbar + stage_id)
|
||||
|
||||
for i in cutlass.range_constexpr(BT // 16):
|
||||
_tcgen05.mma_ts_f16(
|
||||
U_tmem, Ab_tmem + i * 8, vdesc, u_idesc, i > 0
|
||||
)
|
||||
vdesc += (16 * 128) >> 4
|
||||
_tcgen05.commit(mma_u_mbar + stage_id)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
cute.arch.mbarrier_wait(epi_mbar + stage_id, parity ^ 1)
|
||||
_tcgen05.dealloc()
|
||||
|
||||
elif warp_id >= 4:
|
||||
# inv warps
|
||||
tid_ = tid % 128
|
||||
warp_id_ = warp_id % 4
|
||||
|
||||
stage_id = 0
|
||||
parity = 0
|
||||
|
||||
sA_ldsm = cute.logical_divide(sA, (16, cute.make_layout((8, 2))))
|
||||
sAi_ldsm = cute.logical_divide(sAi, (16, cute.make_layout((8, 2))))
|
||||
sA_ldsm = sA_ldsm[(lane_id % 16, None), ((None, lane_id // 16), None)]
|
||||
sAi_ldsm = sAi_ldsm[(lane_id % 16, None), ((None, lane_id // 16), None)]
|
||||
|
||||
for i in cutlass.range_constexpr((BT // 4 * 3) * BT // 128):
|
||||
idx = i * 128 + tid_
|
||||
sAi[idx // BT, idx % BT] = BFloat16(0.0)
|
||||
|
||||
row_indices = cute.make_rmem_tensor((1, 2, 1), Int32)
|
||||
row_indices[0, 0, 0] = warp_id_ * 16 + (lane_id // 4)
|
||||
row_indices[0, 1, 0] = warp_id_ * 16 + (lane_id // 4) + 8
|
||||
row_indices = row_indices.load()
|
||||
|
||||
col_indices = cute.make_rmem_tensor((2, 1, 2), Int32)
|
||||
col_indices[0, 0, 0] = (lane_id % 4) * 2 + 0
|
||||
col_indices[1, 0, 0] = (lane_id % 4) * 2 + 1
|
||||
col_indices[0, 0, 1] = (lane_id % 4) * 2 + 8
|
||||
col_indices[1, 0, 1] = (lane_id % 4) * 2 + 9
|
||||
col_indices = col_indices.load()
|
||||
|
||||
for global_chunk_id in range(bid, num_global_chunks, grid_x):
|
||||
seq_id = chunk_indices[global_chunk_id, 0]
|
||||
chunk_id = chunk_indices[global_chunk_id, 1]
|
||||
bos = cu_seqlens[seq_id]
|
||||
eos = cu_seqlens[seq_id + 1]
|
||||
off_t = bos + chunk_id * BT
|
||||
|
||||
t = off_t + tid_
|
||||
|
||||
##### Phase 1: load beta (KDA: no cumsum) #####
|
||||
if tid_ < BT:
|
||||
in_bounds = t < eos
|
||||
beta_val = beta[t, head_id] if in_bounds else Float32(0.0)
|
||||
s_beta[tid_] = beta_val
|
||||
|
||||
##### Phase 2: A = strictLower(beta * kkt) #####
|
||||
if warp_id_ == 0:
|
||||
cute.arch.mbarrier_wait(mma_kkt_mbar + stage_id, parity)
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
row_coord = (lane_id // 4, None, warp_id_)
|
||||
s_beta_view = cute.make_tensor(s_beta, (8, 2, 4))
|
||||
beta_row = s_beta_view[row_coord].load().reshape((1, 2, 1))
|
||||
|
||||
kkt = _tcgen05.ld(kkt_tmem, 0, "16x256b", BT // 8)
|
||||
kkt = kkt.reshape((2, 2, 2, BT // 16))
|
||||
|
||||
for i in cutlass.range_constexpr(BT // 16):
|
||||
# KDA: decay is already inside KKT; only beta + mask here.
|
||||
A = kkt[None, None, None, i] * beta_row
|
||||
|
||||
A_masked = cute.where(row_indices > col_indices + i * 16, A, 0.0)
|
||||
|
||||
packed = cute.make_rmem_tensor(4, Uint32)
|
||||
packed[0] = cvt.fp32x2_to_bf16x2(
|
||||
A_masked[0, 0, 0], A_masked[1, 0, 0]
|
||||
)
|
||||
packed[1] = cvt.fp32x2_to_bf16x2(
|
||||
A_masked[0, 1, 0], A_masked[1, 1, 0]
|
||||
)
|
||||
packed[2] = cvt.fp32x2_to_bf16x2(
|
||||
A_masked[0, 0, 1], A_masked[1, 0, 1]
|
||||
)
|
||||
packed[3] = cvt.fp32x2_to_bf16x2(
|
||||
A_masked[0, 1, 1], A_masked[1, 1, 1]
|
||||
)
|
||||
|
||||
cute.copy(
|
||||
stsm_atom,
|
||||
cute.recast_tensor(packed, BFloat16),
|
||||
sA_ldsm[warp_id_, None, i],
|
||||
)
|
||||
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
|
||||
##### Phase 3: matrix inverse (VERBATIM from GDN) #####
|
||||
zeros_f32 = cute.make_rmem_tensor(4, Float32)
|
||||
zeros_f32.fill(0.0)
|
||||
|
||||
def set_diagonal(A: cute.Tensor, lane_id: Int32):
|
||||
"Set the diagonal to 1s"
|
||||
if lane_id % 9 == 0:
|
||||
A[0] = (A[0] & Uint32(0xFFFF0000)) | Uint32(0x00003F80)
|
||||
A[3] = (A[3] & Uint32(0xFFFF0000)) | Uint32(0x00003F80)
|
||||
elif lane_id % 9 == 4:
|
||||
A[0] = (A[0] & Uint32(0x0000FFFF)) | Uint32(0x3F800000)
|
||||
A[3] = (A[3] & Uint32(0x0000FFFF)) | Uint32(0x3F800000)
|
||||
|
||||
Ai_bf16 = cute.make_rmem_tensor(8, BFloat16)
|
||||
mma_B_bf16 = cute.make_rmem_tensor(8, BFloat16)
|
||||
M_bf16 = cute.make_rmem_tensor(8, BFloat16)
|
||||
acc = cute.make_rmem_tensor((4, 2), Float32)
|
||||
|
||||
Ai = cute.recast_tensor(Ai_bf16, Uint32)
|
||||
mma_B = cute.logical_divide(cute.recast_tensor(mma_B_bf16, Uint32), 2)
|
||||
M = cute.logical_divide(cute.recast_tensor(M_bf16, Uint32), 2)
|
||||
|
||||
cute.copy(ldsm_atom, sA_ldsm[warp_id_, None, warp_id_], Ai_bf16)
|
||||
for i in cutlass.range_constexpr(4):
|
||||
Ai[i] ^= Uint32(0x80008000)
|
||||
set_diagonal(Ai, lane_id)
|
||||
|
||||
Ai_f32 = cute.logical_divide(cvt.bf16x2_to_fp32x2(Ai), 4)
|
||||
|
||||
cute.copy(ldsm_trans_atom, sA_ldsm[warp_id_, None, warp_id_], M_bf16)
|
||||
set_diagonal(M, lane_id)
|
||||
for i in cutlass.range_constexpr(4):
|
||||
M[i] ^= Uint32(0x80008000)
|
||||
|
||||
for _ in cutlass.range_constexpr(3):
|
||||
cute.copy(stsm_atom, Ai_bf16, sA_ldsm[warp_id_, None, warp_id_])
|
||||
cute.arch.sync_warp()
|
||||
acc[None, 0] = mma_bf16(Ai, M[None, 0], zeros_f32)
|
||||
acc[None, 1] = mma_bf16(Ai, M[None, 1], zeros_f32)
|
||||
Ai_bf16.store(acc.load().to(BFloat16))
|
||||
|
||||
for j in cutlass.range_constexpr(8):
|
||||
Ai_f32[j] *= 2.0
|
||||
cute.copy(
|
||||
ldsm_trans_atom,
|
||||
sA_ldsm[warp_id_, None, warp_id_],
|
||||
mma_B_bf16,
|
||||
)
|
||||
Ai_f32[None, 0] = mma_bf16(Ai, mma_B[None, 0], Ai_f32[None, 0])
|
||||
Ai_f32[None, 1] = mma_bf16(Ai, mma_B[None, 1], Ai_f32[None, 1])
|
||||
Ai_bf16.store(Ai_f32.load().to(BFloat16))
|
||||
|
||||
cute.copy(stsm_atom, Ai_bf16, sAi_ldsm[warp_id_, None, warp_id_])
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
|
||||
if warp_id_ > 0:
|
||||
neg_Ai = cute.make_rmem_tensor(4, Uint32)
|
||||
for i in cutlass.range_constexpr(4):
|
||||
neg_Ai[i] = Ai[i] ^ Uint32(0x80008000)
|
||||
|
||||
cute.copy(
|
||||
ldsm_trans_atom,
|
||||
sA_ldsm[warp_id_, None, warp_id_ - 1],
|
||||
mma_B_bf16,
|
||||
)
|
||||
acc[None, 0] = mma_bf16(neg_Ai, mma_B[None, 0], zeros_f32)
|
||||
acc[None, 1] = mma_bf16(neg_Ai, mma_B[None, 1], zeros_f32)
|
||||
Ai_bf16.store(acc.load().to(BFloat16))
|
||||
|
||||
cute.copy(
|
||||
ldsm_trans_atom,
|
||||
sAi_ldsm[warp_id_ - 1, None, warp_id_ - 1],
|
||||
mma_B_bf16,
|
||||
)
|
||||
acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], zeros_f32)
|
||||
acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], zeros_f32)
|
||||
Ai_bf16.store(acc.load().to(BFloat16))
|
||||
cute.copy(
|
||||
stsm_atom,
|
||||
Ai_bf16,
|
||||
sAi_ldsm[warp_id_, None, warp_id_ - 1],
|
||||
)
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
|
||||
if warp_id_ < 2:
|
||||
cute.copy(
|
||||
ldsm_atom,
|
||||
sA_ldsm[warp_id_ + 2, None, warp_id_],
|
||||
Ai_bf16,
|
||||
)
|
||||
cute.copy(
|
||||
ldsm_trans_atom,
|
||||
sAi_ldsm[warp_id_, None, warp_id_],
|
||||
mma_B_bf16,
|
||||
)
|
||||
acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], zeros_f32)
|
||||
acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], zeros_f32)
|
||||
|
||||
cute.copy(
|
||||
ldsm_atom,
|
||||
sA_ldsm[warp_id_ + 2, None, warp_id_ + 1],
|
||||
Ai_bf16,
|
||||
)
|
||||
cute.copy(
|
||||
ldsm_trans_atom,
|
||||
sAi_ldsm[warp_id_ + 1, None, warp_id_],
|
||||
mma_B_bf16,
|
||||
)
|
||||
acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], acc[None, 0])
|
||||
acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], acc[None, 1])
|
||||
|
||||
tmp = cute.make_rmem_tensor(8, BFloat16)
|
||||
tmp.store(acc.load().to(BFloat16))
|
||||
cute.copy(stsm_atom, tmp, sAi_ldsm[warp_id_ + 2, None, warp_id_])
|
||||
cute.arch.sync_warp()
|
||||
|
||||
cute.copy(
|
||||
ldsm_atom, sAi_ldsm[warp_id_ + 2, None, warp_id_ + 2], Ai_bf16
|
||||
)
|
||||
for i in cutlass.range_constexpr(4):
|
||||
Ai[i] ^= Uint32(0x80008000)
|
||||
cute.copy(
|
||||
ldsm_trans_atom,
|
||||
sAi_ldsm[warp_id_ + 2, None, warp_id_],
|
||||
mma_B_bf16,
|
||||
)
|
||||
acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], zeros_f32)
|
||||
acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], zeros_f32)
|
||||
tmp.store(acc.load().to(BFloat16))
|
||||
cute.copy(stsm_atom, tmp, sAi_ldsm[warp_id_ + 2, None, warp_id_])
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
|
||||
if warp_id_ == 0:
|
||||
cute.copy(ldsm_atom, sA_ldsm[3, None, 0], Ai_bf16)
|
||||
cute.copy(ldsm_trans_atom, sAi_ldsm[0, None, 0], mma_B_bf16)
|
||||
acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], zeros_f32)
|
||||
acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], zeros_f32)
|
||||
|
||||
for i in cutlass.range_constexpr(1, 3):
|
||||
cute.copy(ldsm_atom, sA_ldsm[3, None, i], Ai_bf16)
|
||||
cute.copy(ldsm_trans_atom, sAi_ldsm[i, None, 0], mma_B_bf16)
|
||||
acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], acc[None, 0])
|
||||
acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], acc[None, 1])
|
||||
|
||||
tmp = cute.make_rmem_tensor(8, BFloat16)
|
||||
tmp.store(acc.load().to(BFloat16))
|
||||
cute.copy(stsm_atom, tmp, sAi_ldsm[3, None, 0])
|
||||
cute.arch.sync_warp()
|
||||
|
||||
cute.copy(ldsm_atom, sAi_ldsm[3, None, 3], Ai_bf16)
|
||||
for i in cutlass.range_constexpr(4):
|
||||
Ai[i] ^= Uint32(0x80008000)
|
||||
cute.copy(ldsm_trans_atom, sAi_ldsm[3, None, 0], mma_B_bf16)
|
||||
acc[None, 0] = mma_bf16(Ai, mma_B[None, 0], zeros_f32)
|
||||
acc[None, 1] = mma_bf16(Ai, mma_B[None, 1], zeros_f32)
|
||||
tmp.store(acc.load().to(BFloat16))
|
||||
cute.copy(stsm_atom, tmp, sAi_ldsm[3, None, 0])
|
||||
|
||||
##### Phase 4: Ab = Ai * beta (KDA: no Abg) #####
|
||||
if warp_id_ == 3:
|
||||
cute.arch.mbarrier_wait(mma_u_mbar + stage_id, parity ^ 1)
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
|
||||
for i in cutlass.range_constexpr(BT // 16):
|
||||
cute.copy(ldsm_atom, sAi_ldsm[warp_id_, None, i], Ai_bf16)
|
||||
|
||||
col_coord = (None, lane_id % 4, None, i)
|
||||
s_beta_view = cute.make_tensor(s_beta, (2, 4, 2, BT // 16))
|
||||
beta_col = s_beta_view[col_coord].load().reshape((2, 1, 2))
|
||||
|
||||
Ai_f32 = cvt.bf16x2_to_fp32x2(Ai).load().reshape((2, 2, 2))
|
||||
|
||||
Ab_f32 = Ai_f32 * beta_col
|
||||
Ab = Ab_f32.to(BFloat16)
|
||||
Ab_tmem = Ab_tmem_base + (BT // 2) * stage_id + i * 8
|
||||
_tcgen05.st(warp_id_ * 32, Ab_tmem, "16x128b", 2, Ab)
|
||||
# KDA: Abg == Ab (no per-chunk g on the matrix). Duplicate into the
|
||||
# +16 lane region so the W MMA (reads Abg_tmem) sees valid data,
|
||||
# matching GDN's tmem layout exactly.
|
||||
_tcgen05.st(warp_id_ * 32 + 16, Ab_tmem, "16x128b", 2, Ab)
|
||||
|
||||
_tcgen05.wait_st()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(inv_mbar + stage_id)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
elif warp_id < 4:
|
||||
# epi warps (store U, W) -- VERBATIM from GDN
|
||||
stage_id = 0
|
||||
parity = 0
|
||||
|
||||
gU_tiles = cute.logical_divide(tmaU[None, head_id, None], (BT, None))
|
||||
gW_tiles = cute.logical_divide(tmaW[None, head_id, None], (BT, None))
|
||||
|
||||
s_row = warp_id * 16 + lane_id % 16
|
||||
sW_view = cute.zipped_divide(
|
||||
sW[s_row, None],
|
||||
tiler=cute.make_layout((8, 2)),
|
||||
)
|
||||
sU_view = cute.zipped_divide(
|
||||
sU[s_row, None],
|
||||
tiler=cute.make_layout((8, 2)),
|
||||
)
|
||||
|
||||
sW_view = sW_view[(None, lane_id // 16), None]
|
||||
sU_view = sU_view[(None, lane_id // 16), None]
|
||||
|
||||
for global_chunk_id in range(bid, num_global_chunks, grid_x):
|
||||
U_tmem = U_tmem_base + V_dim * stage_id
|
||||
if warp_id == 0:
|
||||
cute.arch.mbarrier_wait(mma_w_mbar + stage_id, parity)
|
||||
elif warp_id == 1:
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_wait_group(0, read=True)
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
w_f32 = _tcgen05.ld(warp_id * 32 + 16, U_tmem, "16x256b", K_dim // 8)
|
||||
_tcgen05.wait_ld()
|
||||
w_bf16 = cute.make_rmem_tensor((8, K_dim // 16), BFloat16)
|
||||
w_bf16.store(w_f32.to(BFloat16))
|
||||
cute.copy(stsm_atom, w_bf16, sW_view)
|
||||
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
fence_before_tma_store()
|
||||
if warp_id == 0:
|
||||
cute.arch.mbarrier_wait(mma_u_mbar + stage_id, parity)
|
||||
elif warp_id == 1:
|
||||
simple_tma_copy(
|
||||
W_tma_atom, sW, gW_tiles[(None, global_chunk_id), None]
|
||||
)
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
u_f32 = _tcgen05.ld(warp_id * 32, U_tmem, "16x256b", V_dim // 8)
|
||||
_tcgen05.wait_ld()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(epi_mbar + stage_id)
|
||||
u_bf16 = cute.make_rmem_tensor((8, V_dim // 16), BFloat16)
|
||||
u_bf16.store(u_f32.to(BFloat16))
|
||||
cute.copy(stsm_atom, u_bf16, sU_view)
|
||||
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
fence_before_tma_store()
|
||||
if warp_id == 1:
|
||||
simple_tma_copy(
|
||||
U_tma_atom, sU, gU_tiles[(None, global_chunk_id), None]
|
||||
)
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_commit_group()
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
@cache
|
||||
@staticmethod
|
||||
def compile(H: int, Hv: int, K_dim: int, V_dim: int, num_stages: int = 2):
|
||||
total_t = cute.sym_int()
|
||||
pad_t = cute.sym_int()
|
||||
total_chunks_n = cute.sym_int()
|
||||
num_sequences = cute.sym_int()
|
||||
|
||||
KL = make_fake_tensor(BFloat16, (total_t, Hv, K_dim), divisibility=16)
|
||||
KR = make_fake_tensor(BFloat16, (total_t, Hv, K_dim), divisibility=16)
|
||||
KG = make_fake_tensor(BFloat16, (total_t, Hv, K_dim), divisibility=16)
|
||||
V = make_fake_tensor(BFloat16, (total_t, Hv, V_dim), divisibility=16)
|
||||
U = make_fake_tensor(BFloat16, (pad_t, Hv, V_dim), divisibility=16)
|
||||
W = make_fake_tensor(BFloat16, (pad_t, Hv, K_dim), divisibility=16)
|
||||
beta = make_fake_tensor(Float32, (total_t, Hv), divisibility=4)
|
||||
cu_seqlens = make_fake_tensor(Int32, (num_sequences,), divisibility=1)
|
||||
chunk_indices = make_fake_tensor(Int32, (total_chunks_n, 2), divisibility=2)
|
||||
total_chunks = make_fake_tensor(Int32, (1,), divisibility=1)
|
||||
|
||||
kernel = Sm100KdaChunkUWKernel(H, Hv, K_dim, V_dim, num_stages)
|
||||
stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
|
||||
return cute.compile(
|
||||
kernel,
|
||||
KL,
|
||||
KR,
|
||||
KG,
|
||||
V,
|
||||
U,
|
||||
W,
|
||||
beta,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
total_chunks,
|
||||
Int32(148),
|
||||
stream,
|
||||
options="--enable-tvm-ffi",
|
||||
)
|
||||
|
||||
|
||||
def kkt_inv_uw_cutedsl(
|
||||
KL: torch.Tensor,
|
||||
KR: torch.Tensor,
|
||||
KG: torch.Tensor,
|
||||
V: torch.Tensor,
|
||||
U: torch.Tensor,
|
||||
W: torch.Tensor,
|
||||
beta: torch.Tensor,
|
||||
cu_seqlens: torch.Tensor,
|
||||
chunk_indices: torch.Tensor,
|
||||
total_chunks: torch.Tensor,
|
||||
num_sms: int = 148,
|
||||
) -> None:
|
||||
"""KDA KKT-inverse + U/W. KL/KR/KG are the pre-scaled keys (see module doc)."""
|
||||
_, Hv, K_dim = KL.shape
|
||||
_, _, V_dim = V.shape
|
||||
Sm100KdaChunkUWKernel.compile(Hv, Hv, K_dim, V_dim)(
|
||||
KL, KR, KG, V, U, W, beta, cu_seqlens, chunk_indices, total_chunks, num_sms
|
||||
)
|
||||
@@ -0,0 +1,584 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# KDA (Kimi Delta Attention) SM100 output kernel.
|
||||
#
|
||||
# Adapted from gdn_blackwell/kernel_o.py. KDA's decay is PER-CHANNEL, so the
|
||||
# decay cannot be applied as a post-MMA scalar Gamma. Instead all gate + scale
|
||||
# factors are folded OUTSIDE this kernel into three pre-scaled tensors:
|
||||
#
|
||||
# qg [c,d] = scale * q[c,d] * exp(g_cu[c,d]) -> Q @ H.T term
|
||||
# qg2[c,d] = scale * q[c,d] * exp(g_cu[c,d] - g_cu_last[d]) -> Aqk Q operand
|
||||
# kg [j,d] = k[j,d] * exp(g_cu_last[d] - g_cu[j,d]) -> Aqk K operand
|
||||
# (== kernel_h's kg, bounded <=|k|)
|
||||
#
|
||||
# Then:
|
||||
# Aqk = strictLowerIncl(qg2 @ kg.T) (masking warp: causal mask only, NO Gamma)
|
||||
# QH = qg @ H.T (scale + exp(g_cu) already baked)
|
||||
# O = QH + Aqk @ v_new (epilogue: NO scale, NO exp(g_cu))
|
||||
#
|
||||
# Net effect: g_cu is NOT needed inside this kernel at all.
|
||||
from functools import cache
|
||||
|
||||
import cutlass
|
||||
import torch
|
||||
from cuda.bindings.driver import CUstream
|
||||
from cutlass import BFloat16, Int32, Int64, Uint32, cute
|
||||
from cutlass.cute.nvgpu import cpasync, warp
|
||||
from quack.compile_utils import make_fake_tensor
|
||||
|
||||
from sglang.srt.layers.attention.cute_utils import (
|
||||
EVICT_FIRST,
|
||||
_tcgen05,
|
||||
cvt,
|
||||
fence_before_tma_store,
|
||||
simple_tma_copy,
|
||||
)
|
||||
|
||||
|
||||
class Sm100KdaChunkOKernel:
|
||||
"""KDA per-token output (see module docstring)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
H: int,
|
||||
Hv: int,
|
||||
K_dim: int,
|
||||
V_dim: int,
|
||||
BT: int = 64,
|
||||
num_stages: int = 2,
|
||||
) -> None:
|
||||
assert Hv % H == 0
|
||||
assert K_dim == 128
|
||||
assert V_dim == 128
|
||||
assert BT == 64
|
||||
self.H = H
|
||||
self.Hv = Hv
|
||||
self.K_dim = K_dim
|
||||
self.V_dim = V_dim
|
||||
self.BT = BT
|
||||
self.num_stages = num_stages
|
||||
self.num_warps = 10
|
||||
|
||||
@cute.jit
|
||||
def _make_bf16_tma_args(
|
||||
self,
|
||||
tensor: cute.Tensor,
|
||||
dim: cutlass.Constexpr[int],
|
||||
op: cpasync.TmaCopyOp,
|
||||
stages: cutlass.Constexpr[int],
|
||||
):
|
||||
swizzle_128B = cute.make_swizzle(3, 4, 3)
|
||||
slayout = cute.make_layout(
|
||||
(self.BT, 1, (64, dim // 64), stages),
|
||||
stride=(64, 0, (1, self.BT * 64), self.BT * dim),
|
||||
)
|
||||
slayout = cute.make_composed_layout(swizzle_128B, 0, slayout)
|
||||
atom, tma_tensor = cpasync.make_tiled_tma_atom(
|
||||
op,
|
||||
cute.logical_divide(tensor, (None, None, 64)),
|
||||
slayout,
|
||||
cta_tiler=(self.BT, 1, dim),
|
||||
)
|
||||
return atom, tma_tensor, slayout
|
||||
|
||||
@cute.jit
|
||||
def _make_h_tma_args(
|
||||
self,
|
||||
tensor: cute.Tensor,
|
||||
op: cpasync.TmaCopyOp,
|
||||
stages: cutlass.Constexpr[int],
|
||||
):
|
||||
num_elems = 128 // (tensor.element_type.width // 8)
|
||||
swizzle_128B = cute.make_swizzle(3, 4, 3)
|
||||
slayout = cute.make_layout(
|
||||
(1, self.V_dim, (num_elems, self.K_dim // num_elems), stages),
|
||||
stride=(0, num_elems, (1, self.V_dim * num_elems), self.V_dim * self.K_dim),
|
||||
)
|
||||
slayout = cute.make_composed_layout(swizzle_128B, 0, slayout)
|
||||
atom, tma_tensor = cpasync.make_tiled_tma_atom(
|
||||
op,
|
||||
cute.logical_divide(tensor, (None, None, num_elems)),
|
||||
slayout,
|
||||
cta_tiler=(1, self.V_dim, self.K_dim),
|
||||
)
|
||||
return atom, tma_tensor, slayout
|
||||
|
||||
@cute.jit
|
||||
def __call__(
|
||||
self,
|
||||
qg: cute.Tensor, # scale*q*exp(g_cu) [T, Hv, K]
|
||||
qg2: cute.Tensor, # scale*q*exp(g_cu-g_cu_last) [T, Hv, K]
|
||||
kg: cute.Tensor, # k*exp(g_cu_last-g_cu) [T, Hv, K]
|
||||
v_new_chunks: cute.Tensor,
|
||||
h: cute.Tensor,
|
||||
o: cute.Tensor,
|
||||
cu_seqlens: cute.Tensor,
|
||||
chunk_indices: cute.Tensor,
|
||||
total_chunks: cute.Tensor,
|
||||
num_sms: Int32,
|
||||
stream: CUstream,
|
||||
):
|
||||
grid = (num_sms // self.Hv, self.Hv, 1)
|
||||
block = (self.num_warps * 32, 1, 1)
|
||||
tma_g2s = cpasync.CopyBulkTensorTileG2SOp()
|
||||
tma_s2g = cpasync.CopyBulkTensorTileS2GOp()
|
||||
Q_args = self._make_bf16_tma_args(qg2, self.K_dim, tma_g2s, self.num_stages)
|
||||
Q2_args = self._make_bf16_tma_args(qg, self.K_dim, tma_g2s, self.num_stages)
|
||||
K_args = self._make_bf16_tma_args(kg, self.K_dim, tma_g2s, self.num_stages)
|
||||
V_args = self._make_bf16_tma_args(
|
||||
v_new_chunks, self.V_dim, tma_g2s, self.num_stages
|
||||
)
|
||||
H_args = self._make_h_tma_args(h, tma_g2s, self.num_stages)
|
||||
O_args = self._make_bf16_tma_args(o, self.V_dim, tma_s2g, 1)
|
||||
self.kernel(
|
||||
Q_args,
|
||||
Q2_args,
|
||||
K_args,
|
||||
V_args,
|
||||
H_args,
|
||||
O_args,
|
||||
o,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
total_chunks,
|
||||
).launch(grid=grid, block=block, stream=stream)
|
||||
|
||||
@cute.kernel
|
||||
def kernel(
|
||||
self,
|
||||
Q_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
Q2_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
K_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
V_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
H_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
O_args: tuple[cute.CopyAtom, cute.Tensor, cute.ComposedLayout],
|
||||
o: cute.Tensor,
|
||||
cu_seqlens: cute.Tensor,
|
||||
chunk_indices: cute.Tensor,
|
||||
total_chunks: cute.Tensor,
|
||||
):
|
||||
tid, _, _ = cute.arch.thread_idx()
|
||||
bid, v_head_id, _ = cute.arch.block_idx()
|
||||
grid_x, _, _ = cute.arch.grid_dim()
|
||||
warp_id = cute.arch.make_warp_uniform(tid // 32)
|
||||
lane_id = tid % 32
|
||||
|
||||
BT = self.BT
|
||||
K_dim = self.K_dim
|
||||
V_dim = self.V_dim
|
||||
num_stages = self.num_stages
|
||||
|
||||
num_global_chunks = total_chunks[0]
|
||||
|
||||
Q_tma_atom, tmaQ, sQ_layout = Q_args
|
||||
Q2_tma_atom, tmaQ2, sQ2_layout = Q2_args
|
||||
K_tma_atom, tmaK, sK_layout = K_args
|
||||
V_tma_atom, tmaV, sV_layout = V_args
|
||||
H_tma_atom, tmaH, sH_layout = H_args
|
||||
O_tma_atom, tmaO, sO_layout = O_args
|
||||
|
||||
def allocate_tensor(smem, dtype, layout):
|
||||
return smem.allocate_tensor(
|
||||
dtype, layout.outer, byte_alignment=128, swizzle=layout.inner
|
||||
)
|
||||
|
||||
smem = cutlass.utils.SmemAllocator()
|
||||
sQ = allocate_tensor(smem, BFloat16, sQ_layout)[None, 0, None, None]
|
||||
sQ2 = allocate_tensor(smem, BFloat16, sQ2_layout)[None, 0, None, None]
|
||||
sK = allocate_tensor(smem, BFloat16, sK_layout)[None, 0, None, None]
|
||||
sV = allocate_tensor(smem, BFloat16, sV_layout)[None, 0, None, None]
|
||||
sH = allocate_tensor(smem, BFloat16, sH_layout)[0, None, None, None]
|
||||
sO = allocate_tensor(smem, BFloat16, sO_layout)[None, 0, None, 0]
|
||||
|
||||
qk_full_mbar = smem.allocate_array(Int64, num_stages)
|
||||
hv_full_mbar = smem.allocate_array(Int64, num_stages)
|
||||
qk_empty_mbar = smem.allocate_array(Int64, num_stages)
|
||||
pv_mma_mbar = smem.allocate_array(Int64, num_stages)
|
||||
qk_mbar = smem.allocate_array(Int64, 1)
|
||||
mask_mbar = smem.allocate_array(Int64, 1)
|
||||
epi_mbar = smem.allocate_array(Int64, 1)
|
||||
taddr = smem.allocate(Int32, 4)
|
||||
|
||||
qk_tmem = 0
|
||||
p_tmem = 64
|
||||
out_tmem = 128
|
||||
qh_tmem = 256
|
||||
|
||||
if warp_id == 0:
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(num_stages):
|
||||
cute.arch.mbarrier_init(qk_full_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(qk_empty_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(hv_full_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(pv_mma_mbar + i, 1)
|
||||
cute.arch.mbarrier_init(qk_mbar, 1)
|
||||
cute.arch.mbarrier_init(mask_mbar, 128)
|
||||
cute.arch.mbarrier_init(epi_mbar, 128)
|
||||
cute.arch.mbarrier_init_fence()
|
||||
elif warp_id == 9:
|
||||
cpasync.prefetch_descriptor(Q_tma_atom)
|
||||
cpasync.prefetch_descriptor(Q2_tma_atom)
|
||||
cpasync.prefetch_descriptor(K_tma_atom)
|
||||
cpasync.prefetch_descriptor(V_tma_atom)
|
||||
cpasync.prefetch_descriptor(H_tma_atom)
|
||||
cute.arch.sync_threads()
|
||||
|
||||
if warp_id == 9:
|
||||
# TMA warp
|
||||
stage_id = 0
|
||||
parity = 1
|
||||
|
||||
for global_chunk_id in range(bid, num_global_chunks, grid_x):
|
||||
seq_id = chunk_indices[global_chunk_id, 0]
|
||||
chunk_id = chunk_indices[global_chunk_id, 1]
|
||||
bos = cu_seqlens[seq_id]
|
||||
|
||||
# copy qg2 (Q for Aqk), qg (Q for QH), kg (K for Aqk).
|
||||
# KDA: per v-head tensors, index by v_head_id.
|
||||
q_tile = cute.local_tile(
|
||||
cute.domain_offset((bos, 0), tmaQ[None, v_head_id, None]),
|
||||
tiler=(BT, K_dim),
|
||||
coord=(chunk_id, 0),
|
||||
)
|
||||
q2_tile = cute.local_tile(
|
||||
cute.domain_offset((bos, 0), tmaQ2[None, v_head_id, None]),
|
||||
tiler=(BT, K_dim),
|
||||
coord=(chunk_id, 0),
|
||||
)
|
||||
k_tile = cute.local_tile(
|
||||
cute.domain_offset((bos, 0), tmaK[None, v_head_id, None]),
|
||||
tiler=(BT, K_dim),
|
||||
coord=(chunk_id, 0),
|
||||
)
|
||||
mbar = qk_full_mbar + stage_id
|
||||
|
||||
cute.arch.mbarrier_wait(qk_empty_mbar + stage_id, parity)
|
||||
|
||||
with cute.arch.elect_one():
|
||||
STAGE_SIZE = BT * (K_dim + K_dim + K_dim) * 2
|
||||
cute.arch.mbarrier_arrive_and_expect_tx(mbar, STAGE_SIZE)
|
||||
simple_tma_copy(Q_tma_atom, q_tile, sQ[None, None, stage_id], mbar)
|
||||
simple_tma_copy(Q2_tma_atom, q2_tile, sQ2[None, None, stage_id], mbar)
|
||||
simple_tma_copy(K_tma_atom, k_tile, sK[None, None, stage_id], mbar)
|
||||
|
||||
# copy H and V
|
||||
gH = tmaH[global_chunk_id * self.Hv + v_head_id, None, None]
|
||||
gV = cute.local_tile(
|
||||
tmaV[None, v_head_id, None],
|
||||
tiler=(BT, V_dim),
|
||||
coord=(global_chunk_id, 0),
|
||||
)
|
||||
mbar = hv_full_mbar + stage_id
|
||||
|
||||
cute.arch.mbarrier_wait(pv_mma_mbar + stage_id, parity)
|
||||
|
||||
with cute.arch.elect_one():
|
||||
H_STAGE_SIZE = V_dim * K_dim * 2
|
||||
V_STAGE_SIZE = BT * V_dim * 2
|
||||
cute.arch.mbarrier_arrive_and_expect_tx(
|
||||
mbar, H_STAGE_SIZE + V_STAGE_SIZE
|
||||
)
|
||||
simple_tma_copy(
|
||||
H_tma_atom, gH, sH[None, None, stage_id], mbar, EVICT_FIRST
|
||||
)
|
||||
simple_tma_copy(
|
||||
V_tma_atom, gV, sV[None, None, stage_id], mbar, EVICT_FIRST
|
||||
)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
parity ^= 1
|
||||
|
||||
elif warp_id == 8:
|
||||
# MMA warp
|
||||
_tcgen05.alloc(taddr)
|
||||
|
||||
sdesc_template = _tcgen05.make_sdesc_128B_swizzle(BT * 128)
|
||||
qk_idesc = _tcgen05.make_bf16_idesc(BT, BT)
|
||||
qh_idesc = _tcgen05.make_bf16_idesc(BT, V_dim)
|
||||
pv_idesc = _tcgen05.make_bf16_idesc(BT, V_dim, transpose_B=True)
|
||||
|
||||
stage_id = 0
|
||||
tma_parity = 0
|
||||
mask_parity = 0
|
||||
|
||||
for global_chunk_id in range(bid, num_global_chunks, grid_x):
|
||||
qaddr = sQ[None, None, stage_id].iterator.toint()
|
||||
q2addr = sQ2[None, None, stage_id].iterator.toint()
|
||||
kaddr = sK[None, None, stage_id].iterator.toint()
|
||||
haddr = sH[None, None, stage_id].iterator.toint()
|
||||
vaddr = sV[None, None, stage_id].iterator.toint()
|
||||
qdesc_base = sdesc_template | (qaddr >> 4)
|
||||
q2desc_base = sdesc_template | (q2addr >> 4)
|
||||
kdesc_base = sdesc_template | (kaddr >> 4)
|
||||
hdesc_base = sdesc_template | (haddr >> 4)
|
||||
vdesc_base = sdesc_template | (vaddr >> 4)
|
||||
|
||||
##### 1st MMA: Aqk = qg2 @ kg.T #####
|
||||
cute.arch.mbarrier_wait(epi_mbar, mask_parity ^ 1)
|
||||
cute.arch.mbarrier_wait(qk_full_mbar + stage_id, tma_parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(K_dim // BT):
|
||||
for j in cutlass.range_constexpr(BT // 16):
|
||||
qdesc = qdesc_base | ((i * BT * 128 + j * 32) >> 4)
|
||||
kdesc = kdesc_base | ((i * BT * 128 + j * 32) >> 4)
|
||||
_tcgen05.mma_f16(
|
||||
qk_tmem, qdesc, kdesc, qk_idesc, (i > 0) or (j > 0)
|
||||
)
|
||||
_tcgen05.commit(qk_mbar)
|
||||
|
||||
##### 2nd MMA: QH = qg @ H.T #####
|
||||
cute.arch.mbarrier_wait(hv_full_mbar + stage_id, tma_parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(K_dim // BT):
|
||||
for j in cutlass.range_constexpr(BT // 16):
|
||||
q2desc = q2desc_base | ((i * BT * 128 + j * 32) >> 4)
|
||||
hdesc = hdesc_base | ((i * V_dim * 128 + j * 32) >> 4)
|
||||
_tcgen05.mma_f16(
|
||||
qh_tmem, q2desc, hdesc, qh_idesc, (i > 0) or (j > 0)
|
||||
)
|
||||
_tcgen05.commit(qk_empty_mbar + stage_id)
|
||||
|
||||
##### 3rd MMA: P @ V #####
|
||||
cute.arch.mbarrier_wait(mask_mbar, mask_parity)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
with cute.arch.elect_one():
|
||||
for i in cutlass.range_constexpr(BT // 16):
|
||||
vdesc = vdesc_base | ((i * 16 * 128) >> 4)
|
||||
_tcgen05.mma_ts_f16(
|
||||
out_tmem, p_tmem + i * 8, vdesc, pv_idesc, i > 0
|
||||
)
|
||||
_tcgen05.commit(pv_mma_mbar + stage_id)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
tma_parity ^= 1
|
||||
mask_parity ^= 1
|
||||
|
||||
cute.arch.mbarrier_wait(epi_mbar, mask_parity ^ 1)
|
||||
_tcgen05.dealloc()
|
||||
|
||||
elif warp_id >= 4:
|
||||
# masking warps -- KDA: causal mask only, decay is baked into operands.
|
||||
warp_id_ = warp_id % 4
|
||||
parity = 0
|
||||
|
||||
row_indices = cute.make_rmem_tensor(2, Int32)
|
||||
row_indices[0] = warp_id_ * 16 + lane_id // 4
|
||||
row_indices[1] = warp_id_ * 16 + lane_id // 4 + 8
|
||||
row_indices = row_indices.load().reshape((1, 2))
|
||||
|
||||
col_indices = cute.make_rmem_tensor(2, Int32)
|
||||
col_indices[0] = (lane_id % 4) * 2
|
||||
col_indices[1] = (lane_id % 4) * 2 + 1
|
||||
col_indices = col_indices.load().reshape((2, 1))
|
||||
|
||||
for global_chunk_id in range(bid, num_global_chunks, grid_x):
|
||||
if warp_id_ == 0:
|
||||
cute.arch.mbarrier_wait(qk_mbar, parity)
|
||||
cute.arch.barrier(barrier_id=1, number_of_threads=128)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
qk = _tcgen05.ld(warp_id_ * 32, qk_tmem, "16x256b", BT // 8)
|
||||
qk = qk.reshape((2, 2, BT // 8))
|
||||
_tcgen05.wait_ld()
|
||||
|
||||
for i in cutlass.range_constexpr(BT // 8):
|
||||
# KDA: Aqk already carries the per-channel decay (no Gamma).
|
||||
tmp = qk[None, None, i]
|
||||
tmp = cute.where(row_indices >= col_indices + i * 8, tmp, 0.0)
|
||||
|
||||
attn_lo = cute.make_rmem_tensor(2, Uint32)
|
||||
attn_lo[0] = cvt.fp32x2_to_bf16x2(tmp[0, 0], tmp[1, 0])
|
||||
attn_lo[1] = cvt.fp32x2_to_bf16x2(tmp[0, 1], tmp[1, 1])
|
||||
_tcgen05.st(warp_id_ * 32, p_tmem + i * 4, "16x128b", 1, attn_lo)
|
||||
|
||||
_tcgen05.wait_st()
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(mask_mbar)
|
||||
|
||||
parity ^= 1
|
||||
|
||||
else:
|
||||
# epilogue warps -- KDA: O = QH + P@V (scale & exp(g_cu) baked into qg).
|
||||
row0 = warp_id * 16 + lane_id // 4
|
||||
row1 = row0 + 8
|
||||
|
||||
stage_id = 0
|
||||
mma_parity = 0
|
||||
|
||||
op = cute.nvgpu.CopyUniversalOp()
|
||||
cp_4B = cute.make_copy_atom(op, BFloat16, num_bits_per_copy=32)
|
||||
stsm_op = warp.StMatrix8x8x16bOp(num_matrices=4, transpose=False)
|
||||
stsm_atom = cute.make_copy_atom(stsm_op, BFloat16)
|
||||
|
||||
WIDTH = 64
|
||||
o_view = cute.logical_divide(
|
||||
o[None, v_head_id, None],
|
||||
(None, cute.make_layout((2, 4, WIDTH // 8))),
|
||||
)
|
||||
o_view = o_view[None, ((None, lane_id % 4, None), None)]
|
||||
|
||||
for global_chunk_id in range(bid, num_global_chunks, grid_x):
|
||||
seq_id = chunk_indices[global_chunk_id, 0]
|
||||
chunk_id = chunk_indices[global_chunk_id, 1]
|
||||
bos = cu_seqlens[seq_id]
|
||||
eos = cu_seqlens[seq_id + 1]
|
||||
chunk_start = bos + chunk_id * BT
|
||||
full_chunk = chunk_start + BT <= eos
|
||||
|
||||
if warp_id == 0:
|
||||
cute.arch.mbarrier_wait(pv_mma_mbar + stage_id, mma_parity)
|
||||
elif warp_id == 3 and full_chunk:
|
||||
cute.arch.cp_async_bulk_wait_group(0, read=True)
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
_tcgen05.fence_after_thread_sync()
|
||||
|
||||
if full_chunk:
|
||||
for i in cutlass.range_constexpr(V_dim // WIDTH):
|
||||
qh = _tcgen05.ld(
|
||||
warp_id * 32, qh_tmem + i * WIDTH, "16x256b", WIDTH // 8
|
||||
)
|
||||
pv = _tcgen05.ld(
|
||||
warp_id * 32, out_tmem + i * WIDTH, "16x256b", WIDTH // 8
|
||||
)
|
||||
_tcgen05.wait_ld()
|
||||
if i == V_dim // WIDTH - 1:
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(epi_mbar)
|
||||
|
||||
qh = qh.reshape((2, 2, WIDTH // 8))
|
||||
pv = pv.reshape((2, 2, WIDTH // 8))
|
||||
|
||||
out_f32 = qh + pv
|
||||
out_bf16 = cute.make_rmem_tensor((8, WIDTH // 16), BFloat16)
|
||||
out_bf16.store(out_f32.to(BFloat16).reshape((8, WIDTH // 16)))
|
||||
|
||||
for j in cutlass.range_constexpr(WIDTH // 16):
|
||||
s_row = warp_id * 16 + lane_id % 16
|
||||
s_col = i * (WIDTH // 8) + j * 2 + lane_id // 16
|
||||
sO_tile = cute.local_tile(sO[s_row, None], (8,), (s_col,))
|
||||
cute.copy(stsm_atom, out_bf16[None, j], sO_tile)
|
||||
|
||||
cute.arch.barrier(barrier_id=2, number_of_threads=128)
|
||||
fence_before_tma_store()
|
||||
if warp_id == 3:
|
||||
gO = cute.local_tile(
|
||||
cute.domain_offset((bos, 0), tmaO[None, v_head_id, None]),
|
||||
tiler=(BT, V_dim),
|
||||
coord=(chunk_id, 0),
|
||||
)
|
||||
simple_tma_copy(O_tma_atom, sO, gO)
|
||||
with cute.arch.elect_one():
|
||||
cute.arch.cp_async_bulk_commit_group()
|
||||
|
||||
else:
|
||||
for i in cutlass.range_constexpr(V_dim // WIDTH):
|
||||
qh = _tcgen05.ld(
|
||||
warp_id * 32, qh_tmem + i * WIDTH, "16x256b", WIDTH // 8
|
||||
)
|
||||
pv = _tcgen05.ld(
|
||||
warp_id * 32, out_tmem + i * WIDTH, "16x256b", WIDTH // 8
|
||||
)
|
||||
_tcgen05.wait_ld()
|
||||
if i == V_dim // WIDTH - 1:
|
||||
_tcgen05.fence_before_thread_sync()
|
||||
cute.arch.mbarrier_arrive(epi_mbar)
|
||||
|
||||
qh = qh.reshape((2, 2, WIDTH // 8))
|
||||
pv = pv.reshape((2, 2, WIDTH // 8))
|
||||
|
||||
out_f32 = qh + pv
|
||||
out_bf16 = cute.make_rmem_tensor((2, 2, WIDTH // 8), BFloat16)
|
||||
out_bf16.store(out_f32.to(BFloat16))
|
||||
|
||||
if chunk_start + row0 < eos:
|
||||
cute.copy(
|
||||
cp_4B,
|
||||
out_bf16[None, 0, None],
|
||||
o_view[chunk_start + row0, None, None, i],
|
||||
)
|
||||
if chunk_start + row1 < eos:
|
||||
cute.copy(
|
||||
cp_4B,
|
||||
out_bf16[None, 1, None],
|
||||
o_view[chunk_start + row1, None, None, i],
|
||||
)
|
||||
|
||||
stage_id = (stage_id + 1) % num_stages
|
||||
if stage_id == 0:
|
||||
mma_parity ^= 1
|
||||
|
||||
@cache
|
||||
@staticmethod
|
||||
def compile(
|
||||
H: int,
|
||||
Hv: int,
|
||||
K_dim: int,
|
||||
V_dim: int,
|
||||
BT: int = 64,
|
||||
num_stages: int = 2,
|
||||
):
|
||||
total_t = cute.sym_int()
|
||||
pad_t = cute.sym_int()
|
||||
total_chunks_n = cute.sym_int()
|
||||
h_outer_n = cute.sym_int()
|
||||
cu_entries = cute.sym_int()
|
||||
|
||||
qg = make_fake_tensor(BFloat16, (total_t, Hv, K_dim), divisibility=16)
|
||||
qg2 = make_fake_tensor(BFloat16, (total_t, Hv, K_dim), divisibility=16)
|
||||
kg = make_fake_tensor(BFloat16, (total_t, Hv, K_dim), divisibility=16)
|
||||
v_new = make_fake_tensor(BFloat16, (pad_t, Hv, V_dim), divisibility=16)
|
||||
h_flat = make_fake_tensor(BFloat16, (h_outer_n, V_dim, K_dim), divisibility=16)
|
||||
o = make_fake_tensor(BFloat16, (total_t, Hv, V_dim), divisibility=16)
|
||||
cu_seqlens = make_fake_tensor(Int32, (cu_entries,), divisibility=1)
|
||||
chunk_indices = make_fake_tensor(Int32, (total_chunks_n, 2), divisibility=2)
|
||||
total_chunks = make_fake_tensor(Int32, (1,), divisibility=1)
|
||||
|
||||
kernel = Sm100KdaChunkOKernel(H, Hv, K_dim, V_dim, BT, num_stages)
|
||||
stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
|
||||
return cute.compile(
|
||||
kernel,
|
||||
qg,
|
||||
qg2,
|
||||
kg,
|
||||
v_new,
|
||||
h_flat,
|
||||
o,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
total_chunks,
|
||||
Int32(148),
|
||||
stream,
|
||||
options="--enable-tvm-ffi",
|
||||
)
|
||||
|
||||
|
||||
def kda_o_cutedsl(
|
||||
qg: torch.Tensor,
|
||||
qg2: torch.Tensor,
|
||||
kg: torch.Tensor,
|
||||
v_new_chunks: torch.Tensor,
|
||||
h: torch.Tensor,
|
||||
o: torch.Tensor,
|
||||
cu_seqlens: torch.Tensor,
|
||||
chunk_indices: torch.Tensor,
|
||||
total_chunks: torch.Tensor,
|
||||
num_sms: int = 148,
|
||||
) -> None:
|
||||
"""KDA output kernel. qg/qg2/kg are the pre-scaled tensors (see module doc)."""
|
||||
_, Hv, K_dim = qg.shape
|
||||
_, _, V_dim = o.shape
|
||||
Sm100KdaChunkOKernel.compile(Hv, Hv, K_dim, V_dim)(
|
||||
qg,
|
||||
qg2,
|
||||
kg,
|
||||
v_new_chunks.view(-1, Hv, V_dim),
|
||||
h.view(-1, V_dim, K_dim),
|
||||
o,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
total_chunks,
|
||||
num_sms,
|
||||
)
|
||||
@@ -0,0 +1,102 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Fused Triton prologue for the KDA Blackwell pipeline.
|
||||
#
|
||||
# In ONE pass per (chunk, head) it computes the per-chunk cumsum g_cu and the five
|
||||
# pre-scaled key/query tensors the cutedsl kernels consume, replacing ~30 separate
|
||||
# PyTorch elementwise ops + copies:
|
||||
#
|
||||
# g_cu = cumsum_within_chunk(g) [T, Hv, K] (fp32, for kernel_h decay)
|
||||
# g_last[d] = g_cu at the chunk's last token (= total sum over the chunk)
|
||||
# kL = k * exp(g_cu - g_last) (kkt KKT-left)
|
||||
# kR = k * exp(g_last - g_cu) (kkt KKT-right == kernel_h kg == kernel_o Aqk-K)
|
||||
# kgw = k * exp(g_cu) (kkt W operand)
|
||||
# qg = scale * q * exp(g_cu) (kernel_o Q@H)
|
||||
# qg2 = scale * q * exp(g_cu - g_last) (kernel_o Aqk-Q)
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _kda_prologue_kernel(
|
||||
q_ptr,
|
||||
k_ptr,
|
||||
g_ptr,
|
||||
kL_ptr,
|
||||
kR_ptr,
|
||||
kgw_ptr,
|
||||
qg_ptr,
|
||||
qg2_ptr,
|
||||
gcu_ptr,
|
||||
cu_seqlens_ptr,
|
||||
chunk_indices_ptr,
|
||||
scale,
|
||||
Hv: tl.constexpr,
|
||||
K: tl.constexpr,
|
||||
BT: tl.constexpr,
|
||||
):
|
||||
chunk = tl.program_id(0)
|
||||
head = tl.program_id(1)
|
||||
|
||||
seq_id = tl.load(chunk_indices_ptr + chunk * 2 + 0)
|
||||
chunk_id = tl.load(chunk_indices_ptr + chunk * 2 + 1)
|
||||
bos = tl.load(cu_seqlens_ptr + seq_id)
|
||||
eos = tl.load(cu_seqlens_ptr + seq_id + 1)
|
||||
off_t = bos + chunk_id * BT
|
||||
|
||||
row = off_t + tl.arange(0, BT)
|
||||
col = tl.arange(0, K)
|
||||
mask_row = row < eos
|
||||
offs = row[:, None] * (Hv * K) + head * K + col[None, :]
|
||||
mask = mask_row[:, None]
|
||||
|
||||
g = tl.load(g_ptr + offs, mask=mask, other=0.0).to(tl.float32)
|
||||
q = tl.load(q_ptr + offs, mask=mask, other=0.0).to(tl.float32)
|
||||
k = tl.load(k_ptr + offs, mask=mask, other=0.0).to(tl.float32)
|
||||
|
||||
g_cu = tl.cumsum(g, axis=0) # [BT, K]
|
||||
g_last = tl.sum(g, axis=0) # [K] (OOB rows contributed 0)
|
||||
gml = g_cu - g_last[None, :] # g_cu - g_last (>= 0, since g_cu>=g_last)
|
||||
e_gcu = tl.exp(g_cu) # <= 1
|
||||
e_gml = tl.exp(gml) # >= 1 (kL side; huge entries get masked)
|
||||
e_lmg = tl.exp(-gml) # <= 1 (bounded: kR / kg)
|
||||
|
||||
tl.store(gcu_ptr + offs, g_cu, mask=mask)
|
||||
tl.store(kL_ptr + offs, (k * e_gml).to(kL_ptr.dtype.element_ty), mask=mask)
|
||||
tl.store(kR_ptr + offs, (k * e_lmg).to(kR_ptr.dtype.element_ty), mask=mask)
|
||||
tl.store(kgw_ptr + offs, (k * e_gcu).to(kgw_ptr.dtype.element_ty), mask=mask)
|
||||
tl.store(qg_ptr + offs, (scale * q * e_gcu).to(qg_ptr.dtype.element_ty), mask=mask)
|
||||
tl.store(
|
||||
qg2_ptr + offs, (scale * q * e_gml).to(qg2_ptr.dtype.element_ty), mask=mask
|
||||
)
|
||||
|
||||
|
||||
def kda_prologue(q, k, g_act, scale, cu_seqlens, chunk_indices, num_chunks):
|
||||
"""q/k/g_act: [T, Hv, K]. Returns (kL, kR, kgw, qg, qg2) bf16 + g_cu fp32."""
|
||||
T, Hv, K = q.shape
|
||||
kL = torch.empty_like(q, dtype=torch.bfloat16)
|
||||
kR = torch.empty_like(q, dtype=torch.bfloat16)
|
||||
kgw = torch.empty_like(q, dtype=torch.bfloat16)
|
||||
qg = torch.empty_like(q, dtype=torch.bfloat16)
|
||||
qg2 = torch.empty_like(q, dtype=torch.bfloat16)
|
||||
g_cu = torch.empty_like(q, dtype=torch.float32)
|
||||
grid = (num_chunks, Hv)
|
||||
_kda_prologue_kernel[grid](
|
||||
q,
|
||||
k,
|
||||
g_act,
|
||||
kL,
|
||||
kR,
|
||||
kgw,
|
||||
qg,
|
||||
qg2,
|
||||
g_cu,
|
||||
cu_seqlens,
|
||||
chunk_indices,
|
||||
scale,
|
||||
Hv=Hv,
|
||||
K=K,
|
||||
BT=64,
|
||||
num_warps=8,
|
||||
)
|
||||
return kL, kR, kgw, qg, qg2, g_cu
|
||||
@@ -0,0 +1,148 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_blackwell() -> bool:
|
||||
"""True iff running on SM100+ (Blackwell), where the chunk prefill kernels run."""
|
||||
if not torch.cuda.is_available():
|
||||
return False
|
||||
major, _ = torch.cuda.get_device_capability()
|
||||
return major >= 10
|
||||
|
||||
|
||||
class CuteDSLKDAKernel(LinearAttnKernelBase):
|
||||
"""CuTe DSL kernel for KDA.
|
||||
|
||||
Decode: ``cutedsl_fused_sigmoid_gating_kda_update`` (SM90+).
|
||||
Extend (prefill): SM100 chunk pipeline ``chunk_kda_cutedsl`` (SM100+ only,
|
||||
``head_k_dim`` must be 128). On SM90 the prefill path is unsupported; callers
|
||||
query :attr:`supports_prefill` and fall back to Triton.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.supports_prefill = _is_blackwell()
|
||||
self._extend_fn: Optional[callable] = None
|
||||
self._l2norm_fn: Optional[callable] = None
|
||||
|
||||
def _ensure_extend_loaded(self, head_k_dim: int) -> None:
|
||||
if self._extend_fn is not None:
|
||||
return
|
||||
if not self.supports_prefill:
|
||||
major = (
|
||||
torch.cuda.get_device_capability()[0]
|
||||
if torch.cuda.is_available()
|
||||
else -1
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"CuTe DSL KDA prefill requires SM100+ (Blackwell); got SM{major}."
|
||||
)
|
||||
if head_k_dim != 128:
|
||||
raise RuntimeError(
|
||||
f"CuTe DSL KDA prefill requires head_k_dim=128, got {head_k_dim}."
|
||||
)
|
||||
from sglang.srt.layers.attention.fla.l2norm import l2norm_fwd
|
||||
from sglang.srt.layers.attention.linear.kernels.kda_blackwell import (
|
||||
chunk_kda_cutedsl,
|
||||
)
|
||||
|
||||
self._extend_fn = chunk_kda_cutedsl
|
||||
self._l2norm_fn = l2norm_fwd
|
||||
logger.info("Using CuTe DSL KDA prefill (Blackwell)")
|
||||
|
||||
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,
|
||||
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,
|
||||
A_log: Optional[torch.Tensor] = None,
|
||||
dt_bias: Optional[torch.Tensor] = None,
|
||||
lower_bound: Optional[float] = None,
|
||||
**kwargs,
|
||||
) -> torch.Tensor:
|
||||
head_k_dim = k.shape[-1]
|
||||
self._ensure_extend_loaded(head_k_dim)
|
||||
|
||||
# [1, T, HV, D] -> [T, HV, D]; L2-norm Q/K outside the kernel.
|
||||
q_n = self._l2norm_fn(q[0].contiguous()).to(torch.bfloat16)
|
||||
k_n = self._l2norm_fn(k[0].contiguous()).to(torch.bfloat16)
|
||||
v_in = v[0].contiguous().to(torch.bfloat16)
|
||||
# Trim g/beta to q's real token count: the [:real_num_tokens] slice in
|
||||
# unified_linear_attention_with_output narrows their batch dim (a no-op),
|
||||
# not tokens, so padded rows survive and break the kernel's shape check.
|
||||
num_tokens = q_n.shape[0]
|
||||
g_in = g[0][:num_tokens] # raw forget gate; activated inside chunk_kda_cutedsl
|
||||
beta_in = beta[0][:num_tokens].to(torch.float32)
|
||||
cu_seqlens = query_start_loc.to(torch.int32)
|
||||
|
||||
# Pool gather: remap padding (-1) to the last (sentinel) slot. State is
|
||||
# [slots, HV, V, K] == cutedsl [V,K] layout, no transpose needed.
|
||||
ssm_cache_indices = torch.where(
|
||||
cache_indices >= 0, cache_indices, ssm_states.shape[0] - 1
|
||||
).to(torch.long)
|
||||
initial_state = ssm_states[ssm_cache_indices].contiguous()
|
||||
|
||||
o, final_state = self._extend_fn(
|
||||
q_n,
|
||||
k_n,
|
||||
v_in,
|
||||
g_in,
|
||||
beta_in,
|
||||
initial_state,
|
||||
cu_seqlens,
|
||||
A_log=A_log,
|
||||
dt_bias=dt_bias,
|
||||
lower_bound=lower_bound,
|
||||
)
|
||||
|
||||
ssm_states.index_copy_(0, ssm_cache_indices, final_state.to(ssm_states.dtype))
|
||||
# Match chunk_kda's output layout [1, T, HV, V].
|
||||
return o.unsqueeze(0)
|
||||
|
||||
def target_verify(self, *args, **kwargs):
|
||||
raise NotImplementedError("CuteDSLKDAKernel does not support target_verify")
|
||||
@@ -0,0 +1,257 @@
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.layers.attention.linear.kernels.kernel_backend import (
|
||||
LinearAttnKernelBase,
|
||||
)
|
||||
|
||||
# FlashKDA chunk size. Sequences shorter than this fall back to Triton.
|
||||
_FLASHKDA_CHUNK_SIZE = 64
|
||||
|
||||
# FlashKDA's max sequence length, Batches whose longest sequence exceeds this
|
||||
# fall back to Triton for the whole batch.
|
||||
_FLASHKDA_MAX_SEQ_LEN = 2048
|
||||
|
||||
|
||||
def _load_flash_kda():
|
||||
"""Import the optional ``flash_kda`` CUTLASS module."""
|
||||
try:
|
||||
import flash_kda
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"The 'flashkda' KDA prefill backend requires the flash_kda module, "
|
||||
"which is not installed. Install it from source:\n"
|
||||
" pip install git+https://github.com/MoonshotAI/FlashKDA.git"
|
||||
) from e
|
||||
return flash_kda
|
||||
|
||||
|
||||
def _triton_fallback(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
g,
|
||||
beta,
|
||||
ssm_states,
|
||||
cache_indices,
|
||||
query_start_loc,
|
||||
A_log=None,
|
||||
dt_bias=None,
|
||||
lower_bound=None,
|
||||
):
|
||||
"""Fall back to the Triton chunk_kda kernel (handles all preprocessing).
|
||||
|
||||
`g` is the RAW gate; chunk_kda applies the gate activation internally when
|
||||
A_log is provided, so A_log/dt_bias/lower_bound must be threaded through too
|
||||
-- otherwise the fallback silently skips activation. chunk_kda updates the
|
||||
ssm state in-place via cache_indices and returns only the output tensor.
|
||||
"""
|
||||
from sglang.srt.layers.attention.fla.kda import chunk_kda
|
||||
|
||||
return chunk_kda(
|
||||
q=q,
|
||||
k=k,
|
||||
v=v,
|
||||
g=g,
|
||||
beta=beta,
|
||||
initial_state=ssm_states,
|
||||
initial_state_indices=cache_indices,
|
||||
use_qk_l2norm_in_kernel=True,
|
||||
cu_seqlens=query_start_loc,
|
||||
A_log=A_log,
|
||||
dt_bias=dt_bias,
|
||||
lower_bound=lower_bound,
|
||||
)
|
||||
|
||||
|
||||
class FlashKDAKernel(LinearAttnKernelBase):
|
||||
"""FlashKDA (MoonshotAI) fully-fused CUTLASS KDA prefill backend.
|
||||
|
||||
Wraps the external ``flash_kda`` package (https://github.com/MoonshotAI/FlashKDA).
|
||||
|
||||
FlashKDA fuses q/k L2 norm, beta sigmoid, and the KDA gate *inside* the
|
||||
kernel, so we pass RAW tensors plus ``A_log``/``dt_bias``/``lower_bound``.
|
||||
It is prefill-only, bf16, K == V == 128, HV == H (no GVA), and requires the
|
||||
safe (bounded) gate (``lower_bound`` set). The non-safe path and sequences
|
||||
outside [chunk_size, max_seq_len] fall back to Triton ``chunk_kda``.
|
||||
Requires an SM90+ GPU with the ``flash_kda`` package.
|
||||
"""
|
||||
|
||||
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:
|
||||
raise NotImplementedError("FlashKDAKernel only supports prefill (extend)")
|
||||
|
||||
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,
|
||||
A_log: Optional[torch.Tensor] = None,
|
||||
dt_bias: Optional[torch.Tensor] = None,
|
||||
lower_bound: Optional[float] = None,
|
||||
extend_seq_lens_cpu: Optional[list] = None,
|
||||
is_spec_decode: bool = False,
|
||||
**kwargs,
|
||||
) -> torch.Tensor:
|
||||
if self._should_fall_back(
|
||||
lower_bound, is_spec_decode, query_start_loc, extend_seq_lens_cpu
|
||||
):
|
||||
return _triton_fallback(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
g,
|
||||
beta,
|
||||
ssm_states,
|
||||
cache_indices,
|
||||
query_start_loc,
|
||||
A_log=A_log,
|
||||
dt_bias=dt_bias,
|
||||
lower_bound=lower_bound,
|
||||
)
|
||||
|
||||
return self._flashkda_extend(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
g,
|
||||
beta,
|
||||
ssm_states=ssm_states,
|
||||
cache_indices=cache_indices,
|
||||
query_start_loc=query_start_loc,
|
||||
A_log=A_log,
|
||||
dt_bias=dt_bias,
|
||||
lower_bound=lower_bound,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _should_fall_back(
|
||||
lower_bound: Optional[float],
|
||||
is_spec_decode: bool,
|
||||
query_start_loc: torch.Tensor,
|
||||
extend_seq_lens_cpu: Optional[list],
|
||||
) -> bool:
|
||||
"""Whether to use the Triton chunk_kda path instead of the fused kernel."""
|
||||
# Safe-gate only: the fused kernel does not support the unbounded gate
|
||||
# (-exp(A_log)*softplus); those models leave lower_bound unset.
|
||||
if lower_bound is None:
|
||||
return True
|
||||
# FlashKDA writes the committed recurrent state back in place, so it is
|
||||
# unsafe for speculative verify / draft-extend forwards (which must stay
|
||||
# rollback-able). Those reach this backend through forward_extend, so
|
||||
# gate them here rather than relying on the decode/target_verify stubs.
|
||||
if is_spec_decode:
|
||||
return True
|
||||
# Short sequences (< chunk size) and long sequences (> the crossover
|
||||
# where Triton's chunked prefill wins) are faster on Triton. Read the
|
||||
# per-request lengths from the CPU-side extend_seq_lens to avoid a
|
||||
# GPU->CPU sync on every layer; derive from query_start_loc (one sync)
|
||||
# only if they are unavailable.
|
||||
if extend_seq_lens_cpu is not None:
|
||||
if torch.is_tensor(extend_seq_lens_cpu):
|
||||
lo = int(extend_seq_lens_cpu.min())
|
||||
hi = int(extend_seq_lens_cpu.max())
|
||||
else:
|
||||
lo = min(extend_seq_lens_cpu)
|
||||
hi = max(extend_seq_lens_cpu)
|
||||
else:
|
||||
seq_lens = query_start_loc[1:] - query_start_loc[:-1]
|
||||
lo_t, hi_t = torch.aminmax(seq_lens)
|
||||
lo, hi = int(lo_t), int(hi_t)
|
||||
return lo < _FLASHKDA_CHUNK_SIZE or hi > _FLASHKDA_MAX_SEQ_LEN
|
||||
|
||||
def _flashkda_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,
|
||||
A_log: Optional[torch.Tensor] = None,
|
||||
dt_bias: Optional[torch.Tensor] = None,
|
||||
lower_bound: Optional[float] = None,
|
||||
) -> torch.Tensor:
|
||||
flash_kda = _load_flash_kda()
|
||||
|
||||
# Input shapes (varlen, B == 1, matching chunk_kda's contract):
|
||||
# q, k = [1, packed_seq, H, K] v = [1, packed_seq, HV, V]
|
||||
# g = [1, packed_seq, HV, K] beta = [1, packed_seq, H]
|
||||
# flash_kda wants these 4D tensors directly and RAW (it fuses l2norm /
|
||||
# beta sigmoid / gate activation in-kernel).
|
||||
num_heads = q.shape[2]
|
||||
head_dim = q.shape[3]
|
||||
scale = head_dim**-0.5
|
||||
|
||||
q = q.contiguous()
|
||||
k = k.contiguous()
|
||||
v = v.contiguous()
|
||||
g = g.contiguous()
|
||||
|
||||
# KimiDeltaAttention.forward already applies sigmoid to beta on the
|
||||
# prefill path, but flash_kda expects beta LOGITS (it sigmoids
|
||||
# internally). Invert back so the kernel recovers the intended value:
|
||||
# sigmoid(logit(p)) == p. (triton/cuLA consume the post-sigmoid beta.)
|
||||
beta = torch.logit(beta.float().clamp_(1e-7, 1.0 - 1e-7)).to(torch.bfloat16)
|
||||
beta = beta.contiguous()
|
||||
|
||||
# flash_kda wants A_log [H] fp32 and dt_bias [H, K] fp32. The model
|
||||
# stores A_log as [1, 1, H, 1] and dt_bias as 1D [H*K], so reshape both.
|
||||
A_log = A_log.reshape(-1).float().contiguous()
|
||||
if dt_bias is not None:
|
||||
dt_bias = dt_bias.reshape(num_heads, -1).float().contiguous()
|
||||
|
||||
# cu_seqlens must be int64 for flash_kda (FLA casts to long).
|
||||
cu_seqlens = query_start_loc.to(torch.int64)
|
||||
|
||||
# flash_kda varlen state is [N, H, V, K] -- the SAME layout as sglang's
|
||||
# KDA pool, so no transpose is needed. Advanced indexing copies, so the
|
||||
# final state is written back in-place below (matching chunk_kda).
|
||||
initial_state = ssm_states[cache_indices].contiguous()
|
||||
|
||||
out_buf = torch.empty_like(v)
|
||||
final_state = torch.empty_like(initial_state)
|
||||
|
||||
flash_kda.fwd(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
g,
|
||||
beta,
|
||||
scale,
|
||||
out_buf,
|
||||
A_log,
|
||||
dt_bias,
|
||||
lower_bound,
|
||||
initial_state=initial_state,
|
||||
final_state=final_state,
|
||||
cu_seqlens=cu_seqlens,
|
||||
)
|
||||
|
||||
ssm_states[cache_indices] = final_state
|
||||
|
||||
# out_buf is already [1, packed_seq, HV, V].
|
||||
return out_buf
|
||||
@@ -0,0 +1,173 @@
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.layers.attention.linear.kernels.kernel_backend import (
|
||||
LinearAttnKernelBase,
|
||||
)
|
||||
from sglang.srt.utils import is_cpu, is_npu
|
||||
|
||||
if not is_cpu():
|
||||
from sglang.srt.layers.attention.fla.fused_recurrent import (
|
||||
fused_recurrent_kda_packed_decode,
|
||||
)
|
||||
from sglang.srt.layers.attention.fla.fused_recurrent_linear_replayssm import (
|
||||
fused_recurrent_linear_replayssm_decode,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
class TritonKDAKernel(LinearAttnKernelBase):
|
||||
"""Triton-based kernel for KDA (Kimi Delta Attention) 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: feed the conv-1d output ``mixed_qkv``
|
||||
straight into a single fused Triton kernel that does Q/K/V extraction,
|
||||
gate/beta computation, l2-norm, and the recurrent state update.
|
||||
|
||||
Returns output tensor of shape [1, B, HV, V] to match the existing
|
||||
decode kernel output layout.
|
||||
"""
|
||||
B = mixed_qkv.shape[0]
|
||||
out = mixed_qkv.new_empty(B, 1, num_v_heads, head_v_dim)
|
||||
|
||||
# KDA ReplaySSM buffered decode: drop-in for the packed decode, same
|
||||
# args plus the three per-layer ring caches + the per-row write cursor
|
||||
# (and optional radix-track force-flush). Uses the gate-generic kernel
|
||||
# with is_kda=True (per-K gate); g_cache is [num_slots, HV, L, K].
|
||||
# When any ring tensor / cursor is None (flag off) we fall through to
|
||||
# the byte-identical legacy path below.
|
||||
replayssm_d = kwargs.get("replayssm_d")
|
||||
replayssm_k = kwargs.get("replayssm_k")
|
||||
replayssm_g = kwargs.get("replayssm_g")
|
||||
replayssm_write_pos = kwargs.get("replayssm_write_pos")
|
||||
replayssm_force_flush = kwargs.get("replayssm_force_flush")
|
||||
if (
|
||||
replayssm_d is not None
|
||||
and replayssm_k is not None
|
||||
and replayssm_g is not None
|
||||
and replayssm_write_pos is not None
|
||||
):
|
||||
K = ssm_states.shape[-1] # ssm_states: [num_slots, HV, V, K]
|
||||
fused_recurrent_linear_replayssm_decode(
|
||||
mixed_qkv=mixed_qkv,
|
||||
a=a.reshape(B, num_v_heads, K).contiguous(),
|
||||
b=b.reshape(B, num_v_heads).contiguous(),
|
||||
A_log=A_log.reshape(-1),
|
||||
dt_bias=dt_bias.reshape(num_v_heads, K).contiguous(),
|
||||
scale=scale,
|
||||
initial_state=ssm_states,
|
||||
d_cache=replayssm_d,
|
||||
k_cache=replayssm_k,
|
||||
g_cache=replayssm_g,
|
||||
out=out,
|
||||
ssm_state_indices=cache_indices,
|
||||
write_pos=replayssm_write_pos,
|
||||
force_flush=replayssm_force_flush,
|
||||
use_qk_l2norm_in_kernel=True,
|
||||
is_kda=True,
|
||||
)
|
||||
return out.transpose(0, 1)
|
||||
|
||||
# a may come in as [B, HV, K] (or [B, 1, HV*K]); b may come in as
|
||||
# [B, 1, HV]. Flatten both to the 2D shapes the kernel expects.
|
||||
if a.dim() != 2:
|
||||
a = a.reshape(B, -1)
|
||||
if b.dim() != 2:
|
||||
b = b.reshape(B, -1)
|
||||
fused_recurrent_kda_packed_decode(
|
||||
mixed_qkv=mixed_qkv,
|
||||
a=a,
|
||||
b=b,
|
||||
A_log=A_log.reshape(-1),
|
||||
dt_bias=dt_bias.reshape(-1),
|
||||
scale=scale,
|
||||
initial_state=ssm_states,
|
||||
out=out,
|
||||
ssm_state_indices=cache_indices,
|
||||
use_qk_l2norm_in_kernel=True,
|
||||
)
|
||||
# [B, 1, HV, V] -> [1, B, HV, V] view to match existing decode layout.
|
||||
return out.transpose(0, 1)
|
||||
|
||||
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 fused_sigmoid_gating_delta_rule_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,
|
||||
is_kda=True,
|
||||
)
|
||||
|
||||
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,
|
||||
A_log: Optional[torch.Tensor] = None,
|
||||
dt_bias: Optional[torch.Tensor] = None,
|
||||
lower_bound: Optional[float] = None,
|
||||
**kwargs,
|
||||
) -> torch.Tensor:
|
||||
return chunk_kda(
|
||||
q=q,
|
||||
k=k,
|
||||
v=v,
|
||||
g=g,
|
||||
beta=beta,
|
||||
initial_state=ssm_states,
|
||||
initial_state_indices=cache_indices,
|
||||
use_qk_l2norm_in_kernel=True,
|
||||
cu_seqlens=query_start_loc,
|
||||
A_log=A_log,
|
||||
dt_bias=dt_bias,
|
||||
lower_bound=lower_bound,
|
||||
)
|
||||
@@ -0,0 +1,62 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
class LinearAttnKernelBase(ABC):
|
||||
"""Abstract base class for linear attention kernel implementations.
|
||||
|
||||
Each concrete implementation wraps a specific kernel (Triton, CuTe DSL, etc.)
|
||||
and provides decode/extend/target_verify methods with a unified interface.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
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: ...
|
||||
|
||||
@abstractmethod
|
||||
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: ...
|
||||
|
||||
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,
|
||||
**kwargs,
|
||||
) -> torch.Tensor:
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not support target_verify"
|
||||
)
|
||||
@@ -0,0 +1,767 @@
|
||||
# Adapted from https://github.com/vllm-project/vllm/blob/main/vllm/model_executor/layers/mamba/linear_attn.py
|
||||
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
from einops import rearrange
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _fwd_diag_kernel(
|
||||
Q,
|
||||
K,
|
||||
V,
|
||||
Out,
|
||||
S,
|
||||
b: tl.constexpr,
|
||||
h: tl.constexpr,
|
||||
n,
|
||||
d: tl.constexpr,
|
||||
e: tl.constexpr,
|
||||
BLOCK: tl.constexpr,
|
||||
NUM_BLOCK,
|
||||
CBLOCK: tl.constexpr,
|
||||
):
|
||||
# This kernel computes the diagonal blocks of the attention matrix
|
||||
# Each diagonal block represents attention
|
||||
# where queries attend to keys in the same block
|
||||
off = tl.program_id(0)
|
||||
off_bh = off // NUM_BLOCK # batch-head index
|
||||
off_block = off % NUM_BLOCK # block index within the sequence
|
||||
off_cblock = tl.program_id(1) # sub-block index within a block
|
||||
|
||||
off_h = off_bh % h # head index
|
||||
|
||||
# Calculate base offsets for the current batch and head
|
||||
qk_offset = off_bh * n * d
|
||||
v_offset = off_bh * n * e
|
||||
o_offset = off_bh * n * e
|
||||
|
||||
# Calculate offsets for the current block
|
||||
block_offset = off_block * BLOCK
|
||||
qk_block_offset = block_offset * d
|
||||
v_block_offset = block_offset * e
|
||||
o_block_offset = block_offset * e
|
||||
|
||||
# Calculate offsets for the current sub-block
|
||||
cblock_offset = off_cblock * CBLOCK
|
||||
q_cblock_offset = cblock_offset * d
|
||||
o_cblock_offset = cblock_offset * e
|
||||
|
||||
# Calculate pointers to the query, key, value, and output tensors
|
||||
Q_block_ptr = (
|
||||
Q
|
||||
+ qk_offset
|
||||
+ qk_block_offset
|
||||
+ q_cblock_offset
|
||||
+ tl.arange(0, CBLOCK)[:, None] * d
|
||||
+ tl.arange(0, d)[None, :]
|
||||
)
|
||||
K_trans_block_ptr = (
|
||||
K
|
||||
+ qk_offset
|
||||
+ qk_block_offset
|
||||
+ tl.arange(0, CBLOCK)[None, :] * d
|
||||
+ tl.arange(0, d)[:, None]
|
||||
)
|
||||
V_block_ptr = (
|
||||
V
|
||||
+ v_offset
|
||||
+ v_block_offset
|
||||
+ tl.arange(0, CBLOCK)[:, None] * e
|
||||
+ tl.arange(0, e)[None, :]
|
||||
)
|
||||
O_block_ptr = (
|
||||
Out
|
||||
+ o_offset
|
||||
+ o_block_offset
|
||||
+ o_cblock_offset
|
||||
+ tl.arange(0, CBLOCK)[:, None] * e
|
||||
+ tl.arange(0, e)[None, :]
|
||||
)
|
||||
|
||||
# Load the decay rate for the current head
|
||||
S_block_ptr = S + off_h
|
||||
s = tl.load(S_block_ptr)
|
||||
|
||||
i = off_cblock
|
||||
q_index = tl.arange(0, CBLOCK) + i * CBLOCK
|
||||
|
||||
# Load query values
|
||||
q = tl.load(Q_block_ptr, mask=block_offset + q_index[:, None] < n, other=0.0).to(
|
||||
tl.float32
|
||||
)
|
||||
|
||||
# Initialize output accumulator
|
||||
qkv = tl.zeros([CBLOCK, e], dtype=tl.float32)
|
||||
|
||||
# Process all sub-blocks up to and
|
||||
# including the current one (causal attention)
|
||||
for j in range(i + 1):
|
||||
kv_index = tl.arange(0, CBLOCK) + j * CBLOCK
|
||||
diff = q_index[:, None] - kv_index[None, :]
|
||||
s_index = s * diff
|
||||
# Apply causal mask: only attend to positions before the current one
|
||||
s_index = tl.where(diff >= 0, -s_index, float("-inf"))
|
||||
decay = tl.exp(s_index)
|
||||
|
||||
# Load key and value
|
||||
k_trans = tl.load(
|
||||
K_trans_block_ptr,
|
||||
mask=block_offset + kv_index[None, :] < n,
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
v = tl.load(
|
||||
V_block_ptr,
|
||||
mask=block_offset + kv_index[:, None] < n,
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
|
||||
# Compute attention scores and apply decay
|
||||
qk = tl.dot(q, k_trans) * decay
|
||||
|
||||
# Compute weighted values and accumulate
|
||||
qkv += tl.dot(qk, v)
|
||||
|
||||
# Move to the next sub-block
|
||||
K_trans_block_ptr += CBLOCK * d
|
||||
V_block_ptr += CBLOCK * e
|
||||
|
||||
# Store the result
|
||||
tl.store(
|
||||
O_block_ptr,
|
||||
qkv.to(O_block_ptr.dtype.element_ty),
|
||||
mask=block_offset + q_index[:, None] < n,
|
||||
)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _fwd_kv_parallel(
|
||||
K,
|
||||
V,
|
||||
K_decay,
|
||||
KV,
|
||||
b: tl.constexpr,
|
||||
h: tl.constexpr,
|
||||
n,
|
||||
d: tl.constexpr,
|
||||
e: tl.constexpr,
|
||||
BLOCK: tl.constexpr,
|
||||
NUM_BLOCK,
|
||||
D_FBLOCK: tl.constexpr,
|
||||
E_FBLOCK: tl.constexpr,
|
||||
NUM_FBLOCK: tl.constexpr,
|
||||
CBLOCK: tl.constexpr,
|
||||
NUM_CBLOCK: tl.constexpr,
|
||||
):
|
||||
# This kernel computes the key-value outer
|
||||
# products for each block in parallel
|
||||
off_bh = tl.program_id(0) # batch-head index
|
||||
off_block = tl.program_id(1) # block index
|
||||
|
||||
off_h = off_bh % h # head index
|
||||
|
||||
block_offset = off_block * BLOCK
|
||||
|
||||
# Calculate offsets for the current block
|
||||
k_block_offset = block_offset * d
|
||||
v_block_offset = block_offset * e
|
||||
kv_block_offset = off_block * d * e
|
||||
|
||||
# Calculate base offsets for the current batch and head
|
||||
k_offset = off_bh * n * d
|
||||
v_offset = off_bh * n * e
|
||||
kv_offset = off_bh * NUM_BLOCK * d * e
|
||||
|
||||
# Calculate pointers to the key, value, and key-value tensors
|
||||
K_trans_block_ptr = (
|
||||
K
|
||||
+ k_offset
|
||||
+ k_block_offset
|
||||
+ tl.arange(0, CBLOCK)[None, :] * d
|
||||
+ tl.arange(0, D_FBLOCK)[:, None]
|
||||
)
|
||||
V_block_ptr = (
|
||||
V
|
||||
+ v_offset
|
||||
+ v_block_offset
|
||||
+ tl.arange(0, CBLOCK)[:, None] * e
|
||||
+ tl.arange(0, E_FBLOCK)[None, :]
|
||||
)
|
||||
KV_block_ptr = (
|
||||
KV
|
||||
+ kv_offset
|
||||
+ kv_block_offset
|
||||
+ tl.arange(0, D_FBLOCK)[:, None] * e
|
||||
+ tl.arange(0, E_FBLOCK)[None, :]
|
||||
)
|
||||
|
||||
# Load the decay factors for the current head and block
|
||||
k_decay_ptr = K_decay + off_h * BLOCK + tl.arange(0, CBLOCK)[None, :]
|
||||
|
||||
kv_index = tl.arange(0, CBLOCK)
|
||||
|
||||
# Initialize the key-value outer product accumulator
|
||||
kv = tl.zeros([D_FBLOCK, E_FBLOCK], dtype=tl.float32)
|
||||
|
||||
# Handle the last block which might be smaller than BLOCK
|
||||
if off_block == NUM_BLOCK - 1:
|
||||
split_n = n - (NUM_BLOCK - 1) * BLOCK
|
||||
else:
|
||||
split_n = BLOCK
|
||||
left_shift = tl.cdiv(split_n, CBLOCK) * CBLOCK - split_n
|
||||
num_blocks = min(tl.cdiv(split_n, CBLOCK), NUM_CBLOCK)
|
||||
k_decay_ptr += (NUM_CBLOCK - num_blocks) * CBLOCK
|
||||
|
||||
# Process all sub-blocks in the current block
|
||||
for j in range(num_blocks):
|
||||
left_bound = (1 - j) * left_shift
|
||||
# Load key and value, handling boundary conditions
|
||||
k_trans = tl.load(
|
||||
K_trans_block_ptr - left_shift * d,
|
||||
mask=kv_index[None, :] >= left_bound,
|
||||
other=0.0,
|
||||
)
|
||||
v = tl.load(
|
||||
V_block_ptr - left_shift * e,
|
||||
mask=kv_index[:, None] >= left_bound,
|
||||
other=0.0,
|
||||
)
|
||||
|
||||
# Load decay factor and compute weighted key-value outer product
|
||||
k_decay = tl.load(k_decay_ptr)
|
||||
kv += tl.dot(k_trans * k_decay, v)
|
||||
|
||||
# Move to the next sub-block
|
||||
K_trans_block_ptr += CBLOCK * d
|
||||
V_block_ptr += CBLOCK * e
|
||||
k_decay_ptr += CBLOCK
|
||||
|
||||
# Store the result
|
||||
tl.store(KV_block_ptr, kv.to(KV_block_ptr.dtype.element_ty))
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _fwd_kv_reduce(
|
||||
S,
|
||||
KV,
|
||||
KV_HISTORY,
|
||||
b: tl.constexpr,
|
||||
h: tl.constexpr,
|
||||
n,
|
||||
d: tl.constexpr,
|
||||
e: tl.constexpr,
|
||||
BLOCK: tl.constexpr,
|
||||
NUM_BLOCK,
|
||||
D_FBLOCK: tl.constexpr,
|
||||
E_FBLOCK: tl.constexpr,
|
||||
):
|
||||
# This kernel reduces the key-value outer products
|
||||
# across blocks and updates the KV history
|
||||
off_bh = tl.program_id(0) # batch-head index
|
||||
off_h = off_bh % h # head index
|
||||
|
||||
kv_offset = off_bh * NUM_BLOCK * d * e
|
||||
|
||||
# Calculate pointer to the key-value tensor
|
||||
KV_block_ptr = (
|
||||
KV
|
||||
+ kv_offset
|
||||
+ tl.arange(0, D_FBLOCK)[:, None] * e
|
||||
+ tl.arange(0, E_FBLOCK)[None, :]
|
||||
)
|
||||
|
||||
# Load the decay rate for the current head
|
||||
s_ptrs = S + off_h
|
||||
s = tl.load(s_ptrs)
|
||||
|
||||
# Calculate pointer to the key-value history tensor
|
||||
kv_history_offset = off_bh * d * e
|
||||
KV_HISTORY_block_ptr = (
|
||||
KV_HISTORY
|
||||
+ kv_history_offset
|
||||
+ tl.arange(0, D_FBLOCK)[:, None] * e
|
||||
+ tl.arange(0, E_FBLOCK)[None, :]
|
||||
)
|
||||
|
||||
# Load the previous key-value history
|
||||
kv_pre = tl.load(KV_HISTORY_block_ptr).to(tl.float32)
|
||||
|
||||
# Process all blocks in reverse order to compute the prefix sum
|
||||
for i in range(NUM_BLOCK):
|
||||
block_size = min(n - i * BLOCK, BLOCK)
|
||||
# Compute decay factor for the current block
|
||||
block_decay = tl.exp(-s.to(tl.float32) * block_size)
|
||||
|
||||
# Load the current key-value outer product
|
||||
kv_cur = tl.load(KV_block_ptr).to(tl.float32)
|
||||
# Store the previous key-value history to the current block
|
||||
tl.store(KV_block_ptr, kv_pre.to(KV_block_ptr.dtype.element_ty))
|
||||
|
||||
# Update the key-value history with the current block
|
||||
kv_pre = block_decay * kv_pre + kv_cur
|
||||
KV_block_ptr += d * e
|
||||
|
||||
# Store the updated key-value history
|
||||
tl.store(KV_HISTORY_block_ptr, kv_pre)
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _fwd_none_diag_kernel(
|
||||
Q,
|
||||
Out,
|
||||
S,
|
||||
KV,
|
||||
b: tl.constexpr,
|
||||
h: tl.constexpr,
|
||||
n,
|
||||
d: tl.constexpr,
|
||||
e: tl.constexpr,
|
||||
BLOCK: tl.constexpr,
|
||||
NUM_BLOCK,
|
||||
E_FBLOCK: tl.constexpr,
|
||||
CBLOCK: tl.constexpr,
|
||||
NUM_CBLOCK: tl.constexpr,
|
||||
):
|
||||
# This kernel computes the non-diagonal blocks of the attention matrix
|
||||
# Each non-diagonal block represents attention
|
||||
# where queries attend to keys in different blocks
|
||||
off_bh = tl.program_id(0) # batch-head index
|
||||
off_h = off_bh % h # head index
|
||||
|
||||
off_nc = tl.program_id(1)
|
||||
off_n = off_nc // NUM_CBLOCK # block index
|
||||
off_c = off_nc % NUM_CBLOCK # sub-block index
|
||||
off_e = tl.program_id(2) # output feature block index
|
||||
|
||||
n_offset = off_n * BLOCK
|
||||
c_offset = off_c * CBLOCK
|
||||
e_offset = off_e * E_FBLOCK
|
||||
block_offset = n_offset + c_offset
|
||||
|
||||
# Calculate offsets for the current batch, head, and block
|
||||
q_offset = off_bh * n * d + (n_offset + c_offset) * d
|
||||
o_offset = off_bh * n * e + (n_offset + c_offset) * e + e_offset
|
||||
kv_offset = off_bh * NUM_BLOCK * d * e + off_n * d * e + e_offset
|
||||
|
||||
# Calculate pointers to the query, output, and key-value tensors
|
||||
Q_block_ptr = (
|
||||
Q + q_offset + tl.arange(0, CBLOCK)[:, None] * d + tl.arange(0, d)[None, :]
|
||||
)
|
||||
O_block_ptr = (
|
||||
Out
|
||||
+ o_offset
|
||||
+ tl.arange(0, CBLOCK)[:, None] * e
|
||||
+ tl.arange(0, E_FBLOCK)[None, :]
|
||||
)
|
||||
KV_block_ptr = (
|
||||
KV + kv_offset + tl.arange(0, d)[:, None] * e + tl.arange(0, E_FBLOCK)[None, :]
|
||||
)
|
||||
|
||||
# Load the decay rate for the current head
|
||||
S_block_ptr = S + off_h
|
||||
s = tl.load(S_block_ptr)
|
||||
|
||||
c_array = tl.arange(0, CBLOCK)
|
||||
|
||||
# Load the key-value outer product for the current block
|
||||
kv = tl.load(KV_block_ptr).to(tl.float32)
|
||||
q_index = block_offset + tl.arange(0, CBLOCK)
|
||||
|
||||
# Load query values
|
||||
q = tl.load(Q_block_ptr, mask=q_index[:, None] < n, other=0.0).to(tl.float32)
|
||||
|
||||
# Compute decay factors for the current sub-block
|
||||
q_decay = tl.exp(-s.to(tl.float32) * (off_c * CBLOCK + c_array[:, None]))
|
||||
|
||||
# Compute non-diagonal attention output
|
||||
qkv_none_diag = tl.dot(q, kv) * q_decay
|
||||
|
||||
# Load diagonal attention output (computed by _fwd_diag_kernel)
|
||||
qkv_diag = tl.load(O_block_ptr, mask=q_index[:, None] < n, other=0.0).to(tl.float32)
|
||||
|
||||
# Combine diagonal and non-diagonal attention outputs
|
||||
qkv = qkv_diag + qkv_none_diag
|
||||
|
||||
# Store the result
|
||||
tl.store(
|
||||
O_block_ptr, qkv.to(O_block_ptr.dtype.element_ty), mask=q_index[:, None] < n
|
||||
)
|
||||
|
||||
|
||||
class _attention(torch.autograd.Function):
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, q, k, v, s, kv_history):
|
||||
# Forward pass of the lightning attention algorithm
|
||||
q = q.contiguous()
|
||||
k = k.contiguous()
|
||||
v = v.contiguous()
|
||||
s = s.contiguous()
|
||||
|
||||
# Check CUDA compute capability
|
||||
capability = torch.cuda.get_device_capability()
|
||||
if capability[0] < 8:
|
||||
raise RuntimeError(
|
||||
"Flash attention currently only supported",
|
||||
"for compute capability >= 80",
|
||||
)
|
||||
|
||||
# Get input dimensions
|
||||
b, h, n, d = q.shape
|
||||
e = v.shape[-1]
|
||||
|
||||
# Initialize output tensor
|
||||
o = torch.empty((b, h, n, e), dtype=q.dtype, device=q.device)
|
||||
|
||||
# Set block sizes
|
||||
BLOCK = 256
|
||||
NUM_BLOCK = triton.cdiv(n, BLOCK)
|
||||
|
||||
CBLOCK = 32
|
||||
NUM_CBLOCK = BLOCK // CBLOCK
|
||||
assert BLOCK % CBLOCK == 0, "BLOCK must be a multiple of CBLOCK"
|
||||
|
||||
# Compute decay factors for keys
|
||||
array = torch.arange(0, BLOCK, device=q.device) + 1
|
||||
k_decay = torch.exp(-s * (BLOCK - array.reshape(1, -1)))
|
||||
|
||||
# Step 1: Compute diagonal blocks of attention
|
||||
grid = (b * h * NUM_BLOCK, NUM_CBLOCK)
|
||||
_fwd_diag_kernel[grid](
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
o,
|
||||
s,
|
||||
b,
|
||||
h,
|
||||
n,
|
||||
d,
|
||||
e,
|
||||
BLOCK=BLOCK,
|
||||
NUM_BLOCK=NUM_BLOCK,
|
||||
CBLOCK=CBLOCK,
|
||||
)
|
||||
|
||||
# Set feature block sizes
|
||||
NUM_FBLOCK = 1
|
||||
D_FBLOCK = d // NUM_FBLOCK
|
||||
assert d % NUM_FBLOCK == 0
|
||||
E_FBLOCK = e // NUM_FBLOCK
|
||||
assert e % NUM_FBLOCK == 0
|
||||
|
||||
CBLOCK = 64
|
||||
NUM_CBLOCK = BLOCK // CBLOCK
|
||||
assert BLOCK % CBLOCK == 0, "BLOCK must be a multiple of CBLOCK"
|
||||
|
||||
# Step 2: Compute key-value outer products for each block in parallel
|
||||
kv = torch.empty((b, h, NUM_BLOCK, d, e), dtype=torch.float32, device=q.device)
|
||||
grid = (b * h, NUM_BLOCK)
|
||||
_fwd_kv_parallel[grid](
|
||||
k,
|
||||
v,
|
||||
k_decay,
|
||||
kv,
|
||||
b,
|
||||
h,
|
||||
n,
|
||||
d,
|
||||
e,
|
||||
BLOCK=BLOCK,
|
||||
NUM_BLOCK=NUM_BLOCK,
|
||||
D_FBLOCK=D_FBLOCK,
|
||||
E_FBLOCK=E_FBLOCK,
|
||||
NUM_FBLOCK=NUM_FBLOCK,
|
||||
CBLOCK=CBLOCK,
|
||||
NUM_CBLOCK=NUM_CBLOCK,
|
||||
)
|
||||
|
||||
# Step 3: Reduce key-value outer products
|
||||
# across blocks and update KV history
|
||||
grid = (b * h, NUM_FBLOCK)
|
||||
_fwd_kv_reduce[grid](
|
||||
s,
|
||||
kv,
|
||||
kv_history,
|
||||
b,
|
||||
h,
|
||||
n,
|
||||
d,
|
||||
e,
|
||||
BLOCK=BLOCK,
|
||||
NUM_BLOCK=NUM_BLOCK,
|
||||
D_FBLOCK=D_FBLOCK,
|
||||
E_FBLOCK=E_FBLOCK,
|
||||
)
|
||||
|
||||
# Step 4: Compute non-diagonal blocks of attention
|
||||
grid = (b * h, NUM_BLOCK * NUM_CBLOCK)
|
||||
_fwd_none_diag_kernel[grid](
|
||||
q,
|
||||
o,
|
||||
s,
|
||||
kv,
|
||||
b,
|
||||
h,
|
||||
n,
|
||||
d,
|
||||
e,
|
||||
BLOCK=BLOCK,
|
||||
NUM_BLOCK=NUM_BLOCK,
|
||||
E_FBLOCK=E_FBLOCK,
|
||||
CBLOCK=CBLOCK,
|
||||
NUM_CBLOCK=NUM_CBLOCK,
|
||||
)
|
||||
|
||||
# Save tensors for backward pass
|
||||
ctx.save_for_backward(q, k, v, s, kv)
|
||||
ctx.BLOCK = BLOCK
|
||||
|
||||
return o, torch.cat([kv, kv_history.unsqueeze(2)], dim=2)
|
||||
|
||||
|
||||
# Apply the lightning attention function
|
||||
lightning_attention_ = _attention.apply
|
||||
|
||||
|
||||
def lightning_attention(q, k, v, ed, block_size=256, kv_history=None):
|
||||
"""
|
||||
Apply lightning attention algorithm
|
||||
to compute attention efficiently.
|
||||
|
||||
Args:
|
||||
q: Query tensor of shape [batch, heads, seq_len, dim]
|
||||
k: Key tensor of shape [batch, heads, seq_len, dim]
|
||||
v: Value tensor of shape [batch, heads, seq_len, dim_v]
|
||||
ed: Decay rate tensor of shape [heads]
|
||||
block_size: Size of blocks for block-sparse attention
|
||||
kv_history: Optional key-value history from previous computations
|
||||
|
||||
Returns:
|
||||
output: Attention output
|
||||
kv: Updated key-value history
|
||||
"""
|
||||
d = q.shape[-1]
|
||||
e = v.shape[-1]
|
||||
|
||||
if ed.dim() == 1:
|
||||
ed = ed.view(1, -1, 1, 1)
|
||||
|
||||
# Split the computation into chunks for better parallelism
|
||||
m = 128 if d >= 128 else 64
|
||||
assert d % m == 0, f"Dimension d ({d}) must be divisible by m ({m})"
|
||||
arr = [m * i for i in range(d // m + 1)]
|
||||
if arr[-1] != d:
|
||||
arr.append(d)
|
||||
n = len(arr)
|
||||
output = 0
|
||||
|
||||
# Initialize or clone key-value history
|
||||
if kv_history is None:
|
||||
kv_history = torch.zeros(
|
||||
(q.shape[0], q.shape[1], d, e), dtype=torch.float32, device=q.device
|
||||
)
|
||||
else:
|
||||
kv_history = kv_history.clone().contiguous()
|
||||
|
||||
# Process each chunk and accumulate results
|
||||
for i in range(n - 1):
|
||||
s = arr[i]
|
||||
e = arr[i + 1]
|
||||
q1 = q[..., s:e]
|
||||
k1 = k[..., s:e]
|
||||
o, kv = lightning_attention_(q1, k1, v, ed, kv_history)
|
||||
output = output + o
|
||||
return output, kv
|
||||
|
||||
|
||||
@triton.jit
|
||||
def _linear_attn_decode_kernel(
|
||||
q_ptr,
|
||||
k_ptr,
|
||||
v_ptr,
|
||||
kv_cache_ptr,
|
||||
slope_rate,
|
||||
slot_idx,
|
||||
output_ptr,
|
||||
D: tl.constexpr,
|
||||
qkv_b_stride,
|
||||
qkv_h_stride,
|
||||
cache_b_stride,
|
||||
cache_h_stride,
|
||||
cache_d0_stride,
|
||||
cache_d1_stride,
|
||||
BLOCK_SIZE: tl.constexpr,
|
||||
):
|
||||
"""
|
||||
Kernel for linear attention decoding with KV cache.
|
||||
|
||||
This kernel computes attention for a single token using the KV cache.
|
||||
"""
|
||||
pid_b = tl.program_id(0) # batch index
|
||||
pid_h = tl.program_id(1) # head index
|
||||
pid_d = tl.program_id(2) # dimension block index
|
||||
|
||||
# Load slot index for the current batch
|
||||
slot_id = tl.load(slot_idx + pid_b)
|
||||
|
||||
# Skip if slot_id is -1 (padding)
|
||||
if slot_id == -1:
|
||||
return
|
||||
|
||||
batch_id = pid_b
|
||||
head_id = pid_h
|
||||
|
||||
# Load decay rate for the current head
|
||||
ratio = tl.load(slope_rate + pid_h)
|
||||
|
||||
# Calculate offsets for dimensions
|
||||
qk_d_offsets = tl.arange(0, D)
|
||||
v_d_offsets = tl.arange(0, BLOCK_SIZE) + pid_d * BLOCK_SIZE
|
||||
cache_d_offsets = (
|
||||
qk_d_offsets[:, None] * cache_d0_stride + v_d_offsets[None, :] * cache_d1_stride
|
||||
)
|
||||
|
||||
# Calculate offsets for the current batch and head
|
||||
q_offset = batch_id * qkv_b_stride + head_id * qkv_h_stride
|
||||
k_offset = batch_id * qkv_b_stride + head_id * qkv_h_stride
|
||||
v_offset = batch_id * qkv_b_stride + head_id * qkv_h_stride
|
||||
|
||||
cache_offset = slot_id * cache_b_stride + head_id * cache_h_stride
|
||||
|
||||
# Create masks for loading tensors
|
||||
qk_mask = qk_d_offsets < D
|
||||
v_mask = v_d_offsets < D
|
||||
|
||||
# Load query, key, and value tensors
|
||||
q = tl.load(q_ptr + q_offset + qk_d_offsets, mask=qk_mask, other=0.0)
|
||||
k = tl.load(k_ptr + k_offset + qk_d_offsets, mask=qk_mask, other=0.0)
|
||||
v = tl.load(v_ptr + v_offset + v_d_offsets, mask=v_mask, other=0.0)
|
||||
|
||||
# Compute key-value outer product
|
||||
kv_outer = k[:, None] * v[None, :]
|
||||
kv_mask = qk_mask[:, None] & v_mask[None, :]
|
||||
|
||||
# Apply decay to previous KV cache
|
||||
ratio = tl.exp(-ratio)
|
||||
kv_ptr = kv_cache_ptr + cache_offset + cache_d_offsets
|
||||
kv_cache_old = tl.load(kv_ptr, mask=kv_mask, other=0.0)
|
||||
kv_outer = kv_outer + ratio * kv_cache_old
|
||||
|
||||
# Compute attention output
|
||||
output = q[:, None].to(tl.float32) * kv_outer
|
||||
output = tl.sum(output, axis=0)
|
||||
|
||||
# Update KV cache and store output
|
||||
tl.store(kv_ptr, kv_outer, mask=kv_mask)
|
||||
tl.store(output_ptr + q_offset + v_d_offsets, output, mask=v_mask)
|
||||
|
||||
|
||||
def linear_decode_forward_triton(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
kv_caches: torch.Tensor,
|
||||
slope_rate: torch.Tensor,
|
||||
slot_idx: torch.Tensor,
|
||||
BLOCK_SIZE: int = 32,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Perform linear attention decoding using Triton kernels.
|
||||
|
||||
Args:
|
||||
q: Query tensor of shape [B, H, 1, D]
|
||||
k: Key tensor of shape [B, H, 1, D]
|
||||
v: Value tensor of shape [B, H, 1, D]
|
||||
kv_caches: Key-value cache tensor
|
||||
slope_rate: Decay rate tensor
|
||||
slot_idx: Slot indices for batches
|
||||
BLOCK_SIZE: Size of blocks for processing
|
||||
|
||||
Returns:
|
||||
output: Attention output tensor
|
||||
"""
|
||||
B, H, _, D = q.shape
|
||||
assert k.shape == (B, H, 1, D)
|
||||
assert v.shape == (B, H, 1, D)
|
||||
|
||||
# Initialize output tensor
|
||||
output = torch.empty_like(q)
|
||||
|
||||
# Set grid dimensions for the kernel
|
||||
grid = (B, H, D // BLOCK_SIZE)
|
||||
|
||||
# Calculate strides for tensors
|
||||
qkv_b_stride = q.stride(0)
|
||||
qkv_h_stride = q.stride(1)
|
||||
|
||||
cache_b_stride = kv_caches.stride(0)
|
||||
cache_h_stride = kv_caches.stride(1)
|
||||
cache_d0_stride = kv_caches.stride(2)
|
||||
cache_d1_stride = kv_caches.stride(3)
|
||||
|
||||
# Launch the kernel
|
||||
_linear_attn_decode_kernel[grid](
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
kv_caches,
|
||||
slope_rate,
|
||||
slot_idx,
|
||||
output,
|
||||
D,
|
||||
qkv_b_stride,
|
||||
qkv_h_stride,
|
||||
cache_b_stride,
|
||||
cache_h_stride,
|
||||
cache_d0_stride,
|
||||
cache_d1_stride,
|
||||
BLOCK_SIZE=BLOCK_SIZE,
|
||||
)
|
||||
|
||||
# Reshape output and return
|
||||
output = rearrange(output, "b h n d -> b n (h d)")
|
||||
return output.squeeze(1).contiguous()
|
||||
|
||||
|
||||
class BailingLinearKernel:
|
||||
"""
|
||||
Linear attention kernel implementation for Bailing models.
|
||||
|
||||
This class is adapted from MiniMaxText01LinearKernel in vllm:
|
||||
https://github.com/vllm-project/vllm/blob/a9138e85b14047e06300685b48e3485b995425fb/vllm/model_executor/models/minimax_text_01.py#L289
|
||||
|
||||
The implementation maintains the same functionality while being renamed to
|
||||
match our Bailing model naming convention.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def jit_linear_forward_prefix(
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
kv_caches: torch.Tensor,
|
||||
slope_rate: torch.Tensor,
|
||||
block_size: int,
|
||||
layer_idx: int = None,
|
||||
**kwargs,
|
||||
) -> torch.Tensor:
|
||||
|
||||
slope_rate = slope_rate.to(torch.float32)
|
||||
should_pad_dim = q.dim() == 3
|
||||
if should_pad_dim:
|
||||
q = q.unsqueeze(0)
|
||||
k = k.unsqueeze(0)
|
||||
v = v.unsqueeze(0)
|
||||
b, h, n, d = q.shape
|
||||
e = d
|
||||
kv_history = kv_caches.reshape(1, h, d, e).contiguous()
|
||||
output, kv_history = lightning_attention(
|
||||
q, k, v, slope_rate, block_size=block_size, kv_history=kv_history
|
||||
)
|
||||
kv_caches.copy_(kv_history[:, :, -1, :, :].reshape(h, d, e))
|
||||
assert output.shape[0] == 1, "batch size must be 1"
|
||||
return output.squeeze(0).transpose(0, 1).reshape([n, h * d]).contiguous()
|
||||
@@ -0,0 +1,378 @@
|
||||
import logging
|
||||
import math
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.layers.attention.hybrid_linear_attn_backend import MambaAttnBackendBase
|
||||
from sglang.srt.layers.attention.linear.lightning_attn import (
|
||||
BailingLinearKernel,
|
||||
linear_decode_forward_triton,
|
||||
)
|
||||
from sglang.srt.layers.attention.linear.linear_metadata import (
|
||||
BailingLinearMetadata,
|
||||
)
|
||||
from sglang.srt.layers.attention.linear.seg_la import SegLaMeta, seg_la_fwd
|
||||
from sglang.srt.layers.radix_attention import RadixAttention
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||
from sglang.srt.model_executor.model_runner import ModelRunner
|
||||
from sglang.srt.runtime_context import get_parallel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LightningAttentionBackend(MambaAttnBackendBase):
|
||||
"""
|
||||
Note about the init:
|
||||
- If no spec decoding
|
||||
- FlashAttentionBackend will be init once when the server starts.
|
||||
- If spec decoding
|
||||
- FlashAttentionBackend will be init once for the target worker
|
||||
- FlashAttentionMultiStepBackend will be once for the draft worker
|
||||
- It will spawn num_steps FlashAttentionBackend for the draft worker
|
||||
|
||||
Note about CUDA Graph:
|
||||
- We only support CUDA Graph for Decode (Normal Decode and Draft Decode) and Target Verify.
|
||||
- We don't support CUDA Graph for Extend and Draft Extend.
|
||||
- When server init, init_cuda_graph_state will be called first and then init_cuda_graph_capture will be called.
|
||||
- For each forward batch, init_replay_cuda_graph will be called first and then replay the graph.
|
||||
"""
|
||||
|
||||
def __init__(self, model_runner: ModelRunner):
|
||||
super().__init__(model_runner)
|
||||
# seg_la processes draft tokens as a chain -- it has no parent-indices
|
||||
# plumbing for tree-shaped drafts, so spec v2 tree verify (topk > 1) would
|
||||
# commit wrong mamba states silently. Fail fast instead of mis-decoding.
|
||||
if self.topk > 1:
|
||||
raise NotImplementedError(
|
||||
"Lightning (seg_la) linear-attention backend does not support "
|
||||
f"speculative decoding with topk > 1 (got topk={self.topk}); "
|
||||
"seg_la verifies a draft tree as a chain. Use "
|
||||
"--speculative-eagle-topk 1."
|
||||
)
|
||||
# lightning attn does not need conv cache, but to keep the interface for mamba cache
|
||||
self.conv_states_shape = (
|
||||
model_runner.req_to_token_pool.mamba_pool.mamba_cache.conv[0].shape
|
||||
)
|
||||
|
||||
assert not (
|
||||
model_runner.sliding_window_size is not None
|
||||
and model_runner.model_config.is_encoder_decoder
|
||||
), "Sliding window and cross attention are not supported together"
|
||||
|
||||
# extra metadata for handling speculative decoding topk > 1, extended draft decode and verify
|
||||
self.max_context_len = model_runner.model_config.context_len
|
||||
self.device = model_runner.device
|
||||
self.decode_cuda_graph_metadata = {}
|
||||
self.kv_cache_dtype = model_runner.kv_cache_dtype
|
||||
self.kv_cache_dtype_str = model_runner.server_args.kv_cache_dtype
|
||||
self.BLOCK = (
|
||||
model_runner.model_config.block
|
||||
if hasattr(model_runner.model_config, "block")
|
||||
else 256
|
||||
)
|
||||
total_num_heads = model_runner.model_config.hf_config.num_attention_heads
|
||||
num_hidden_layers = model_runner.model_config.hf_config.num_hidden_layers
|
||||
self.tp_slope = LightningAttentionBackend._build_slope_tensor(
|
||||
total_num_heads, num_hidden_layers, self.device
|
||||
)
|
||||
self.linear_backend = getattr(
|
||||
model_runner.model_config.hf_config, "linear_backend", "seg_la"
|
||||
)
|
||||
logger.info(
|
||||
f"linear_backend for linear attention in hybrid_linear_backend: {self.linear_backend}"
|
||||
)
|
||||
|
||||
def init_forward_metadata_out_graph(
|
||||
self,
|
||||
forward_batch: ForwardBatch,
|
||||
in_capture: bool = False,
|
||||
):
|
||||
# seq_lens_cpu is unused by the underlying _replay_metadata for
|
||||
# non-target-verify modes; pass it through for compatibility.
|
||||
bs = forward_batch.batch_size
|
||||
metadata = self._replay_metadata(
|
||||
bs,
|
||||
forward_batch.req_pool_indices,
|
||||
forward_batch.forward_mode,
|
||||
forward_batch.spec_info,
|
||||
forward_batch.seq_lens_cpu if not in_capture else None,
|
||||
)
|
||||
self.forward_metadata = BailingLinearMetadata.prepare_decode(
|
||||
metadata.query_start_loc,
|
||||
metadata.mamba_cache_indices,
|
||||
bs,
|
||||
forward_batch.seq_lens,
|
||||
)
|
||||
|
||||
def init_forward_metadata(self, forward_batch: ForwardBatch):
|
||||
metadata = self._forward_metadata(forward_batch)
|
||||
self.forward_metadata = BailingLinearMetadata.prepare_mixed(
|
||||
metadata.query_start_loc,
|
||||
metadata.mamba_cache_indices,
|
||||
forward_batch,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_slope_tensor(
|
||||
n_attention_heads: int, num_hidden_layers: int, device="cuda"
|
||||
):
|
||||
def get_slopes(n):
|
||||
def get_slopes_power_of_2(n):
|
||||
start = 2 ** (-(2 ** -(math.log2(n) - 3)))
|
||||
ratio = start
|
||||
return [start * ratio**i for i in range(n)]
|
||||
|
||||
if math.log2(n).is_integer():
|
||||
return get_slopes_power_of_2(n)
|
||||
else:
|
||||
closest_power_of_2 = 2 ** math.floor(math.log2(n))
|
||||
return (
|
||||
get_slopes_power_of_2(closest_power_of_2)
|
||||
+ get_slopes(2 * closest_power_of_2)[0::2][: n - closest_power_of_2]
|
||||
)
|
||||
|
||||
slopes = torch.tensor(
|
||||
get_slopes(n_attention_heads), dtype=torch.float32
|
||||
).reshape(n_attention_heads, 1, 1)
|
||||
|
||||
tp_heads = n_attention_heads // get_parallel().attn_tp_size
|
||||
tp_rank = get_parallel().attn_tp_rank
|
||||
if num_hidden_layers <= 1:
|
||||
slope_rate_list = [slopes * (1 + 1e-5)]
|
||||
else:
|
||||
slope_rate_list = [
|
||||
slopes * (1 - layer_id / (num_hidden_layers - 1) + 1e-5)
|
||||
for layer_id in range(num_hidden_layers)
|
||||
]
|
||||
|
||||
tp_slope = [
|
||||
slope_rate_list[layer_id][tp_rank * tp_heads : (tp_rank + 1) * tp_heads]
|
||||
.contiguous()
|
||||
.to(device)
|
||||
for layer_id in range(num_hidden_layers)
|
||||
]
|
||||
|
||||
return tp_slope
|
||||
|
||||
def _prefill_and_mix_infer(
|
||||
self,
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
kv_cache,
|
||||
state_indices_tensor,
|
||||
forward_batch,
|
||||
layer,
|
||||
metadata,
|
||||
):
|
||||
hidden = []
|
||||
for _prefill_idx in range(metadata.num_prefills):
|
||||
if _prefill_idx >= forward_batch.extend_start_loc.shape[0]:
|
||||
break
|
||||
if _prefill_idx >= state_indices_tensor.shape[0]:
|
||||
break
|
||||
|
||||
_start = forward_batch.extend_start_loc[_prefill_idx]
|
||||
|
||||
if _prefill_idx + 1 < forward_batch.extend_start_loc.shape[0]:
|
||||
_end = forward_batch.extend_start_loc[_prefill_idx + 1]
|
||||
else:
|
||||
if (
|
||||
forward_batch.extend_seq_lens is not None
|
||||
and _prefill_idx < forward_batch.extend_seq_lens.shape[0]
|
||||
and metadata.num_decodes > 0
|
||||
):
|
||||
seq_len = forward_batch.extend_seq_lens[_prefill_idx]
|
||||
_end = _start + seq_len
|
||||
else:
|
||||
_end = q.shape[0]
|
||||
|
||||
slot_id = state_indices_tensor[_prefill_idx]
|
||||
qs = q[_start:_end].transpose(0, 1).contiguous()
|
||||
ks = k[_start:_end].transpose(0, 1).contiguous()
|
||||
vs = v[_start:_end].transpose(0, 1).contiguous()
|
||||
slice_layer_cache = kv_cache[slot_id, ...]
|
||||
out_slice = BailingLinearKernel.jit_linear_forward_prefix(
|
||||
qs,
|
||||
ks,
|
||||
vs,
|
||||
slice_layer_cache,
|
||||
self.tp_slope[layer.layer_id],
|
||||
self.BLOCK,
|
||||
layer_idx=layer.layer_id,
|
||||
)
|
||||
hidden.append(out_slice.contiguous())
|
||||
if metadata.num_decodes > 0:
|
||||
hidden.append(
|
||||
self._decode_infer(
|
||||
q, k, v, kv_cache, state_indices_tensor, metadata, layer
|
||||
)
|
||||
)
|
||||
|
||||
if not hidden:
|
||||
return torch.empty((0, q.size(-1)), device=q.device, dtype=q.dtype)
|
||||
|
||||
hidden = torch.concat(hidden, dim=0).contiguous()
|
||||
return hidden
|
||||
|
||||
def _decode_infer(self, q, k, v, kv_cache, state_indices_tensor, metadata, layer):
|
||||
num_prefill_tokens = metadata.num_prefill_tokens
|
||||
num_prefills = metadata.num_prefills
|
||||
q = q[num_prefill_tokens:].unsqueeze(2).contiguous()
|
||||
k = k[num_prefill_tokens:].unsqueeze(2).contiguous()
|
||||
v = v[num_prefill_tokens:].unsqueeze(2).contiguous()
|
||||
slot_id = state_indices_tensor[num_prefills:]
|
||||
|
||||
assert slot_id.shape[0] == q.shape[0], (
|
||||
f"slot_id length {slot_id.shape[0]} does not match decode batch size {q.shape[0]}. "
|
||||
"This indicates a bug in the upstream logic that should be investigated."
|
||||
)
|
||||
hidden = linear_decode_forward_triton(
|
||||
q, k, v, kv_cache, self.tp_slope[layer.layer_id], slot_id, 32
|
||||
)
|
||||
return hidden
|
||||
|
||||
def _linear_attention_entry(
|
||||
self,
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
kv_cache,
|
||||
state_indices_tensor,
|
||||
metadata,
|
||||
layer,
|
||||
mask=None,
|
||||
temp_cache=None,
|
||||
intermediate_state_indices=None,
|
||||
):
|
||||
q_offsets = metadata.query_start_loc
|
||||
|
||||
seg_meta = SegLaMeta(
|
||||
batch_size=metadata.batch_size,
|
||||
q_offsets=metadata.query_start_loc,
|
||||
s_offsets=state_indices_tensor,
|
||||
q_lengths=q_offsets.diff(),
|
||||
s_scales=metadata.has_initial_states,
|
||||
max_q_length=None,
|
||||
mask=mask,
|
||||
)
|
||||
hidden = seg_la_fwd(
|
||||
q=q,
|
||||
k=k,
|
||||
v=v,
|
||||
s=kv_cache,
|
||||
decay_scales=self.tp_slope[layer.layer_id],
|
||||
meta=seg_meta,
|
||||
caches=temp_cache,
|
||||
cache_indices=intermediate_state_indices,
|
||||
decouple=True,
|
||||
)
|
||||
return hidden
|
||||
|
||||
def forward_extend(
|
||||
self,
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
layer: RadixAttention,
|
||||
forward_batch: ForwardBatch,
|
||||
save_kv_cache=True,
|
||||
**kwargs,
|
||||
):
|
||||
layer_id = layer.layer_id if layer else kwargs["layer_id"]
|
||||
|
||||
metadata = self.forward_metadata
|
||||
|
||||
if self.kv_cache_dtype_str != "auto" and layer.k_scale is not None:
|
||||
q = q.to(self.kv_cache_dtype)
|
||||
|
||||
cache_indices = self.forward_metadata.mamba_cache_indices
|
||||
mamba_cache_params = self.req_to_token_pool.mamba2_layer_cache(layer_id)
|
||||
ssm_states = mamba_cache_params.temporal
|
||||
if self.linear_backend == "minimax":
|
||||
o = self._prefill_and_mix_infer(
|
||||
q.contiguous().view(-1, layer.tp_q_head_num, layer.head_dim),
|
||||
k,
|
||||
v,
|
||||
ssm_states,
|
||||
cache_indices,
|
||||
forward_batch,
|
||||
layer,
|
||||
metadata,
|
||||
)
|
||||
elif self.linear_backend == "seg_la":
|
||||
intermediate_state_indices = (
|
||||
torch.arange(
|
||||
cache_indices.shape[0],
|
||||
dtype=torch.int32,
|
||||
device=cache_indices.device,
|
||||
)
|
||||
if forward_batch.forward_mode.is_target_verify()
|
||||
else None
|
||||
)
|
||||
o = self._linear_attention_entry(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
ssm_states,
|
||||
cache_indices,
|
||||
metadata,
|
||||
layer,
|
||||
temp_cache=(
|
||||
mamba_cache_params.intermediate_ssm
|
||||
if forward_batch.forward_mode.is_target_verify()
|
||||
else None
|
||||
),
|
||||
intermediate_state_indices=intermediate_state_indices,
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"linear backend: {self.linear_backend} is not support for now"
|
||||
)
|
||||
|
||||
if (
|
||||
not forward_batch.forward_mode.is_target_verify()
|
||||
and forward_batch.mamba_track_mask is not None
|
||||
):
|
||||
# save mamba cache for extra buffer
|
||||
mamba_track_mask = forward_batch.mamba_track_mask
|
||||
mamba_track_indices = forward_batch.mamba_track_indices
|
||||
dst_masked = mamba_track_indices[mamba_track_mask]
|
||||
src_masked = metadata.mamba_cache_indices[mamba_track_mask]
|
||||
ssm_states[dst_masked] = ssm_states[src_masked]
|
||||
|
||||
return o.view(-1, layer.tp_q_head_num * layer.v_head_dim)
|
||||
|
||||
def forward_decode(
|
||||
self,
|
||||
q: torch.Tensor,
|
||||
k: torch.Tensor,
|
||||
v: torch.Tensor,
|
||||
layer: RadixAttention,
|
||||
forward_batch: ForwardBatch,
|
||||
save_kv_cache=True,
|
||||
**kwargs,
|
||||
) -> torch.Tensor:
|
||||
layer_id = layer.layer_id if layer else kwargs["layer_id"]
|
||||
|
||||
# Use precomputed metadata across all layers
|
||||
metadata = self.forward_metadata
|
||||
|
||||
if self.kv_cache_dtype_str != "auto":
|
||||
q = q.to(self.kv_cache_dtype)
|
||||
|
||||
# Do linear attention
|
||||
cache_indices = self.forward_metadata.mamba_cache_indices
|
||||
mamba_cache_params = self.req_to_token_pool.mamba2_layer_cache(layer_id)
|
||||
ssm_states = mamba_cache_params.temporal
|
||||
if self.linear_backend == "minimax":
|
||||
o = self._decode_infer(q, k, v, ssm_states, cache_indices, metadata, layer)
|
||||
elif self.linear_backend == "seg_la":
|
||||
o = self._linear_attention_entry(
|
||||
q, k, v, ssm_states, cache_indices, metadata, layer
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"linear backend: {self.linear_backend} is not support for now"
|
||||
)
|
||||
return o.view(-1, layer.tp_q_head_num * layer.v_head_dim)
|
||||
@@ -0,0 +1,70 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.layers.attention.mamba.mamba2_metadata import ForwardMetadata
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class BailingLinearMetadata(ForwardMetadata):
|
||||
num_prefills: int
|
||||
num_prefill_tokens: int
|
||||
num_decodes: int
|
||||
batch_size: int
|
||||
has_initial_states: torch.Tensor
|
||||
q_lengths: torch.Tensor
|
||||
|
||||
@staticmethod
|
||||
def prepare_decode(
|
||||
query_start_loc: torch.Tensor,
|
||||
mamba_cache_indices: torch.Tensor,
|
||||
bs: int,
|
||||
seq_lens: torch.Tensor,
|
||||
) -> "BailingLinearMetadata":
|
||||
"""This path is run during CUDA graph capture, i.e. decode only, so `num_prefills` is 0"""
|
||||
return BailingLinearMetadata(
|
||||
batch_size=bs,
|
||||
query_start_loc=query_start_loc,
|
||||
mamba_cache_indices=mamba_cache_indices,
|
||||
num_decodes=seq_lens.shape[0],
|
||||
num_prefills=0,
|
||||
num_prefill_tokens=0,
|
||||
has_initial_states=torch.ones_like(seq_lens),
|
||||
q_lengths=query_start_loc.diff(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def prepare_mixed(
|
||||
cls,
|
||||
query_start_loc: torch.Tensor,
|
||||
mamba_cache_indices: torch.Tensor,
|
||||
forward_batch: ForwardBatch,
|
||||
) -> "BailingLinearMetadata":
|
||||
"""This path cannot run with CUDA graph, as it contains extend requests."""
|
||||
if forward_batch.extend_num_tokens is None:
|
||||
return cls.prepare_decode(
|
||||
query_start_loc=query_start_loc,
|
||||
mamba_cache_indices=mamba_cache_indices,
|
||||
bs=forward_batch.batch_size,
|
||||
seq_lens=forward_batch.seq_lens,
|
||||
)
|
||||
num_prefills = len(forward_batch.extend_seq_lens)
|
||||
num_prefill_tokens = forward_batch.extend_num_tokens
|
||||
num_decodes = len(forward_batch.seq_lens) - num_prefills
|
||||
context_lens_tensor = forward_batch.extend_prefix_lens
|
||||
assert context_lens_tensor is not None
|
||||
has_initial_states = context_lens_tensor > 0
|
||||
|
||||
query_start_loc = query_start_loc[: num_prefills + 1]
|
||||
|
||||
return BailingLinearMetadata(
|
||||
batch_size=forward_batch.batch_size,
|
||||
query_start_loc=query_start_loc,
|
||||
mamba_cache_indices=mamba_cache_indices,
|
||||
num_prefills=num_prefills,
|
||||
num_prefill_tokens=num_prefill_tokens,
|
||||
num_decodes=num_decodes,
|
||||
has_initial_states=has_initial_states,
|
||||
q_lengths=query_start_loc.diff(),
|
||||
)
|
||||
@@ -0,0 +1,910 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Copyright (c) Ant Financial Service Group and its affiliates.
|
||||
"""
|
||||
|
||||
# Copied from https://code.alipay.com/pia/PainlessInferenceAcceleration/blob/v0.0.6/flood/flood/ops/seg_la.py
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
import triton
|
||||
import triton.language as tl
|
||||
|
||||
|
||||
# arg `meta` of `seg_la_fwd` is SegLaMeta
|
||||
@dataclass
|
||||
class SegLaMeta:
|
||||
batch_size: int # batch size, num of requests
|
||||
max_q_length: int # max(seq_lens)
|
||||
q_offsets: torch.Tensor # [bs+1], query_start_locations,
|
||||
s_offsets: torch.Tensor # [bs], slot_ids
|
||||
q_lengths: torch.Tensor # [bs], query length
|
||||
s_scales: torch.Tensor # [bs], prefill = 0, decode = 1
|
||||
s_offsets_stride: int = 0
|
||||
q_offsets_stride: int = 0
|
||||
s_scales_stride: int = 0
|
||||
decay_scales_stride: int = 0
|
||||
mask: Optional[torch.Tensor] = None # Currently not supported
|
||||
|
||||
|
||||
# fused
|
||||
@triton.jit
|
||||
def seg_la_kernel(
|
||||
Q,
|
||||
K,
|
||||
V,
|
||||
S,
|
||||
Out,
|
||||
softmax_scale,
|
||||
stride_q,
|
||||
stride_k,
|
||||
stride_v,
|
||||
stride_s,
|
||||
stride_o,
|
||||
s_offsets,
|
||||
q_offsets,
|
||||
q_lengths,
|
||||
s_scales,
|
||||
decay_scales,
|
||||
HEAD_DIM: tl.constexpr,
|
||||
SPLIT_DIM: tl.constexpr,
|
||||
BLOCK: tl.constexpr,
|
||||
EVEN: tl.constexpr,
|
||||
DECOUPLE: tl.constexpr,
|
||||
):
|
||||
bid = tl.program_id(0)
|
||||
hid = tl.program_id(1)
|
||||
sid = tl.program_id(2)
|
||||
|
||||
# s_scale is 0 (prefill) or 1 (decode)
|
||||
s_scale = tl.load(s_scales + bid)
|
||||
q_length = tl.load(q_lengths + bid)
|
||||
q_offset = tl.load(q_offsets + bid)
|
||||
s_offset = tl.load(s_offsets + bid)
|
||||
decay_scale = -tl.load(decay_scales + hid)
|
||||
|
||||
offs_b = tl.arange(0, BLOCK)
|
||||
offs_d = tl.arange(0, HEAD_DIM)
|
||||
offs_s = tl.arange(0, SPLIT_DIM)
|
||||
|
||||
if s_offset == -1:
|
||||
return
|
||||
|
||||
q_ptrs = (
|
||||
Q
|
||||
+ q_offset * stride_q
|
||||
+ hid * HEAD_DIM
|
||||
+ (offs_b[:, None] * stride_q + offs_d[None, :])
|
||||
)
|
||||
k_ptrs = (
|
||||
K
|
||||
+ q_offset * stride_k
|
||||
+ hid * HEAD_DIM
|
||||
+ (offs_b[:, None] * stride_k + offs_d[None, :])
|
||||
)
|
||||
v_ptrs = (
|
||||
V
|
||||
+ q_offset * stride_v
|
||||
+ hid * HEAD_DIM
|
||||
+ sid * SPLIT_DIM
|
||||
+ (offs_b[:, None] * stride_v + offs_s[None, :])
|
||||
)
|
||||
out_ptrs = (
|
||||
Out
|
||||
+ q_offset * stride_o
|
||||
+ hid * HEAD_DIM
|
||||
+ sid * SPLIT_DIM
|
||||
+ (offs_b[:, None] * stride_o + offs_s[None, :])
|
||||
)
|
||||
s_ptrs = (
|
||||
S
|
||||
+ s_offset * stride_s
|
||||
+ hid * HEAD_DIM * HEAD_DIM
|
||||
+ sid * SPLIT_DIM
|
||||
+ (offs_d[:, None] * HEAD_DIM + offs_s[None, :])
|
||||
)
|
||||
state = tl.load(s_ptrs, mask=s_scale > 0).to(tl.float32)
|
||||
|
||||
if BLOCK > 1:
|
||||
for n in range(0, q_length, BLOCK):
|
||||
n = tl.multiple_of(n, BLOCK)
|
||||
|
||||
if EVEN:
|
||||
q = tl.load(q_ptrs + n * stride_q).to(tl.float32)
|
||||
k = tl.trans(tl.load(k_ptrs + n * stride_k)).to(tl.float32)
|
||||
v = tl.load(v_ptrs + n * stride_k).to(tl.float32)
|
||||
else:
|
||||
q = tl.load(
|
||||
q_ptrs + n * stride_q,
|
||||
mask=(n + offs_b)[:, None] < q_length,
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
k = tl.trans(
|
||||
tl.load(
|
||||
k_ptrs + n * stride_k,
|
||||
mask=(n + offs_b)[:, None] < q_length,
|
||||
other=0.0,
|
||||
)
|
||||
).to(tl.float32)
|
||||
v = tl.load(
|
||||
v_ptrs + n * stride_k,
|
||||
mask=(n + offs_b)[:, None] < q_length,
|
||||
other=0.0,
|
||||
).to(tl.float32)
|
||||
|
||||
if DECOUPLE:
|
||||
# only work with small scales
|
||||
if EVEN:
|
||||
b = BLOCK
|
||||
else:
|
||||
b = min(BLOCK, q_length - n)
|
||||
b_offs = b - 1 - offs_b
|
||||
|
||||
edb = tl.exp(decay_scale * b_offs)
|
||||
decays = tl.where(b_offs >= 0, edb, 0)
|
||||
inv_decays = tl.where(b_offs >= 0, 1 / edb, 0)
|
||||
|
||||
q = q * inv_decays[:, None]
|
||||
k = k * decays[None, :]
|
||||
qk = tl.dot(q, k) * softmax_scale
|
||||
qk = tl.where(offs_b[None, :] <= offs_b[:, None], qk, 0.0)
|
||||
o = tl.dot(qk, v)
|
||||
|
||||
block_decay = tl.exp(decay_scale * b)
|
||||
block_decay_plus = block_decay * softmax_scale
|
||||
o = tl.dot(q, state) * block_decay_plus + o
|
||||
|
||||
state = state * block_decay + tl.dot(k, v)
|
||||
else:
|
||||
|
||||
qk = tl.dot(q, k) * softmax_scale
|
||||
decays = tl.exp(decay_scale * (offs_b[:, None] - offs_b[None, :]))
|
||||
decays = tl.where(offs_b[None, :] <= offs_b[:, None], decays, 0.0)
|
||||
qk *= decays
|
||||
o = tl.dot(qk, v)
|
||||
|
||||
decay_arr = tl.exp(decay_scale * (offs_b[:, None] + 1)) * softmax_scale
|
||||
o = tl.dot(q * decay_arr, state, acc=o)
|
||||
|
||||
if EVEN:
|
||||
b = BLOCK
|
||||
else:
|
||||
b = min(BLOCK, q_length - n)
|
||||
b_offs = b - 1 - offs_b
|
||||
b_offs = tl.where(b_offs >= 0, b_offs, 10000)
|
||||
decays = tl.exp(decay_scale * b_offs)
|
||||
block_decay = tl.exp(decay_scale * b)
|
||||
state = state * block_decay + tl.dot(k * decays[None, :], v)
|
||||
|
||||
if EVEN:
|
||||
tl.store(out_ptrs + n * stride_o, o.to(Out.dtype.element_ty))
|
||||
else:
|
||||
tl.store(
|
||||
out_ptrs + n * stride_o,
|
||||
o.to(Out.dtype.element_ty),
|
||||
mask=(n + offs_b)[:, None] < q_length,
|
||||
)
|
||||
|
||||
tl.store(s_ptrs, state.to(S.dtype.element_ty))
|
||||
|
||||
else:
|
||||
q = tl.trans(tl.load(q_ptrs)).to(tl.float32) * softmax_scale
|
||||
k = tl.trans(tl.load(k_ptrs)).to(tl.float32)
|
||||
v = tl.load(v_ptrs).to(tl.float32)
|
||||
state = state * tl.exp(decay_scale) + k * v
|
||||
|
||||
o = tl.sum(q * state, axis=0, keep_dims=True)
|
||||
|
||||
tl.store(out_ptrs, o.to(Out.dtype.element_ty))
|
||||
|
||||
tl.store(s_ptrs, state.to(S.dtype.element_ty))
|
||||
|
||||
|
||||
# used for prefilling
|
||||
@triton.jit
|
||||
def seg_la_p_kernel(
|
||||
Q,
|
||||
K,
|
||||
V,
|
||||
S,
|
||||
Out,
|
||||
softmax_scale,
|
||||
stride_q,
|
||||
stride_k,
|
||||
stride_v,
|
||||
stride_s,
|
||||
stride_o,
|
||||
s_offsets,
|
||||
q_offsets,
|
||||
q_lengths,
|
||||
s_scales,
|
||||
decay_scales,
|
||||
HEAD_DIM: tl.constexpr,
|
||||
K_SPLIT_DIM: tl.constexpr,
|
||||
V_SPLIT_DIM: tl.constexpr,
|
||||
BLOCK: tl.constexpr,
|
||||
EVEN: tl.constexpr,
|
||||
):
|
||||
bid = tl.program_id(0)
|
||||
hid = tl.program_id(1)
|
||||
kvid = tl.program_id(2)
|
||||
N = HEAD_DIM // V_SPLIT_DIM
|
||||
kid = kvid // N
|
||||
vid = kvid % N
|
||||
H = tl.num_programs(1)
|
||||
|
||||
# s_scale is 0 (first prefill chunk) or 1 (next prefill chunk)
|
||||
s_scale = tl.load(s_scales + bid)
|
||||
q_length = tl.load(q_lengths + bid)
|
||||
q_offset = tl.load(q_offsets + bid)
|
||||
s_offset = tl.load(s_offsets + bid)
|
||||
decay_scale = -tl.load(decay_scales + hid)
|
||||
|
||||
offs_b = tl.arange(0, BLOCK)
|
||||
offs_k = tl.arange(0, K_SPLIT_DIM)
|
||||
offs_v = tl.arange(0, V_SPLIT_DIM)
|
||||
|
||||
if s_offset == -1:
|
||||
return
|
||||
|
||||
q_ptrs = (
|
||||
Q
|
||||
+ q_offset * stride_q
|
||||
+ hid * HEAD_DIM
|
||||
+ kid * K_SPLIT_DIM
|
||||
+ (offs_b[:, None] * stride_q + offs_k[None, :])
|
||||
)
|
||||
k_ptrs = (
|
||||
K
|
||||
+ q_offset * stride_k
|
||||
+ hid * HEAD_DIM
|
||||
+ kid * K_SPLIT_DIM
|
||||
+ (offs_b[:, None] * stride_k + offs_k[None, :])
|
||||
)
|
||||
v_ptrs = (
|
||||
V
|
||||
+ q_offset * stride_v
|
||||
+ hid * HEAD_DIM
|
||||
+ vid * V_SPLIT_DIM
|
||||
+ (offs_b[:, None] * stride_v + offs_v[None, :])
|
||||
)
|
||||
# (num_dim_block, length, qo_heads, d)
|
||||
out_ptrs = (
|
||||
Out
|
||||
+ kid * stride_o
|
||||
+ q_offset * HEAD_DIM * H
|
||||
+ hid * HEAD_DIM
|
||||
+ vid * V_SPLIT_DIM
|
||||
+ (offs_b[:, None] * H * HEAD_DIM + offs_v[None, :])
|
||||
)
|
||||
s_ptrs = (
|
||||
S
|
||||
+ s_offset * stride_s
|
||||
+ hid * HEAD_DIM * HEAD_DIM
|
||||
+ kid * HEAD_DIM * K_SPLIT_DIM
|
||||
+ vid * V_SPLIT_DIM
|
||||
+ (offs_k[:, None] * HEAD_DIM + offs_v[None, :])
|
||||
)
|
||||
state = tl.load(s_ptrs, mask=s_scale > 0).to(tl.float32)
|
||||
|
||||
for n in range(0, q_length, BLOCK):
|
||||
n = tl.multiple_of(n, BLOCK)
|
||||
|
||||
if EVEN:
|
||||
q = tl.load(q_ptrs + n * stride_q).to(tl.float32)
|
||||
k = tl.trans(tl.load(k_ptrs + n * stride_k)).to(tl.float32)
|
||||
v = tl.load(v_ptrs + n * stride_v).to(tl.float32)
|
||||
b = BLOCK
|
||||
b_offs = b - 1 - offs_b
|
||||
decays = tl.exp(decay_scale * b_offs)
|
||||
inv_decays = 1 / decays
|
||||
else:
|
||||
q = tl.load(
|
||||
q_ptrs + n * stride_q, mask=(n + offs_b)[:, None] < q_length, other=0.0
|
||||
).to(tl.float32)
|
||||
k = tl.trans(
|
||||
tl.load(
|
||||
k_ptrs + n * stride_k,
|
||||
mask=(n + offs_b)[:, None] < q_length,
|
||||
other=0.0,
|
||||
)
|
||||
).to(tl.float32)
|
||||
v = tl.load(
|
||||
v_ptrs + n * stride_v, mask=(n + offs_b)[:, None] < q_length, other=0.0
|
||||
).to(tl.float32)
|
||||
b = min(BLOCK, q_length - n)
|
||||
b_offs = b - 1 - offs_b
|
||||
block_decays = tl.exp(decay_scale * b_offs)
|
||||
decays = tl.where(b_offs >= 0, block_decays, 0)
|
||||
inv_decays = tl.where(b_offs >= 0, 1 / block_decays, 0)
|
||||
|
||||
q = q * inv_decays[:, None]
|
||||
k = k * decays[None, :]
|
||||
qk = tl.dot(q, k) * softmax_scale
|
||||
qk = tl.where(offs_b[None, :] <= offs_b[:, None], qk, 0.0)
|
||||
o = tl.dot(qk, v)
|
||||
|
||||
block_decay = tl.exp(decay_scale * b)
|
||||
o = tl.dot(q, state) * block_decay * softmax_scale + o
|
||||
|
||||
state = state * block_decay + tl.dot(k, v)
|
||||
|
||||
if EVEN:
|
||||
tl.store(out_ptrs + n * H * HEAD_DIM, o.to(Out.dtype.element_ty))
|
||||
else:
|
||||
tl.store(
|
||||
out_ptrs + n * H * HEAD_DIM,
|
||||
o.to(Out.dtype.element_ty),
|
||||
mask=(n + offs_b)[:, None] < q_length,
|
||||
)
|
||||
|
||||
tl.store(s_ptrs, state.to(S.dtype.element_ty))
|
||||
|
||||
|
||||
# used for speculative
|
||||
@triton.jit
|
||||
def seg_la_s_kernel(
|
||||
Q,
|
||||
K,
|
||||
V,
|
||||
S,
|
||||
Out,
|
||||
Mask,
|
||||
softmax_scale,
|
||||
stride_q,
|
||||
stride_k,
|
||||
stride_v,
|
||||
stride_s,
|
||||
stride_o,
|
||||
s_offsets,
|
||||
q_offsets,
|
||||
q_lengths,
|
||||
s_scales,
|
||||
decay_scales,
|
||||
HEAD_DIM: tl.constexpr,
|
||||
K_SPLIT_DIM: tl.constexpr,
|
||||
V_SPLIT_DIM: tl.constexpr,
|
||||
BLOCK: tl.constexpr,
|
||||
EVEN: tl.constexpr,
|
||||
):
|
||||
bid = tl.program_id(0)
|
||||
hid = tl.program_id(1)
|
||||
kvid = tl.program_id(2)
|
||||
N = HEAD_DIM // V_SPLIT_DIM
|
||||
kid = kvid // N
|
||||
vid = kvid % N
|
||||
H = tl.num_programs(1)
|
||||
|
||||
# s_scale is 0 (first prefill chunk) or 1 (next prefill chunk)
|
||||
s_scale = tl.load(s_scales + bid)
|
||||
q_length = tl.load(q_lengths + bid)
|
||||
q_offset = tl.load(q_offsets + bid)
|
||||
s_offset = tl.load(s_offsets + bid)
|
||||
decay_scale = -tl.load(decay_scales + hid)
|
||||
|
||||
offs_b = tl.arange(0, BLOCK)
|
||||
offs_k = tl.arange(0, K_SPLIT_DIM)
|
||||
offs_v = tl.arange(0, V_SPLIT_DIM)
|
||||
|
||||
if s_offset == -1:
|
||||
return
|
||||
|
||||
q_ptrs = (
|
||||
Q
|
||||
+ q_offset * stride_q
|
||||
+ hid * HEAD_DIM
|
||||
+ kid * K_SPLIT_DIM
|
||||
+ (offs_b[:, None] * stride_q + offs_k[None, :])
|
||||
)
|
||||
k_ptrs = (
|
||||
K
|
||||
+ q_offset * stride_k
|
||||
+ hid * HEAD_DIM
|
||||
+ kid * K_SPLIT_DIM
|
||||
+ (offs_b[:, None] * stride_k + offs_k[None, :])
|
||||
)
|
||||
v_ptrs = (
|
||||
V
|
||||
+ q_offset * stride_v
|
||||
+ hid * HEAD_DIM
|
||||
+ vid * V_SPLIT_DIM
|
||||
+ (offs_b[:, None] * stride_v + offs_v[None, :])
|
||||
)
|
||||
# (num_dim_block, length, qo_heads, d)
|
||||
out_ptrs = (
|
||||
Out
|
||||
+ kid * stride_o
|
||||
+ q_offset * HEAD_DIM * H
|
||||
+ hid * HEAD_DIM
|
||||
+ vid * V_SPLIT_DIM
|
||||
+ (offs_b[:, None] * H * HEAD_DIM + offs_v[None, :])
|
||||
)
|
||||
s_ptrs = (
|
||||
S
|
||||
+ s_offset * stride_s
|
||||
+ hid * HEAD_DIM * HEAD_DIM
|
||||
+ kid * HEAD_DIM * K_SPLIT_DIM
|
||||
+ vid * V_SPLIT_DIM
|
||||
+ (offs_k[:, None] * HEAD_DIM + offs_v[None, :])
|
||||
)
|
||||
state = tl.load(s_ptrs, mask=s_scale > 0).to(tl.float32)
|
||||
|
||||
if EVEN:
|
||||
q = tl.load(q_ptrs).to(tl.float32)
|
||||
k = tl.trans(tl.load(k_ptrs)).to(tl.float32)
|
||||
v = tl.load(v_ptrs).to(tl.float32)
|
||||
mask = tl.load(
|
||||
Mask
|
||||
+ bid * BLOCK * BLOCK
|
||||
+ tl.arange(0, BLOCK)[:, None] * BLOCK
|
||||
+ tl.arange(0, BLOCK)[None, :]
|
||||
).to(tl.int32)
|
||||
positions = tl.sum(mask, 1) - 1
|
||||
max_pos = tl.max(positions)
|
||||
b_offs = max_pos - positions
|
||||
else:
|
||||
q = tl.load(q_ptrs, mask=offs_b[:, None] < q_length).to(tl.float32)
|
||||
k = tl.trans(tl.load(k_ptrs, mask=offs_b[:, None] < q_length)).to(tl.float32)
|
||||
v = tl.load(v_ptrs, mask=offs_b[:, None] < q_length).to(tl.float32)
|
||||
mask = tl.load(
|
||||
Mask
|
||||
+ bid * q_length * q_length
|
||||
+ tl.arange(0, BLOCK)[:, None] * q_length
|
||||
+ tl.arange(0, BLOCK)[None, :],
|
||||
mask=(tl.arange(0, BLOCK)[:, None] < q_length)
|
||||
& (tl.arange(0, BLOCK)[None, :] < q_length),
|
||||
).to(tl.int32)
|
||||
positions = tl.sum(mask, 1) - 1
|
||||
max_pos = tl.max(positions)
|
||||
b_offs = max_pos - positions
|
||||
|
||||
decays = tl.exp(decay_scale * b_offs)
|
||||
inv_decays = 1 / decays
|
||||
|
||||
q = q * inv_decays[:, None]
|
||||
k = k * decays[None, :]
|
||||
qk = tl.dot(q, k) * softmax_scale
|
||||
qk = qk * mask.to(tl.float32)
|
||||
o = tl.dot(qk, v)
|
||||
|
||||
block_decay = tl.exp(decay_scale * (max_pos + 1))
|
||||
o = tl.dot(q, state) * block_decay * softmax_scale + o
|
||||
|
||||
if EVEN:
|
||||
tl.store(out_ptrs, o.to(Out.dtype.element_ty))
|
||||
else:
|
||||
tl.store(out_ptrs, o.to(Out.dtype.element_ty), mask=offs_b[:, None] < q_length)
|
||||
|
||||
|
||||
# used for decode
|
||||
@triton.jit
|
||||
def seg_la_d_kernel(
|
||||
Q,
|
||||
K,
|
||||
V,
|
||||
S,
|
||||
Out,
|
||||
softmax_scale,
|
||||
stride_q,
|
||||
stride_k,
|
||||
stride_v,
|
||||
stride_s,
|
||||
stride_o,
|
||||
s_offsets,
|
||||
decay_scales,
|
||||
HEAD_DIM: tl.constexpr,
|
||||
K_SPLIT_DIM: tl.constexpr,
|
||||
V_SPLIT_DIM: tl.constexpr,
|
||||
):
|
||||
bid = tl.program_id(0)
|
||||
hid = tl.program_id(1)
|
||||
kvid = tl.program_id(2)
|
||||
N = HEAD_DIM // V_SPLIT_DIM
|
||||
kid = kvid // N
|
||||
vid = kvid % N
|
||||
H = tl.num_programs(1)
|
||||
|
||||
# s_scale is 0 (first prefill chunk) or 1 (next prefill chunk)
|
||||
s_offset = tl.load(s_offsets + bid)
|
||||
if s_offset == -1:
|
||||
return
|
||||
|
||||
decay_scale = -tl.load(decay_scales + hid)
|
||||
|
||||
offs_k = tl.arange(0, K_SPLIT_DIM)
|
||||
offs_v = tl.arange(0, V_SPLIT_DIM)
|
||||
|
||||
q_ptrs = Q + bid * stride_q + hid * HEAD_DIM + kid * K_SPLIT_DIM + (offs_k)
|
||||
k_ptrs = K + bid * stride_k + hid * HEAD_DIM + kid * K_SPLIT_DIM + (offs_k)
|
||||
v_ptrs = V + bid * stride_v + hid * HEAD_DIM + vid * V_SPLIT_DIM + (offs_v)
|
||||
# (num_dim_block, length, qo_heads, d)
|
||||
out_ptrs = (
|
||||
Out
|
||||
+ kid * stride_o
|
||||
+ bid * H * HEAD_DIM
|
||||
+ hid * HEAD_DIM
|
||||
+ vid * V_SPLIT_DIM
|
||||
+ (offs_v)
|
||||
)
|
||||
s_ptrs = (
|
||||
S
|
||||
+ s_offset * stride_s
|
||||
+ hid * HEAD_DIM * HEAD_DIM
|
||||
+ kid * HEAD_DIM * K_SPLIT_DIM
|
||||
+ vid * V_SPLIT_DIM
|
||||
+ (offs_k[:, None] * HEAD_DIM + offs_v[None, :])
|
||||
)
|
||||
state = tl.load(s_ptrs).to(tl.float32)
|
||||
|
||||
k = tl.load(k_ptrs).to(tl.float32)
|
||||
v = tl.load(v_ptrs).to(tl.float32)
|
||||
q = tl.load(q_ptrs).to(tl.float32) * softmax_scale
|
||||
|
||||
state = state * tl.exp(decay_scale) + k[:, None] * v
|
||||
o = tl.sum(q[:, None] * state, axis=0)
|
||||
|
||||
tl.store(out_ptrs, o.to(Out.dtype.element_ty))
|
||||
tl.store(s_ptrs, state.to(S.dtype.element_ty))
|
||||
|
||||
|
||||
# used for MTP with only spec-topk=1.
|
||||
@triton.jit
|
||||
def seg_la_mtp_kernel(
|
||||
Q,
|
||||
K,
|
||||
V,
|
||||
S,
|
||||
CACHES,
|
||||
Out,
|
||||
softmax_scale,
|
||||
stride_q,
|
||||
stride_k,
|
||||
stride_v,
|
||||
stride_s,
|
||||
stride_c,
|
||||
stride_o,
|
||||
s_offsets,
|
||||
cache_indices,
|
||||
decay_scales,
|
||||
step,
|
||||
HEAD_DIM: tl.constexpr,
|
||||
K_SPLIT_DIM: tl.constexpr,
|
||||
V_SPLIT_DIM: tl.constexpr,
|
||||
):
|
||||
bid = tl.program_id(0)
|
||||
hid = tl.program_id(1)
|
||||
kvid = tl.program_id(2)
|
||||
N = HEAD_DIM // V_SPLIT_DIM
|
||||
kid = kvid // N
|
||||
vid = kvid % N
|
||||
H = tl.num_programs(1)
|
||||
|
||||
s_offset = tl.load(s_offsets + bid)
|
||||
if s_offset == -1:
|
||||
return
|
||||
|
||||
decay_scale = tl.exp(-tl.load(decay_scales + hid))
|
||||
|
||||
offs_k = tl.arange(0, K_SPLIT_DIM)
|
||||
offs_v = tl.arange(0, V_SPLIT_DIM)
|
||||
|
||||
# (length, qo_heads, d)
|
||||
q_ptrs = Q + bid * step * stride_q + hid * HEAD_DIM + kid * K_SPLIT_DIM + (offs_k)
|
||||
k_ptrs = K + bid * step * stride_k + hid * HEAD_DIM + kid * K_SPLIT_DIM + (offs_k)
|
||||
v_ptrs = V + bid * step * stride_v + hid * HEAD_DIM + vid * V_SPLIT_DIM + (offs_v)
|
||||
# (num_dim_block, length, qo_heads, d)
|
||||
out_ptrs = (
|
||||
Out
|
||||
+ kid * stride_o
|
||||
+ bid * step * H * HEAD_DIM
|
||||
+ hid * HEAD_DIM
|
||||
+ vid * V_SPLIT_DIM
|
||||
+ (offs_v)
|
||||
)
|
||||
# (bs, qo_heads, d, d)
|
||||
s_ptrs = (
|
||||
S
|
||||
+ s_offset * stride_s
|
||||
+ hid * HEAD_DIM * HEAD_DIM
|
||||
+ kid * HEAD_DIM * K_SPLIT_DIM
|
||||
+ vid * V_SPLIT_DIM
|
||||
+ (offs_k[:, None] * HEAD_DIM + offs_v[None, :])
|
||||
)
|
||||
state = tl.load(s_ptrs).to(tl.float32)
|
||||
# (bs, step, kv_heads, d, d)
|
||||
cache_indices = tl.load(cache_indices + bid)
|
||||
c_ptrs = (
|
||||
CACHES
|
||||
+ cache_indices * stride_c
|
||||
+ hid * HEAD_DIM * HEAD_DIM
|
||||
+ kid * HEAD_DIM * K_SPLIT_DIM
|
||||
+ vid * V_SPLIT_DIM
|
||||
+ (offs_k[:, None] * HEAD_DIM + offs_v[None, :])
|
||||
)
|
||||
|
||||
for i in range(step):
|
||||
q = tl.load(q_ptrs).to(tl.float32) * softmax_scale
|
||||
k = tl.load(k_ptrs).to(tl.float32)
|
||||
v = tl.load(v_ptrs).to(tl.float32)
|
||||
|
||||
state = state * decay_scale + k[:, None] * v
|
||||
o = tl.sum(q[:, None] * state, axis=0)
|
||||
|
||||
tl.store(out_ptrs, o.to(Out.dtype.element_ty))
|
||||
tl.store(c_ptrs, state.to(CACHES.dtype.element_ty))
|
||||
q_ptrs += stride_q
|
||||
k_ptrs += stride_k
|
||||
v_ptrs += stride_v
|
||||
out_ptrs += H * HEAD_DIM
|
||||
c_ptrs += H * HEAD_DIM * HEAD_DIM
|
||||
|
||||
|
||||
# (k_dim_block, length, qo_heads, d)
|
||||
@triton.jit
|
||||
def seg_la_sum_kernel(T, O, DIM: tl.constexpr, NUM_BLOCK: tl.constexpr):
|
||||
pid = tl.program_id(0)
|
||||
length = tl.num_programs(0)
|
||||
x = tl.zeros((DIM,), dtype=tl.float32)
|
||||
for i in range(NUM_BLOCK):
|
||||
x += tl.load(T + i * length * DIM + pid * DIM + tl.arange(0, DIM)).to(
|
||||
tl.float32
|
||||
)
|
||||
tl.store(O + pid * DIM + tl.arange(0, DIM), x)
|
||||
|
||||
|
||||
def seg_la_fwd(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
s,
|
||||
decay_scales,
|
||||
meta,
|
||||
caches=None,
|
||||
cache_indices=None,
|
||||
softmax_scale=None,
|
||||
decouple=False,
|
||||
):
|
||||
length, qo_heads, HEAD_DIM = q.shape
|
||||
_, kv_heads, _ = k.shape
|
||||
bs = meta.batch_size
|
||||
if softmax_scale is None:
|
||||
softmax_scale = HEAD_DIM ** (-0.5)
|
||||
|
||||
# MAX_LENGTH = meta.max_q_length
|
||||
MAX_LENGTH = triton.cdiv(length, bs)
|
||||
|
||||
assert qo_heads == kv_heads, "seg_la does NOT support GQA currently"
|
||||
|
||||
if MAX_LENGTH > 1:
|
||||
# prefill with partitioning q/k/v
|
||||
# BLOCK should <= 64 with decouple
|
||||
K_SPLIT_DIM = 32
|
||||
V_SPLIT_DIM = 32 if bs <= 2 else 64
|
||||
|
||||
num_warps = 2 # 2
|
||||
num_stages = 3 # 3
|
||||
|
||||
k_dim_block = HEAD_DIM // K_SPLIT_DIM
|
||||
v_dim_block = HEAD_DIM // V_SPLIT_DIM
|
||||
tmp = torch.empty(
|
||||
(k_dim_block, length, qo_heads, HEAD_DIM), device=q.device, dtype=q.dtype
|
||||
)
|
||||
grid = (bs, kv_heads, k_dim_block * v_dim_block)
|
||||
|
||||
if caches is not None:
|
||||
# mtp
|
||||
EVEN = False
|
||||
BLOCK = 32
|
||||
step = length // bs
|
||||
|
||||
seg_la_mtp_kernel[grid](
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
s,
|
||||
caches,
|
||||
tmp,
|
||||
softmax_scale,
|
||||
q.stride(0),
|
||||
k.stride(0),
|
||||
v.stride(0),
|
||||
s.stride(0),
|
||||
caches.stride(0),
|
||||
tmp.stride(0),
|
||||
meta.s_offsets,
|
||||
cache_indices,
|
||||
decay_scales,
|
||||
step,
|
||||
HEAD_DIM=HEAD_DIM,
|
||||
K_SPLIT_DIM=K_SPLIT_DIM,
|
||||
V_SPLIT_DIM=V_SPLIT_DIM,
|
||||
num_warps=num_warps,
|
||||
num_stages=num_stages,
|
||||
)
|
||||
|
||||
elif meta.mask is not None:
|
||||
# spec
|
||||
ms = meta.mask.size(-1)
|
||||
BLOCK = (ms + 15) // 16 * 16
|
||||
EVEN = BLOCK == ms
|
||||
|
||||
seg_la_s_kernel[grid](
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
s,
|
||||
tmp,
|
||||
meta.mask,
|
||||
softmax_scale,
|
||||
q.stride(0),
|
||||
k.stride(0),
|
||||
v.stride(0),
|
||||
s.stride(0),
|
||||
tmp.stride(0),
|
||||
meta.s_offsets,
|
||||
meta.q_offsets,
|
||||
meta.q_lengths,
|
||||
meta.s_scales,
|
||||
decay_scales,
|
||||
HEAD_DIM=HEAD_DIM,
|
||||
K_SPLIT_DIM=K_SPLIT_DIM,
|
||||
V_SPLIT_DIM=V_SPLIT_DIM,
|
||||
BLOCK=BLOCK,
|
||||
EVEN=EVEN,
|
||||
num_warps=num_warps,
|
||||
num_stages=num_stages,
|
||||
)
|
||||
|
||||
else:
|
||||
# prefill
|
||||
BLOCK = 32
|
||||
EVEN = MAX_LENGTH % BLOCK == 0 if bs == 1 else False
|
||||
|
||||
seg_la_p_kernel[grid](
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
s,
|
||||
tmp,
|
||||
softmax_scale,
|
||||
q.stride(0),
|
||||
k.stride(0),
|
||||
v.stride(0),
|
||||
s.stride(0),
|
||||
tmp.stride(0),
|
||||
meta.s_offsets,
|
||||
meta.q_offsets,
|
||||
meta.q_lengths,
|
||||
meta.s_scales,
|
||||
decay_scales,
|
||||
HEAD_DIM=HEAD_DIM,
|
||||
K_SPLIT_DIM=K_SPLIT_DIM,
|
||||
V_SPLIT_DIM=V_SPLIT_DIM,
|
||||
BLOCK=BLOCK,
|
||||
EVEN=EVEN,
|
||||
num_warps=num_warps,
|
||||
num_stages=num_stages,
|
||||
)
|
||||
|
||||
if k_dim_block > 1:
|
||||
if length < 2048:
|
||||
o = tmp.sum(0)
|
||||
else:
|
||||
o = torch.empty(
|
||||
(length, qo_heads, HEAD_DIM), device=q.device, dtype=q.dtype
|
||||
)
|
||||
seg_la_sum_kernel[(length,)](
|
||||
tmp,
|
||||
o,
|
||||
DIM=qo_heads * HEAD_DIM,
|
||||
NUM_BLOCK=k_dim_block,
|
||||
num_warps=2,
|
||||
num_stages=3,
|
||||
)
|
||||
else:
|
||||
o = tmp[0]
|
||||
|
||||
else:
|
||||
# decode with partitioning q/k/v
|
||||
if bs <= 128:
|
||||
K_SPLIT_DIM = 128 # 128
|
||||
V_SPLIT_DIM = 32 # 32
|
||||
num_warps = 2 # 2
|
||||
num_stages = 2 # 3
|
||||
else:
|
||||
K_SPLIT_DIM = 128 # 128
|
||||
V_SPLIT_DIM = 64 # 32
|
||||
num_warps = 2 # 2
|
||||
num_stages = 3 # 3
|
||||
k_dim_block = HEAD_DIM // K_SPLIT_DIM
|
||||
v_dim_block = HEAD_DIM // V_SPLIT_DIM
|
||||
tmp = torch.empty(
|
||||
(k_dim_block, length, qo_heads, HEAD_DIM), device=q.device, dtype=q.dtype
|
||||
)
|
||||
grid = (bs, kv_heads, k_dim_block * v_dim_block)
|
||||
|
||||
seg_la_d_kernel[grid](
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
s,
|
||||
tmp,
|
||||
softmax_scale,
|
||||
q.stride(0),
|
||||
k.stride(0),
|
||||
v.stride(0),
|
||||
s.stride(0),
|
||||
tmp.stride(0),
|
||||
meta.s_offsets,
|
||||
decay_scales,
|
||||
HEAD_DIM=HEAD_DIM,
|
||||
K_SPLIT_DIM=K_SPLIT_DIM,
|
||||
V_SPLIT_DIM=V_SPLIT_DIM,
|
||||
num_warps=num_warps,
|
||||
num_stages=num_stages,
|
||||
)
|
||||
if k_dim_block > 1:
|
||||
o = tmp.sum(0)
|
||||
else:
|
||||
o = tmp[0]
|
||||
|
||||
# if fallback:
|
||||
# # prefill/decode with partitioning v only
|
||||
# o = torch.empty(q.shape, device=q.device, dtype=q.dtype)
|
||||
# if MAX_LENGTH == 1:
|
||||
# # decode
|
||||
# BLOCK = 1
|
||||
# EVEN = False
|
||||
# SPLIT_DIM = 32
|
||||
# num_warps = 8
|
||||
# num_stages = 2
|
||||
# num_dim_block = HEAD_DIM // SPLIT_DIM
|
||||
# grid = (batch, kv_heads, num_dim_block)
|
||||
# else:
|
||||
# # prefill
|
||||
# if decouple:
|
||||
# BLOCK = 64
|
||||
# SPLIT_DIM = 16
|
||||
# else:
|
||||
# BLOCK = HEAD_DIM
|
||||
# SPLIT_DIM = 32
|
||||
# # EVEN = all([x % BLOCK == 0 for x in meta.qls])
|
||||
# EVEN = False
|
||||
# num_warps = 8
|
||||
# num_stages = 2
|
||||
# # prop = torch.cuda.get_device_properties(q.device.index)
|
||||
# # arch = prop.major * 10 + prop.minor
|
||||
# # if arch not in (80, 90):
|
||||
# # num_stages = 1
|
||||
|
||||
# num_dim_block = HEAD_DIM // SPLIT_DIM
|
||||
# grid = (batch, kv_heads, num_dim_block)
|
||||
|
||||
# seg_la_kernel[grid](
|
||||
# q,
|
||||
# k,
|
||||
# v,
|
||||
# s,
|
||||
# o,
|
||||
# softmax_scale,
|
||||
# q.stride(0),
|
||||
# k.stride(0),
|
||||
# v.stride(0),
|
||||
# s.stride(0),
|
||||
# o.stride(0),
|
||||
# meta.s_offsets,
|
||||
# meta.q_offsets,
|
||||
# meta.q_lengths,
|
||||
# meta.s_scales,
|
||||
# decay_scales,
|
||||
# HEAD_DIM=HEAD_DIM,
|
||||
# SPLIT_DIM=SPLIT_DIM,
|
||||
# BLOCK=BLOCK,
|
||||
# EVEN=EVEN,
|
||||
# DECOUPLE=decouple,
|
||||
# num_warps=num_warps,
|
||||
# num_stages=num_stages
|
||||
# )
|
||||
return o
|
||||
@@ -0,0 +1,219 @@
|
||||
# Copyright 2023-2026 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Short-convolution attention backend.
|
||||
|
||||
Several hybrid models interleave a *causal short conv with per-request conv
|
||||
state* (stored in the centralized ``MambaPool``) with softmax attention layers:
|
||||
|
||||
* **LFM2** (:class:`Lfm2ShortConv <sglang.srt.models.lfm2.Lfm2ShortConv>`) --
|
||||
a depthwise gated short conv (``causal_conv1d_fn`` / ``causal_conv1d_update``)
|
||||
as a standalone token mixer on its own conv layers.
|
||||
* **ZAYA1** (:class:`CCA <sglang.srt.models.zaya.CCA>`) -- a two-stage grouped
|
||||
conv plus a one-token ``prev_hs`` lag, preprocessing q/k for the layer's
|
||||
softmax attention.
|
||||
|
||||
These share the *state plumbing* -- resolving the per-request slot indices, the
|
||||
``has_initial_state`` prefix mask, the ``query_start_loc`` cu-seqlens, and the
|
||||
cuda-graph static index buffers, all once per forward step -- but NOT the conv
|
||||
kernel itself. ``ShortConvAttnBackend`` owns only the plumbing and hands it out
|
||||
via :meth:`conv_state_metadata` as a :class:`ShortConvMetadata`; each model runs
|
||||
its own conv kernel against that handle, so the model definition holds no pool
|
||||
access.
|
||||
|
||||
The backend is a *sidecar*: it is invoked directly by the model (through
|
||||
:class:`ShortConvHybridAttnBackend
|
||||
<sglang.srt.layers.attention.hybrid_linear_attn_backend.ShortConvHybridAttnBackend>`),
|
||||
never through the full-vs-linear ``forward_decode`` / ``forward_extend``
|
||||
dispatch. Metadata + cuda-graph capture/replay come from
|
||||
:class:`MambaAttnBackendBase`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, List, NamedTuple, Optional
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.srt.layers.attention.hybrid_linear_attn_backend import (
|
||||
MambaAttnBackendBase,
|
||||
)
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.model_executor.model_runner import ModelRunner
|
||||
|
||||
|
||||
class ShortConvMetadata(NamedTuple):
|
||||
"""Per-(layer, step) conv-state handle handed to a model's conv kernel.
|
||||
|
||||
``layer_cache`` exposes the per-layer pool views (``conv[0]`` = conv state,
|
||||
``conv[1]`` = an optional second state such as ZAYA1's ``prev_hs``,
|
||||
``temporal`` = SSM state, unused by pure short convs). The device tensors are
|
||||
cuda-graph-static on the decode/replay path; the ``*_cpu`` host mirrors are
|
||||
built once per step only for models whose extend path runs a host loop
|
||||
(e.g. ZAYA1 v1) and are ``None`` on decode.
|
||||
"""
|
||||
|
||||
layer_cache: Any
|
||||
cache_indices: torch.Tensor
|
||||
# cu-seqlens for the varlen prefill conv (device, int32). None on decode.
|
||||
query_start_loc: Optional[torch.Tensor] = None
|
||||
# Per-request "resumes a cached prefix" mask (device bool). None on decode.
|
||||
has_initial_state: Optional[torch.Tensor] = None
|
||||
# Host mirror of cache_indices for extend host loops. None on decode.
|
||||
slot_ids_cpu: Optional[List[int]] = None
|
||||
# Host mirror of has_initial_state for extend host loops. None on decode.
|
||||
has_prefix_cpu: Optional[List[bool]] = None
|
||||
|
||||
|
||||
class ShortConvAttnBackend(MambaAttnBackendBase):
|
||||
"""Owns the short-conv per-request state plumbing (see module docstring)."""
|
||||
|
||||
# State IO is index-driven; no host seq-lens plumbing required from the
|
||||
# runner. (The extend path reads ``extend_*_cpu`` off the batch, which is
|
||||
# always populated for extend regardless of this flag.)
|
||||
needs_cpu_seq_lens: bool = False
|
||||
|
||||
def __init__(self, model_runner: ModelRunner):
|
||||
super().__init__(model_runner)
|
||||
mamba_cache = self.req_to_token_pool.mamba_pool.mamba_cache
|
||||
# conv[0] == conv_state: [n_layers, n_slots, conv_dim, conv_kernel - 1]
|
||||
self.conv_states_shape = mamba_cache.conv[0].shape
|
||||
|
||||
# Per-step state, resolved ONCE per step in init_forward_metadata /
|
||||
# init_forward_metadata_out_graph (never per conv layer). The extend host
|
||||
# mirrors drive the extend loop; ``_cache_indices`` is the int64 slot
|
||||
# index view shared by all conv layers within the step.
|
||||
self._has_initial_state: Optional[torch.Tensor] = None
|
||||
self._slot_ids_cpu: Optional[List[int]] = None
|
||||
self._has_prefix_cpu: Optional[List[bool]] = None
|
||||
self._cache_indices: Optional[torch.Tensor] = None
|
||||
self._cache_indices_buf: Optional[torch.Tensor] = None
|
||||
|
||||
def _reset_step_state(self):
|
||||
self._has_initial_state = None
|
||||
self._slot_ids_cpu = None
|
||||
self._has_prefix_cpu = None
|
||||
|
||||
def _alloc_cache_indices_buf(self, max_bs: int):
|
||||
# Persistent int64 index buffer, refilled in place per step so the
|
||||
# captured (cuda or cpu) graph reads a stable address.
|
||||
self._cache_indices_buf = torch.empty(
|
||||
max_bs, dtype=torch.int64, device=self.device
|
||||
)
|
||||
|
||||
def _refresh_cache_indices(self):
|
||||
# Resolve the int64 slot-index view ONCE per step, shared by every conv
|
||||
# layer. When a graph index buffer is allocated and large enough, refill
|
||||
# it IN PLACE and hand out a view -- the captured graph then reads a
|
||||
# stable address that this (pre-replay) hook keeps current, so it is
|
||||
# cuda- and cpu-graph safe. Otherwise (eager, or bs beyond the buffer)
|
||||
# a fresh cast is fine.
|
||||
md = self.forward_metadata
|
||||
idx = md.mamba_cache_indices if md is not None else None
|
||||
buf = self._cache_indices_buf
|
||||
if idx is None:
|
||||
self._cache_indices = None
|
||||
elif buf is not None and idx.shape[0] <= buf.shape[0]:
|
||||
n = idx.shape[0]
|
||||
buf[:n].copy_(idx)
|
||||
self._cache_indices = buf[:n]
|
||||
else:
|
||||
self._cache_indices = idx.to(torch.long)
|
||||
|
||||
def init_cuda_graph_state(self, max_bs: int, max_num_tokens: int):
|
||||
super().init_cuda_graph_state(max_bs, max_num_tokens)
|
||||
self._alloc_cache_indices_buf(max_bs)
|
||||
|
||||
def init_cpu_graph_state(self, max_bs: int, max_num_tokens: int):
|
||||
super().init_cpu_graph_state(max_bs, max_num_tokens)
|
||||
self._alloc_cache_indices_buf(max_bs)
|
||||
|
||||
def init_forward_metadata(self, forward_batch: ForwardBatch):
|
||||
# Eager path (also the CPU-graph replay path). Builds
|
||||
# self.forward_metadata and runs the deferred mamba clear/COW ops.
|
||||
super().init_forward_metadata(forward_batch)
|
||||
self._reset_step_state()
|
||||
self._refresh_cache_indices()
|
||||
mode = forward_batch.forward_mode
|
||||
if (
|
||||
mode.is_extend()
|
||||
and not mode.is_target_verify()
|
||||
and not mode.is_draft_extend_v2()
|
||||
):
|
||||
self._has_initial_state = forward_batch.extend_prefix_lens > 0
|
||||
if self._cache_indices is not None:
|
||||
self._slot_ids_cpu = self._cache_indices.tolist()
|
||||
self._has_prefix_cpu = [
|
||||
int(p) > 0 for p in forward_batch.extend_prefix_lens_cpu
|
||||
]
|
||||
|
||||
def init_forward_metadata_out_graph(
|
||||
self, forward_batch: ForwardBatch, in_capture: bool = False
|
||||
):
|
||||
# Decode cuda-graph capture + replay path -- no extend prefix state.
|
||||
super().init_forward_metadata_out_graph(forward_batch, in_capture)
|
||||
self._reset_step_state()
|
||||
self._refresh_cache_indices()
|
||||
|
||||
def init_forward_metadata_capture_cpu_graph(self, *args, **kwargs):
|
||||
# Decode CPU-graph capture path. The base fills forward_metadata but not
|
||||
# the int64 view; without this the conv layers would capture a ``None``
|
||||
# index (crash / corrupt state). Replay goes through init_forward_metadata
|
||||
# and refills the SAME buffer, so the captured cpu graph reads a stable
|
||||
# address kept current at replay.
|
||||
super().init_forward_metadata_capture_cpu_graph(*args, **kwargs)
|
||||
self._reset_step_state()
|
||||
self._refresh_cache_indices()
|
||||
|
||||
def conv_state_metadata(
|
||||
self, layer_id: int, forward_batch: ForwardBatch
|
||||
) -> ShortConvMetadata:
|
||||
"""Return the conv-state handle for ``layer_id`` at the current step.
|
||||
|
||||
The per-step fields are already resolved on ``self.forward_metadata`` /
|
||||
``self._*`` (in ``init_forward_metadata`` / ``_out_graph``);
|
||||
``forward_batch`` is accepted for interface parity with the unit-test
|
||||
mock and is not otherwise required here.
|
||||
"""
|
||||
layer_cache = self.req_to_token_pool.mamba2_layer_cache(layer_id)
|
||||
md = self.forward_metadata
|
||||
|
||||
# Slot indices are cached ONCE per step in init_forward_metadata /
|
||||
# init_forward_metadata_out_graph (int64). Hand back the cached view -- no
|
||||
# per-layer recompute. Decode is cuda-graph-safe because that view is a
|
||||
# persistent buffer refilled in place before each replay.
|
||||
return ShortConvMetadata(
|
||||
layer_cache=layer_cache,
|
||||
cache_indices=self._cache_indices,
|
||||
query_start_loc=md.query_start_loc,
|
||||
has_initial_state=self._has_initial_state,
|
||||
slot_ids_cpu=self._slot_ids_cpu,
|
||||
has_prefix_cpu=self._has_prefix_cpu,
|
||||
)
|
||||
|
||||
# The short-conv layers are invoked via conv_state_metadata + the model's own
|
||||
# conv kernel, never through the HybridLinearAttnBackend full-vs-linear
|
||||
# dispatch. Mirror Mamba2AttnBackend and guard the routed entrypoints.
|
||||
def forward_decode(self, *args, **kwargs):
|
||||
raise NotImplementedError(
|
||||
"ShortConvAttnBackend is invoked via conv_state_metadata; "
|
||||
"it does not run through forward_decode."
|
||||
)
|
||||
|
||||
def forward_extend(self, *args, **kwargs):
|
||||
raise NotImplementedError(
|
||||
"ShortConvAttnBackend is invoked via conv_state_metadata; "
|
||||
"it does not run through forward_extend."
|
||||
)
|
||||
@@ -0,0 +1,77 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from sglang.srt.utils.common import rank0_log
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LinearAttnKernelBackend(Enum):
|
||||
TRITON = "triton"
|
||||
CUTEDSL = "cutedsl"
|
||||
FLASHINFER = "flashinfer"
|
||||
FLASHKDA = "flashkda"
|
||||
CUSTOM = "custom"
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, value):
|
||||
return cls.CUSTOM
|
||||
|
||||
def is_triton(self):
|
||||
return self == LinearAttnKernelBackend.TRITON
|
||||
|
||||
def is_cutedsl(self):
|
||||
return self == LinearAttnKernelBackend.CUTEDSL
|
||||
|
||||
def is_flashinfer(self):
|
||||
return self == LinearAttnKernelBackend.FLASHINFER
|
||||
|
||||
def is_flashkda(self):
|
||||
return self == LinearAttnKernelBackend.FLASHKDA
|
||||
|
||||
def is_custom(self):
|
||||
return self == LinearAttnKernelBackend.CUSTOM
|
||||
|
||||
|
||||
LINEAR_ATTN_DECODE_BACKEND: Optional[LinearAttnKernelBackend] = None
|
||||
LINEAR_ATTN_PREFILL_BACKEND: Optional[LinearAttnKernelBackend] = None
|
||||
|
||||
|
||||
def initialize_linear_attn_config(server_args: ServerArgs):
|
||||
global LINEAR_ATTN_DECODE_BACKEND
|
||||
global LINEAR_ATTN_PREFILL_BACKEND
|
||||
|
||||
base = server_args.linear_attn_backend
|
||||
decode = server_args.linear_attn_decode_backend or base
|
||||
prefill = server_args.linear_attn_prefill_backend or base
|
||||
|
||||
LINEAR_ATTN_DECODE_BACKEND = LinearAttnKernelBackend(decode)
|
||||
LINEAR_ATTN_PREFILL_BACKEND = LinearAttnKernelBackend(prefill)
|
||||
|
||||
rank0_log(f"Linear attention kernel backend: decode={decode}, prefill={prefill}")
|
||||
|
||||
|
||||
def get_linear_attn_decode_backend() -> LinearAttnKernelBackend:
|
||||
global LINEAR_ATTN_DECODE_BACKEND
|
||||
if LINEAR_ATTN_DECODE_BACKEND is None:
|
||||
logger.warning(
|
||||
"LINEAR_ATTN_DECODE_BACKEND is not initialized, using triton backend"
|
||||
)
|
||||
LINEAR_ATTN_DECODE_BACKEND = LinearAttnKernelBackend.TRITON
|
||||
return LINEAR_ATTN_DECODE_BACKEND
|
||||
|
||||
|
||||
def get_linear_attn_prefill_backend() -> LinearAttnKernelBackend:
|
||||
global LINEAR_ATTN_PREFILL_BACKEND
|
||||
if LINEAR_ATTN_PREFILL_BACKEND is None:
|
||||
logger.warning(
|
||||
"LINEAR_ATTN_PREFILL_BACKEND is not initialized, using triton backend"
|
||||
)
|
||||
LINEAR_ATTN_PREFILL_BACKEND = LinearAttnKernelBackend.TRITON
|
||||
return LINEAR_ATTN_PREFILL_BACKEND
|
||||
Reference in New Issue
Block a user