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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1,89 @@
"""KV-cache write/transfer kernels.
This group wraps the Triton ``reshape_and_cache`` launcher, whose implementation
now lives in this package (``sglang.kernels.ops.kvcache.cache_ops``) after being
migrated out of ``sglang.srt.layers.attention.triton_ops`` (RFC #29630).
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from sglang.kernels.registry import register_kernel
from sglang.kernels.selector import get_kernel
from sglang.kernels.spec import FormatSignature, KernelBackend, KernelSpec
if TYPE_CHECKING:
import torch
register_kernel(
KernelSpec(
op="kvcache.reshape_and_cache_flash",
backend=KernelBackend.TRITON,
target="sglang.kernels.ops.kvcache.cache_ops:launch_reshape_and_cache_flash",
format_signature=FormatSignature(
in_place=True,
description="write token-major K/V into paged KV cache layout",
),
description="Reshape-and-cache (Triton launcher).",
)
)
def reshape_and_cache_flash(
key: torch.Tensor,
value: torch.Tensor,
key_cache: torch.Tensor,
value_cache: torch.Tensor,
slot_mapping: torch.Tensor,
swa_slot_mapping: Optional[torch.Tensor] = None,
k_scale: Optional[torch.Tensor] = None,
v_scale: Optional[torch.Tensor] = None,
) -> None:
"""Write token-major ``key``/``value`` into paged KV cache layout."""
return get_kernel("kvcache.reshape_and_cache_flash", KernelBackend.TRITON)(
key,
value,
key_cache,
value_cache,
slot_mapping,
swa_slot_mapping,
k_scale,
v_scale,
)
__all__ = ["reshape_and_cache_flash"]
# Other Triton kernels migrated into this group (from attention/mem_cache
# triton_ops); registered for inventory. Import them from their modules.
_TRITON_KERNELS = [
("cache_ops", "concat_and_cast_mha_k_triton"),
("cache_ops", "launch_reshape_and_cache_flash"),
("kv_indices", "create_flashinfer_kv_indices_triton"),
("kv_indices", "create_flashmla_kv_indices_triton"),
("kv_indices", "create_chunked_prefix_cache_kv_indices"),
("kv_indices", "get_num_kv_index_blocks_flashmla"),
("kv_indices", "get_num_page_per_block_flashmla"),
("rope_cache", "fused_qk_rope_reshape_and_cache"),
("trtllm_fp8_kv_kernel", "fused_fp8_set_kv_buffer"),
("trtllm_mha_page_table", "build_trtllm_mha_page_table"),
("trtllm_mha_graph_metadata", "update_trtllm_mha_graph_metadata"),
("aiter_unified_attention", "scatter_ragged_to_page_table_kernel"),
("aiter_unified_attention", "scatter_req_to_token_to_page_table_kernel"),
("cache_move", "store_cache_4d"),
("cache_move", "set_kv_buffer_prefix_valid_tiled"),
("cache_move", "copy_all_layer_kv_cache_tiled"),
("mla_buffer", "set_mla_kv_buffer_triton"),
("mla_buffer", "get_mla_kv_buffer_triton"),
]
for _mod, _fn in _TRITON_KERNELS:
register_kernel(
KernelSpec(
op=f"kvcache.{_fn}",
backend=KernelBackend.TRITON,
target=f"sglang.kernels.ops.kvcache.{_mod}:{_fn}",
)
)
del _mod, _fn
@@ -0,0 +1,97 @@
import triton
import triton.language as tl
@triton.jit
def scatter_ragged_to_page_table_kernel(
kv_flat_ptr,
kv_indptr_ptr,
dest_ptr,
dest_stride,
sw_page_table_ptr,
swa_slot_mapping_ptr,
PAGE_SIZE: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
HAS_SWA: tl.constexpr,
):
"""Scatter ragged token-level kv_indices into a 2D block-level page table."""
pid = tl.program_id(0)
block_id = tl.program_id(1)
start = tl.load(kv_indptr_ptr + pid).to(tl.int64)
kv_len = tl.load(kv_indptr_ptr + pid + 1).to(tl.int64) - start
num_blocks = (kv_len + PAGE_SIZE - 1) // PAGE_SIZE
offsets = block_id * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
if block_id * BLOCK_SIZE >= num_blocks:
return
mask = offsets < num_blocks
token_idx = offsets.to(tl.int64) * PAGE_SIZE
vals = tl.load(kv_flat_ptr + start + token_idx, mask=mask, other=0)
block_vals = vals // PAGE_SIZE
tl.store(
dest_ptr + pid.to(tl.int64) * dest_stride + offsets,
block_vals,
mask=mask,
)
if HAS_SWA:
sw_vals = tl.load(swa_slot_mapping_ptr + vals)
block_vals = sw_vals // PAGE_SIZE
tl.store(
sw_page_table_ptr + pid.to(tl.int64) * dest_stride + offsets,
block_vals,
mask=mask,
)
@triton.jit
def scatter_req_to_token_to_page_table_kernel(
req_to_token_ptr,
req_pool_indices_ptr,
seq_lens_ptr,
page_table_ptr,
req_to_token_stride,
page_table_stride,
sw_page_table_ptr,
swa_slot_mapping_ptr,
DRAFT_NUM: tl.constexpr,
PAGE_SIZE: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
HAS_SWA: tl.constexpr,
):
"""Build the 2D block-level page_table for target_verify from req_to_token."""
pid = tl.program_id(0)
block_id = tl.program_id(1)
seq_len = tl.load(seq_lens_ptr + pid).to(tl.int64)
kv_len = seq_len + DRAFT_NUM
num_blocks = (kv_len + PAGE_SIZE - 1) // PAGE_SIZE
offsets = block_id * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE)
if block_id * BLOCK_SIZE >= num_blocks:
return
mask = offsets < num_blocks
rp = tl.load(req_pool_indices_ptr + pid).to(tl.int64)
token_idx = offsets.to(tl.int64) * PAGE_SIZE
vals = tl.load(
req_to_token_ptr + rp * req_to_token_stride + token_idx,
mask=mask,
other=0,
)
block_vals = vals // PAGE_SIZE
tl.store(
page_table_ptr + pid.to(tl.int64) * page_table_stride + offsets,
block_vals,
mask=mask,
)
if HAS_SWA:
sw_vals = tl.load(swa_slot_mapping_ptr + vals)
block_vals = sw_vals // PAGE_SIZE
tl.store(
sw_page_table_ptr + pid.to(tl.int64) * page_table_stride + offsets,
block_vals,
mask=mask,
)
@@ -0,0 +1,306 @@
import torch
import triton
import triton.language as tl
from sglang.srt.utils import is_cpu
_is_cpu = is_cpu()
if _is_cpu:
from sgl_kernel import copy_all_layer_kv_cache_cpu
@triton.jit
def set_kv_buffer_prefix_valid_tiled(
src_k_ptr,
src_v_ptr,
dst_k_ptr,
dst_v_ptr,
loc_2d_ptr,
commit_len_ptr,
src_k_row_stride,
src_v_row_stride,
dst_k_row_stride,
dst_v_row_stride,
block_size,
ROW_BYTES: tl.constexpr,
BYTES_PER_TILE: tl.constexpr,
):
bid = tl.program_id(0)
row = tl.program_id(1)
tid = tl.program_id(2)
commit_len = tl.load(commit_len_ptr + bid)
if row >= commit_len:
return
byte_off = tid * BYTES_PER_TILE + tl.arange(0, BYTES_PER_TILE)
mask_byte = byte_off < ROW_BYTES
tl.multiple_of(byte_off, 16)
loc = tl.load(loc_2d_ptr + bid * block_size + row)
src_row = bid * block_size + row
src_k_ptr = tl.cast(src_k_ptr, tl.pointer_type(tl.uint8))
src_v_ptr = tl.cast(src_v_ptr, tl.pointer_type(tl.uint8))
dst_k_ptr = tl.cast(dst_k_ptr, tl.pointer_type(tl.uint8))
dst_v_ptr = tl.cast(dst_v_ptr, tl.pointer_type(tl.uint8))
src_k_row_ptr = src_k_ptr + src_row * src_k_row_stride + byte_off
src_v_row_ptr = src_v_ptr + src_row * src_v_row_stride + byte_off
dst_k_row_ptr = dst_k_ptr + loc * dst_k_row_stride + byte_off
dst_v_row_ptr = dst_v_ptr + loc * dst_v_row_stride + byte_off
k_val = tl.load(src_k_row_ptr, mask=mask_byte, other=0)
v_val = tl.load(src_v_row_ptr, mask=mask_byte, other=0)
tl.store(dst_k_row_ptr, k_val, mask=mask_byte)
tl.store(dst_v_row_ptr, v_val, mask=mask_byte)
@triton.jit
def copy_all_layer_kv_cache_tiled(
data_ptrs,
strides,
tgt_loc_ptr,
src_loc_ptr,
num_locs,
num_locs_upper: tl.constexpr,
BYTES_PER_TILE: tl.constexpr,
):
"""2D tiled kernel. Safe for in-place copy."""
bid = tl.program_id(0)
tid = tl.program_id(1)
stride = tl.load(strides + bid)
base_ptr = tl.load(data_ptrs + bid)
base_ptr = tl.cast(base_ptr, tl.pointer_type(tl.uint8))
byte_off = tid * BYTES_PER_TILE + tl.arange(0, BYTES_PER_TILE)
mask_byte = byte_off < stride
tl.multiple_of(byte_off, 16)
loc_idx = tl.arange(0, num_locs_upper)
mask_loc = loc_idx < num_locs
src = tl.load(src_loc_ptr + loc_idx, mask=mask_loc, other=0)
tgt = tl.load(tgt_loc_ptr + loc_idx, mask=mask_loc, other=0)
src_ptr = base_ptr + src[:, None] * stride + byte_off[None, :]
tgt_ptr = base_ptr + tgt[:, None] * stride + byte_off[None, :]
mask = mask_loc[:, None] & mask_byte[None, :]
vals = tl.load(src_ptr, mask=mask)
tl.store(tgt_ptr, vals, mask=mask)
def copy_all_layer_kv_cache_func(
data_ptrs: torch.Tensor,
strides: torch.Tensor,
tgt_loc: torch.Tensor,
src_loc: torch.Tensor,
num_locs: int,
num_locs_upper: int,
kv_copy_config: dict,
):
if _is_cpu:
copy_all_layer_kv_cache_cpu(
data_ptrs,
strides,
tgt_loc[:num_locs],
src_loc[:num_locs],
)
return
grid = (data_ptrs.numel(), kv_copy_config["byte_tiles"])
copy_all_layer_kv_cache_tiled[grid](
data_ptrs,
strides,
tgt_loc,
src_loc,
num_locs,
num_locs_upper,
BYTES_PER_TILE=kv_copy_config["bytes_per_tile"],
num_warps=kv_copy_config["num_warps"],
num_stages=2,
)
# ---------------------------------------------------------------------------
# store_cache_4d — single-launch Triton write into the 4-D page-major envelope
# K/V views. At `PAGE_SIZE = 1` the kernel constexpr-folds to byte-identical
# addresses as the slot-major envelope view; at `PAGE_SIZE > 1` it uses the
# same `(page_id, tok_in_p)` split the attention read kernels use.
# ---------------------------------------------------------------------------
@triton.jit
def store_cache_4d_kernel(
k_view_ptr,
v_view_ptr,
cache_k_ptr,
cache_v_ptr,
loc_ptr,
# Strides in ELEMENTS (not bytes); wrapper passes view.stride(D)
# directly. K and V may have different head_dim → different per-token
# strides, so we carry both.
stride_k_page,
stride_k_tok,
stride_v_page,
stride_v_tok,
stride_src_k_row,
stride_src_v_row,
K_ROW_DIM: tl.constexpr, # head_num * head_dim
V_ROW_DIM: tl.constexpr, # head_num * v_head_dim
PAGE_SIZE: tl.constexpr,
BLOCK: tl.constexpr,
):
"""Token-parallel Triton write into a 4-D envelope-strided K/V view.
Grid: ``(N, ceil(max(K_ROW_DIM, V_ROW_DIM) / BLOCK), 2)`` where:
- axis 0 → one program per token (loc[i])
- axis 1 → blocks within one slot's K (or V) row
- axis 2 → 0 = K, 1 = V (two-tensor write fused into one launch)
For each token i, the kernel writes:
page_id = loc[i] // PAGE_SIZE
tok_in_p = loc[i] % PAGE_SIZE
k_view[page_id, tok_in_p, :, :] = cache_k[i, :, :]
v_view[page_id, tok_in_p, :, :] = cache_v[i, :, :]
Cuda-graph safe: no Python branching on tensor values, no `.item()`,
all shapes/strides known at launch time.
"""
pid_n = tl.program_id(0)
pid_b = tl.program_id(1)
pid_kv = tl.program_id(2)
# 1. Resolve destination slot in the 4-D view.
loc = tl.load(loc_ptr + pid_n).to(tl.int64)
if PAGE_SIZE == 1:
page_id = loc
tok_in_p = tl.zeros([], dtype=tl.int64)
else:
page_id = loc // PAGE_SIZE
tok_in_p = loc % PAGE_SIZE
# 2. Compute per-tensor source/dest pointers.
base_off = pid_b * BLOCK + tl.arange(0, BLOCK)
if pid_kv == 0:
mask = base_off < K_ROW_DIM
# The trailing (head_num, head_dim) axes of `k_view` are
# contiguous: stride[-1]==1, stride[-2]==head_dim. So we can
# treat them as a flat K_ROW_DIM dimension addressed by `base_off`.
# The wrapper asserts this invariant.
src_ptr = cache_k_ptr + pid_n * stride_src_k_row + base_off
dst_ptr = (
k_view_ptr + page_id * stride_k_page + tok_in_p * stride_k_tok + base_off
)
else:
mask = base_off < V_ROW_DIM
src_ptr = cache_v_ptr + pid_n * stride_src_v_row + base_off
dst_ptr = (
v_view_ptr + page_id * stride_v_page + tok_in_p * stride_v_tok + base_off
)
src = tl.load(src_ptr, mask=mask)
tl.store(dst_ptr, src, mask=mask)
def store_cache_4d(
k_view: torch.Tensor,
v_view: torch.Tensor,
cache_k: torch.Tensor,
cache_v: torch.Tensor,
loc: torch.Tensor,
page_size: int,
) -> None:
"""One-launch Triton write into the 4-D page-major envelope K/V views.
Writes ``cache_k[i]`` and ``cache_v[i]`` to
``k_view[loc[i]//ps, loc[i]%ps, :, :]`` (and analogously for V) for
``i in [0, N)``.
Contract:
- ``k_view``, ``v_view``: 4-D ``(num_pages, page_size, head_num,
head_dim*)``, contiguous in the trailing ``(head_num, head_dim)``
dims (i.e., ``stride[-1] == 1`` and ``stride[-2] == head_dim``).
- ``cache_k``, ``cache_v``: 3-D ``(N, head_num, head_dim*)``,
contiguous in the trailing ``(head_num, head_dim)`` dims.
- ``loc``: 1-D int64 or int32, N elements, values in
``[0, num_pages * page_size)``. The caller is responsible for
clamping any negative entries to ≥ 0.
- At ``page_size == 1`` the kernel produces byte-identical output
to the legacy advanced-indexing path.
Returns nothing; writes in place.
"""
if loc.numel() == 0:
return
assert k_view.is_cuda and v_view.is_cuda, "store_cache_4d: CUDA only"
assert k_view.ndim == 4 and v_view.ndim == 4, (
f"store_cache_4d: k_view/v_view must be 4-D, "
f"got {k_view.ndim}/{v_view.ndim}"
)
assert cache_k.ndim == 3 and cache_v.ndim == 3, (
f"store_cache_4d: cache_k/cache_v must be 3-D, "
f"got {cache_k.ndim}/{cache_v.ndim}"
)
assert cache_k.shape[0] == cache_v.shape[0] == loc.numel(), (
"store_cache_4d: cache_k/cache_v/loc batch dim mismatch: "
f"{cache_k.shape[0]}, {cache_v.shape[0]}, {loc.numel()}"
)
assert k_view.dtype == v_view.dtype == cache_k.dtype == cache_v.dtype, (
"store_cache_4d: dtype mismatch: "
f"k_view={k_view.dtype}, v_view={v_view.dtype}, "
f"cache_k={cache_k.dtype}, cache_v={cache_v.dtype}"
)
# Stride invariants — the kernel addresses (head_num, head_dim) as one
# flat ROW_DIM dimension; this requires the trailing two dims to be
# contiguous. This holds for the page-major envelope views
# (k_stride = (page_bytes/itemsize, k_row_bytes/itemsize, head_dim, 1)) and
# for cache_k/cache_v produced by the model forward.
assert k_view.stride(-1) == 1 and k_view.stride(-2) == k_view.shape[-1], (
f"store_cache_4d: k_view trailing dims must be contiguous; "
f"got stride={k_view.stride()}, shape={tuple(k_view.shape)}"
)
assert v_view.stride(-1) == 1 and v_view.stride(-2) == v_view.shape[-1], (
f"store_cache_4d: v_view trailing dims must be contiguous; "
f"got stride={v_view.stride()}, shape={tuple(v_view.shape)}"
)
assert cache_k.stride(-1) == 1 and cache_k.stride(-2) == cache_k.shape[-1], (
f"store_cache_4d: cache_k trailing dims must be contiguous; "
f"got stride={cache_k.stride()}, shape={tuple(cache_k.shape)}"
)
assert cache_v.stride(-1) == 1 and cache_v.stride(-2) == cache_v.shape[-1], (
f"store_cache_4d: cache_v trailing dims must be contiguous; "
f"got stride={cache_v.stride()}, shape={tuple(cache_v.shape)}"
)
head_num = k_view.shape[2]
head_dim = k_view.shape[3]
v_head_dim = v_view.shape[3]
K_ROW_DIM = head_num * head_dim
V_ROW_DIM = head_num * v_head_dim
BLOCK = 128
N = loc.numel()
row_dim_max = max(K_ROW_DIM, V_ROW_DIM)
grid = (N, triton.cdiv(row_dim_max, BLOCK), 2)
store_cache_4d_kernel[grid](
k_view,
v_view,
cache_k,
cache_v,
loc,
k_view.stride(0),
k_view.stride(1),
v_view.stride(0),
v_view.stride(1),
cache_k.stride(0),
cache_v.stride(0),
K_ROW_DIM=K_ROW_DIM,
V_ROW_DIM=V_ROW_DIM,
PAGE_SIZE=page_size,
BLOCK=BLOCK,
num_warps=4,
)
@@ -0,0 +1,266 @@
import torch
import triton
import triton.language as tl
@triton.jit
def concat_and_cast_mha_k_kernel(
k_ptr,
k_nope_ptr,
k_rope_ptr,
head_cnt: tl.constexpr,
k_stride0: tl.constexpr,
k_stride1: tl.constexpr,
nope_stride0: tl.constexpr,
nope_stride1: tl.constexpr,
rope_stride0: tl.constexpr,
nope_dim: tl.constexpr,
rope_dim: tl.constexpr,
):
pid_loc = tl.program_id(0)
head_range = tl.arange(0, head_cnt)
k_head_ptr = k_ptr + pid_loc * k_stride0 + head_range[:, None] * k_stride1
nope_offs = tl.arange(0, nope_dim)
src_nope_ptr = (
k_nope_ptr
+ pid_loc * nope_stride0
+ head_range[:, None] * nope_stride1
+ nope_offs[None, :]
)
dst_nope_ptr = k_head_ptr + nope_offs[None, :]
src_nope = tl.load(src_nope_ptr)
tl.store(dst_nope_ptr, src_nope)
rope_offs = tl.arange(0, rope_dim)
src_rope_ptr = k_rope_ptr + pid_loc * rope_stride0 + rope_offs[None, :]
dst_rope_ptr = k_head_ptr + nope_dim + rope_offs[None, :]
src_rope = tl.load(src_rope_ptr)
tl.store(dst_rope_ptr, src_rope)
def concat_and_cast_mha_k_triton(
k: torch.Tensor,
k_nope: torch.Tensor,
k_rope: torch.Tensor,
):
# The source data type will be implicitly converted to the target data type.
assert (
len(k.shape) == 3 and len(k_nope.shape) == 3 and len(k_rope.shape) == 3
), f"shape should be 3d, but got {k.shape=}, {k_nope.shape=}, {k_rope.shape=}"
assert (
k.shape[0] == k_nope.shape[0] and k.shape[0] == k_rope.shape[0]
), f"invalid shape, got {k.shape=}, {k_nope.shape=}, {k_rope.shape=}"
assert (
k.shape[1] == k_nope.shape[1] and 1 == k_rope.shape[1]
), f"invalid shape, got {k.shape=}, {k_nope.shape=}, {k_rope.shape=}"
assert (
k.shape[-1] == k_nope.shape[-1] + k_rope.shape[-1]
), f"invalid shape, got {k.shape=}, {k_nope.shape=}, {k_rope.shape=}"
nope_dim = k_nope.shape[-1]
rope_dim = k_rope.shape[-1]
grid = (k.shape[0],)
concat_and_cast_mha_k_kernel[grid](
k,
k_nope,
k_rope,
k.shape[1],
k.stride(0),
k.stride(1),
k_nope.stride(0),
k_nope.stride(1),
k_rope.stride(0),
nope_dim,
rope_dim,
)
@triton.jit
def reshape_and_cache_flash(
key_ptr,
value_ptr,
key_cache_ptr,
value_cache_ptr,
slot_mapping_ptr,
swa_slot_mapping_ptr,
k_scale_ptr,
v_scale_ptr,
block_stride,
key_stride,
value_stride,
num_heads,
head_size,
block_size,
HEAD_BLOCK: tl.constexpr,
BLOCK_D: tl.constexpr,
HAS_SWA: tl.constexpr,
USE_SCALE: tl.constexpr,
):
"""
Triton kernel for reshaping per-token K/V tensors into paged KV cache layout.
Source layout:
key/value: [num_tokens, num_heads, head_size]
Target cache layout:
cache: [num_blocks, block_size, num_heads, head_size]
Each Triton program instance handles:
- one token (program_id(0))
- one block of heads (program_id(1))
Features:
- optional SWA slot remapping
- optional FP8 scale dequantization before cache write
Args:
key_ptr: Pointer to source key tensor.
value_ptr: Pointer to source value tensor.
key_cache_ptr: Pointer to destination key cache tensor.
value_cache_ptr: Pointer to destination value cache tensor.
slot_mapping_ptr: Maps token -> cache slot.
swa_slot_mapping_ptr: Optional second-stage slot remap for SWA mode.
k_scale_ptr: Optional key scaling factor pointer.
v_scale_ptr: Optional value scaling factor pointer.
block_stride: Stride between cache blocks.
key_stride: Stride between source key tokens.
value_stride: Stride between source value tokens.
num_heads: Number of attention heads.
head_size: Hidden dimension per head.
block_size: Number of slots per cache block.
HEAD_BLOCK: Number of heads processed per program.
BLOCK_D: Vectorized dimension size (power-of-2 padded).
HAS_SWA: Enable SWA remapping.
USE_SCALE: Enable scale division before storing.
"""
# ----------------------------------
# program ids
# pid0 = token
# pid1 = head block
# ----------------------------------
token_idx = tl.program_id(0)
head_block_idx = tl.program_id(1)
# ----------------------------------
# slot mapping
# ----------------------------------
slot_idx = tl.load(slot_mapping_ptr + token_idx)
if HAS_SWA:
slot_idx = tl.load(swa_slot_mapping_ptr + slot_idx)
if slot_idx < 0:
return
block_idx = slot_idx // block_size
block_offset = slot_idx % block_size
# ----------------------------------
# head range
# ----------------------------------
head_idx = head_block_idx * HEAD_BLOCK + tl.arange(0, HEAD_BLOCK)
head_mask = head_idx < num_heads
dim_idx = tl.arange(0, BLOCK_D)
# shape = [HEAD_BLOCK, BLOCK_D]
offs = head_idx[:, None] * head_size + dim_idx[None, :]
mask = head_mask[:, None] & (dim_idx[None, :] < head_size)
# ----------------------------------
# source load
# ----------------------------------
src_key = token_idx * key_stride + offs
src_value = token_idx * value_stride + offs
k = tl.load(key_ptr + src_key, mask=mask)
v = tl.load(value_ptr + src_value, mask=mask)
# ----------------------------------
# optional scale
# ----------------------------------
if USE_SCALE:
k_scale = tl.load(k_scale_ptr)
v_scale = tl.load(v_scale_ptr)
k = k / k_scale
v = v / v_scale
# ----------------------------------
# target layout
# [block_idx, block_offset, head, dim]
# ----------------------------------
tgt = block_idx * block_stride + block_offset * num_heads * head_size + offs
tl.store(key_cache_ptr + tgt, k, mask=mask)
tl.store(value_cache_ptr + tgt, v, mask=mask)
def launch_reshape_and_cache_flash(
key,
value,
key_cache,
value_cache,
slot_mapping,
swa_slot_mapping=None,
k_scale=None,
v_scale=None,
):
"""
Launch wrapper for reshape_and_cache_flash Triton kernel.
This wrapper prepares launch configuration and dispatches the Triton kernel
that writes token-major K/V tensors into paged KV cache layout.
Args:
key: Source key tensor [num_tokens, num_heads, head_size]
value: Source value tensor [num_tokens, num_heads, head_size]
key_cache: Destination key cache [num_blocks, block_size, num_heads, head_size]
value_cache: Destination value cache [num_blocks, block_size, num_heads, head_size]
slot_mapping: Token-to-cache slot mapping
swa_slot_mapping: Optional SWA remapping table
k_scale: Optional key scaling factor
v_scale: Optional value scaling factor
"""
num_tokens = key.shape[0]
num_heads = key.shape[1]
head_size = key.shape[2]
HEAD_BLOCK = 4
BLOCK_D = triton.next_power_of_2(head_size)
grid = (
num_tokens,
triton.cdiv(num_heads, HEAD_BLOCK),
)
reshape_and_cache_flash[grid](
key,
value,
key_cache,
value_cache,
slot_mapping,
swa_slot_mapping,
k_scale if k_scale is not None else key,
v_scale if v_scale is not None else key,
key_cache.stride(0),
key.stride(0),
value.stride(0),
num_heads,
head_size,
key_cache.shape[1],
HEAD_BLOCK=HEAD_BLOCK,
BLOCK_D=BLOCK_D,
HAS_SWA=(swa_slot_mapping is not None),
USE_SCALE=(k_scale is not None),
)
@@ -0,0 +1,152 @@
import triton
import triton.language as tl
_FLASHMLA_CREATE_KV_BLOCK_SIZE = 4096
FLASHMLA_CREATE_KV_BLOCK_SIZE_TRITON = tl.constexpr(_FLASHMLA_CREATE_KV_BLOCK_SIZE)
@triton.jit
def create_flashinfer_kv_indices_triton(
req_to_token_ptr, # [max_batch, max_context_len]
req_pool_indices_ptr,
page_kernel_lens_ptr,
kv_indptr,
kv_start_idx,
kv_indices_ptr,
req_to_token_ptr_stride: tl.constexpr,
):
BLOCK_SIZE: tl.constexpr = 512
pid = tl.program_id(axis=0)
# find the req pool idx, this is for batch to token
req_pool_index = tl.load(req_pool_indices_ptr + pid)
kv_indices_offset = tl.load(kv_indptr + pid)
kv_start = 0
kv_end = 0
if kv_start_idx:
kv_start = tl.load(kv_start_idx + pid).to(tl.int32)
kv_end = kv_start
kv_end += tl.load(page_kernel_lens_ptr + pid).to(tl.int32)
num_loop = tl.cdiv(kv_end - kv_start, BLOCK_SIZE)
for i in range(num_loop):
# index into req_to_token_ptr needs to be int64
offset = tl.arange(0, BLOCK_SIZE).to(tl.int64) + i * BLOCK_SIZE
mask = offset < kv_end - kv_start
data = tl.load(
req_to_token_ptr
+ req_pool_index * req_to_token_ptr_stride
+ kv_start
+ offset,
mask=mask,
)
tl.store(kv_indices_ptr + kv_indices_offset + offset, data, mask=mask)
@triton.jit
def create_chunked_prefix_cache_kv_indices(
req_to_token_ptr, # (max_batch, max_context_len,)
req_pool_indices_ptr, # (batch_size,)
chunk_start_idx_ptr, # (batch_size,)
chunk_seq_lens_ptr, # (batch_size,)
chunk_cu_seq_lens_ptr, # (batch_size + 1,)
chunk_kv_indices_ptr, # (num_chunk_tokens,)
req_to_token_ptr_stride: tl.constexpr,
):
BLOCK_SIZE: tl.constexpr = 512
pid = tl.program_id(axis=0)
# find the req pool idx, this is for batch to token
req_pool_index = tl.load(req_pool_indices_ptr + pid)
chunk_kv_indices_offset = tl.load(chunk_cu_seq_lens_ptr + pid)
# get the token positions of current chunk
chunk_start_pos = tl.load(chunk_start_idx_ptr + pid).to(tl.int32)
chunk_seq_len = tl.load(chunk_seq_lens_ptr + pid).to(tl.int32)
num_loop = tl.cdiv(chunk_seq_len, BLOCK_SIZE)
for i in range(num_loop):
offset = tl.arange(0, BLOCK_SIZE) + i * BLOCK_SIZE
mask = offset < chunk_seq_len
data = tl.load(
req_to_token_ptr
+ req_pool_index * req_to_token_ptr_stride
+ chunk_start_pos
+ offset,
mask=mask,
)
tl.store(
chunk_kv_indices_ptr + chunk_kv_indices_offset + offset, data, mask=mask
)
def get_num_page_per_block_flashmla(page_size: int = 64) -> int:
num_page_per_block = _FLASHMLA_CREATE_KV_BLOCK_SIZE // page_size
return num_page_per_block
def get_num_kv_index_blocks_flashmla(kv_indices_width: int, page_size: int) -> int:
"""Grid axis-1 size for create_flashmla_kv_indices_triton: the number of
page-blocks spanning the widest sequence (one CTA per block). kv_indices_width
is the per-row width of the kv_indices buffer (the kernel's kv_indices_ptr_stride).
"""
npb = get_num_page_per_block_flashmla(page_size)
return (kv_indices_width + npb - 1) // npb
@triton.jit
def create_flashmla_kv_indices_triton(
req_to_token_ptr, # [max_batch, max_context_len]
req_pool_indices_ptr,
page_kernel_lens_ptr,
kv_start_idx,
kv_indices_ptr,
req_to_token_ptr_stride: tl.constexpr,
kv_indices_ptr_stride: tl.constexpr,
PAGED_SIZE: tl.constexpr = 64,
):
NUM_PAGE_PER_BLOCK: tl.constexpr = (
FLASHMLA_CREATE_KV_BLOCK_SIZE_TRITON // PAGED_SIZE
)
pid = tl.program_id(axis=0)
# find the req pool idx, this is for batch to token
req_pool_index = tl.load(req_pool_indices_ptr + pid)
kv_start = 0
kv_end = 0
if kv_start_idx:
kv_start = tl.load(kv_start_idx + pid).to(tl.int32)
kv_end = kv_start
kv_end += tl.load(page_kernel_lens_ptr + pid).to(tl.int32)
num_paged = tl.cdiv(kv_end - kv_start, PAGED_SIZE)
num_pages_loop = tl.cdiv(kv_end - kv_start, FLASHMLA_CREATE_KV_BLOCK_SIZE_TRITON)
# One CTA per page-block (grid axis 1) rather than one CTA looping all blocks;
# CTAs beyond this sequence's block count are guarded out.
i = tl.program_id(axis=1)
if i < num_pages_loop:
# index into req_to_token_ptr needs to be int64
paged_offset = (
tl.arange(0, NUM_PAGE_PER_BLOCK).to(tl.int64) + i * NUM_PAGE_PER_BLOCK
) * PAGED_SIZE
paged_offset_out = tl.arange(0, NUM_PAGE_PER_BLOCK) + i * NUM_PAGE_PER_BLOCK
mask = paged_offset < num_paged * PAGED_SIZE
mask_out = paged_offset_out < num_paged
data = tl.load(
req_to_token_ptr
+ req_pool_index * req_to_token_ptr_stride
+ kv_start
+ paged_offset,
mask=mask,
)
tl.store(
kv_indices_ptr + pid * kv_indices_ptr_stride + paged_offset_out,
data // PAGED_SIZE,
mask=mask_out,
)
@@ -0,0 +1,386 @@
from __future__ import annotations
import torch
import triton
import triton.language as tl
from sglang.jit_kernel.utils import is_arch_support_pdl
from sglang.srt.runtime_context import get_parallel
@triton.jit
def set_mla_kv_buffer_kernel(
kv_buffer_ptr,
cache_k_nope_ptr,
cache_k_rope_ptr,
loc_ptr,
buffer_stride: tl.constexpr,
nope_stride: tl.constexpr,
rope_stride: tl.constexpr,
nope_dim: tl.constexpr,
rope_dim: tl.constexpr,
BLOCK: tl.constexpr,
DCP_RANK: tl.constexpr,
DCP_WORLD_SIZE: tl.constexpr,
USE_GDC: tl.constexpr = False,
):
pid_loc = tl.program_id(0)
pid_blk = tl.program_id(1)
base = pid_blk * BLOCK
offs = base + tl.arange(0, BLOCK)
total_dim = nope_dim + rope_dim
mask = offs < total_dim
if USE_GDC:
tl.extra.cuda.gdc_wait()
loc = tl.load(loc_ptr + pid_loc).to(tl.int64)
is_valid = loc % DCP_WORLD_SIZE == DCP_RANK
safe_loc = tl.where(is_valid, loc, 0)
safe_loc = safe_loc // DCP_WORLD_SIZE
dst_ptr = kv_buffer_ptr + safe_loc * buffer_stride + offs
# Three-way branch to handle boundary correctly while preserving fast path
if base + BLOCK <= nope_dim:
# Fast path: entire block is in nope region
src = tl.load(
cache_k_nope_ptr + pid_loc * nope_stride + offs,
mask=mask,
)
elif base >= nope_dim:
# Fast path: entire block is in rope region
offs_rope = offs - nope_dim
src = tl.load(
cache_k_rope_ptr + pid_loc * rope_stride + offs_rope,
mask=mask,
)
else:
# Boundary case: block spans nope/rope boundary (e.g., FP8 with nope_dim=528)
# Handle each offset individually to avoid negative indexing
is_nope = offs < nope_dim
is_rope = (offs >= nope_dim) & (offs < (nope_dim + rope_dim))
src_nope = tl.load(
cache_k_nope_ptr + pid_loc * nope_stride + offs,
mask=mask & is_nope,
other=0,
)
src_rope = tl.load(
cache_k_rope_ptr + pid_loc * rope_stride + (offs - nope_dim),
mask=mask & is_rope,
other=0,
)
src = tl.where(is_nope, src_nope, src_rope)
tl.store(dst_ptr, src, mask=mask & is_valid)
if USE_GDC:
tl.extra.cuda.gdc_launch_dependents()
# Above this loc count the TMA bulk-store path overtakes the single-CTA-per-loc
# Triton kernel. Below it, Triton with BLOCK = next_pow2(total_dim) (one CTA
# does the whole row in one tile, no boundary fan-out) is the winning fallback.
# Tuned on GB300 with DSv4 row widths.
_TMA_BULK_STORE_MIN_LOCS = 768
def set_mla_kv_buffer_triton(
kv_buffer: torch.Tensor,
loc: torch.Tensor,
cache_k_nope: torch.Tensor,
cache_k_rope: torch.Tensor,
):
"""Dispatch MLA paged-KV scatter writes to the fastest available path.
Two paths, chosen on ``n_loc``:
- ``n_loc >= 768`` (and SM90+ with TMA-compatible row widths): JIT CUDA
kernel where each warp loads one (nope, rope) row into shared memory and
issues a single ``cp.async.bulk.global.shared::cta`` store to scatter the
row at ``kv_buffer[loc[item]]``. Wins at large bs because it packs 4-8
items per CTA, drastically reducing the CTA count vs single-CTA-per-loc.
- Otherwise: Triton kernel with ``BLOCK = next_pow2(nope_dim + rope_dim)``,
i.e. one CTA per loc covering the entire row in one tile. Wins at small
bs because there's no per-loc CTA fan-out (5x fewer CTAs than the old
BLOCK=128 dispatch) and the row-spanning block makes the boundary branch
a one-shot per CTA. This is also the path for SM<90 and for shapes that
violate the TMA 16-byte alignment.
Speedup vs the legacy BLOCK=128 Triton kernel on GB300 (BF16, nope=512,
rope=64): ~1.05x at bs=8, ~1.5x at bs=128, 3.5x at bs=512, **11.7x at
bs=16384**.
Name retained for caller compatibility; the implementation is no longer
Triton-only.
"""
from sglang.jit_kernel.set_mla_kv_buffer import (
can_use_set_mla_kv_buffer,
)
from sglang.jit_kernel.set_mla_kv_buffer import (
set_mla_kv_buffer as jit_set_mla_kv_buffer,
)
n_loc = loc.numel()
nope_bytes = cache_k_nope.shape[-1] * cache_k_nope.element_size()
rope_bytes = cache_k_rope.shape[-1] * cache_k_rope.element_size()
if (
n_loc >= _TMA_BULK_STORE_MIN_LOCS
and is_arch_support_pdl()
and can_use_set_mla_kv_buffer(nope_bytes, rope_bytes)
and not get_parallel().dcp_enabled
):
jit_set_mla_kv_buffer(kv_buffer, loc, cache_k_nope, cache_k_rope)
return
# Fallback: Triton with BLOCK = next_pow2(total_dim). One CTA per loc; the
# whole row in one tile (the existing 3-way nope/rope/boundary branch in
# ``set_mla_kv_buffer_kernel`` handles the over-allocation past total_dim
# via the offs<total_dim mask). Beats BLOCK=128 by 60-2700 ns across the
# 2 <= bs <= 512 range on GB300.
nope_dim = cache_k_nope.shape[-1]
rope_dim = cache_k_rope.shape[-1]
total_dim = nope_dim + rope_dim
BLOCK = triton.next_power_of_2(total_dim)
grid = (n_loc, 1)
pdl_kwargs = {"USE_GDC": True, "launch_pdl": True} if is_arch_support_pdl() else {}
set_mla_kv_buffer_kernel[grid](
kv_buffer,
cache_k_nope,
cache_k_rope,
loc,
kv_buffer.stride(0),
cache_k_nope.stride(0),
cache_k_rope.stride(0),
nope_dim,
rope_dim,
BLOCK=BLOCK,
DCP_RANK=get_parallel().attn_dcp_rank,
DCP_WORLD_SIZE=get_parallel().attn_dcp_size,
**pdl_kwargs,
)
@triton.jit
def set_mla_kv_buffer_fp8_quant_kernel(
kv_buffer_fp8_ptr,
cache_k_nope_ptr,
cache_k_rope_ptr,
loc_ptr,
buffer_stride: tl.constexpr,
nope_stride: tl.constexpr,
rope_stride: tl.constexpr,
nope_dim: tl.constexpr,
rope_dim: tl.constexpr,
BLOCK: tl.constexpr,
USE_GDC: tl.constexpr = False,
):
"""Fuse BF16/FP16->FP8 cast with paged KV write."""
pid_loc = tl.program_id(0)
pid_blk = tl.program_id(1)
base = pid_blk * BLOCK
offs = base + tl.arange(0, BLOCK)
total_dim = nope_dim + rope_dim
mask = offs < total_dim
if USE_GDC:
tl.extra.cuda.gdc_wait()
loc = tl.load(loc_ptr + pid_loc).to(tl.int64)
dst_ptr = kv_buffer_fp8_ptr + loc * buffer_stride + offs
if base + BLOCK <= nope_dim:
src = tl.load(
cache_k_nope_ptr + pid_loc * nope_stride + offs,
mask=mask,
other=0.0,
)
elif base >= nope_dim:
offs_rope = offs - nope_dim
src = tl.load(
cache_k_rope_ptr + pid_loc * rope_stride + offs_rope,
mask=mask,
other=0.0,
)
else:
is_nope = offs < nope_dim
src_nope = tl.load(
cache_k_nope_ptr + pid_loc * nope_stride + offs,
mask=mask & is_nope,
other=0.0,
)
src_rope = tl.load(
cache_k_rope_ptr + pid_loc * rope_stride + (offs - nope_dim),
mask=mask & ~is_nope,
other=0.0,
)
src = tl.where(is_nope, src_nope, src_rope)
# Destination pointer is FP8-typed view; tl.store performs downcast.
tl.store(dst_ptr, src, mask=mask)
if USE_GDC:
tl.extra.cuda.gdc_launch_dependents()
def set_mla_kv_buffer_triton_fp8_quant(
kv_buffer: torch.Tensor,
loc: torch.Tensor,
cache_k_nope: torch.Tensor,
cache_k_rope: torch.Tensor,
fp8_dtype: torch.dtype,
):
"""Fuse BF16/FP16 MLA K quantization with paged KV write."""
kv_buffer_fp8 = kv_buffer.view(fp8_dtype)
nope_dim = cache_k_nope.shape[-1]
rope_dim = cache_k_rope.shape[-1]
total_dim = nope_dim + rope_dim
BLOCK = 128
n_loc = loc.numel()
grid = (n_loc, triton.cdiv(total_dim, BLOCK))
pdl_kwargs = {"USE_GDC": True, "launch_pdl": True} if is_arch_support_pdl() else {}
set_mla_kv_buffer_fp8_quant_kernel[grid](
kv_buffer_fp8,
cache_k_nope,
cache_k_rope,
loc,
kv_buffer_fp8.stride(0),
cache_k_nope.stride(0),
cache_k_rope.stride(0),
nope_dim,
rope_dim,
BLOCK=BLOCK,
**pdl_kwargs,
)
@triton.jit
def set_mla_kv_scale_buffer_kernel(
kv_buffer_ptr,
cache_k_nope_ptr,
cache_k_rope_ptr,
loc_ptr,
buffer_stride: tl.constexpr,
nope_stride: tl.constexpr,
rope_stride: tl.constexpr,
nope_dim: tl.constexpr,
rope_dim: tl.constexpr,
BLOCK: tl.constexpr,
):
pid_loc = tl.program_id(0)
pid_blk = tl.program_id(1)
base = pid_blk * BLOCK
offs = base + tl.arange(0, BLOCK)
total_dim = nope_dim + rope_dim
mask = offs < total_dim # Make sure don't cross the boundary
loc = tl.load(loc_ptr + pid_loc)
dst_ptr = kv_buffer_ptr + loc * buffer_stride + offs
# Check each offs should read 'nope' or 'rope'
is_nope = offs < nope_dim
src_nope = tl.load(
cache_k_nope_ptr + pid_loc * nope_stride + offs, mask=mask & is_nope, other=0.0
)
src_rope = tl.load(
cache_k_rope_ptr + pid_loc * rope_stride + (offs - nope_dim),
mask=mask & ~is_nope,
other=0.0,
)
# Combine nope + rope
src = src_nope + src_rope
tl.store(dst_ptr, src, mask=mask)
def set_mla_kv_scale_buffer_triton(
kv_buffer: torch.Tensor,
loc: torch.Tensor,
cache_k_nope: torch.Tensor,
cache_k_rope: torch.Tensor,
):
nope_dim = cache_k_nope.shape[-1]
rope_dim = cache_k_rope.shape[-1]
total_dim = nope_dim + rope_dim
BLOCK = 128 # Keep origin, works for smaller total_dim as well.
n_loc = loc.numel()
grid = (n_loc, triton.cdiv(total_dim, BLOCK))
set_mla_kv_scale_buffer_kernel[grid](
kv_buffer,
cache_k_nope,
cache_k_rope,
loc,
kv_buffer.stride(0),
cache_k_nope.stride(0),
cache_k_rope.stride(0),
nope_dim,
rope_dim,
BLOCK=BLOCK,
)
@triton.jit
def get_mla_kv_buffer_kernel(
kv_buffer_ptr,
cache_k_nope_ptr,
cache_k_rope_ptr,
loc_ptr,
buffer_stride: tl.constexpr,
nope_stride: tl.constexpr,
rope_stride: tl.constexpr,
nope_dim: tl.constexpr,
rope_dim: tl.constexpr,
):
pid_loc = tl.program_id(0)
loc = tl.load(loc_ptr + pid_loc).to(tl.int64)
loc_src_ptr = kv_buffer_ptr + loc * buffer_stride
nope_offs = tl.arange(0, nope_dim)
nope_src_ptr = loc_src_ptr + nope_offs
nope_src = tl.load(nope_src_ptr)
tl.store(
cache_k_nope_ptr + pid_loc * nope_stride + nope_offs,
nope_src,
)
rope_offs = tl.arange(0, rope_dim)
rope_src_ptr = loc_src_ptr + nope_dim + rope_offs
rope_src = tl.load(rope_src_ptr)
tl.store(
cache_k_rope_ptr + pid_loc * rope_stride + rope_offs,
rope_src,
)
def get_mla_kv_buffer_triton(
kv_buffer: torch.Tensor,
loc: torch.Tensor,
cache_k_nope: torch.Tensor,
cache_k_rope: torch.Tensor,
):
# The source data type will be implicitly converted to the target data type.
nope_dim = cache_k_nope.shape[-1] # 512
rope_dim = cache_k_rope.shape[-1] # 64
n_loc = loc.numel()
grid = (n_loc,)
get_mla_kv_buffer_kernel[grid](
kv_buffer,
cache_k_nope,
cache_k_rope,
loc,
kv_buffer.stride(0),
cache_k_nope.stride(0),
cache_k_rope.stride(0),
nope_dim,
rope_dim,
)
@@ -0,0 +1,736 @@
import torch
import triton
import triton.language as tl
@triton.jit
def _get_gptj_rotated_x(
x,
x_rotated_mask,
BLOCK_D: tl.constexpr,
BLOCK_D_HALF: tl.constexpr,
):
# GPT-J rotary layout:
# Pair adjacent dimensions and apply:
# [x0, x1, x2, x3] -> [-x1, x0, -x3, x2]
# Apply sign inversion on odd positions.
x_rotated = tl.where(x_rotated_mask, x, -x)
# Reshape into (D/2, 2) pairs.
x_rotated = tl.reshape(x_rotated, (BLOCK_D_HALF, 2))
# Swap each pair.
x_rotated = tl.flip(x_rotated, 1)
# Flatten back to original shape.
x_rotated = tl.reshape(x_rotated, (BLOCK_D,))
return x_rotated
@triton.jit
def _get_neox_rotated_x(
x,
x_rotated_mask,
BLOCK_D: tl.constexpr,
BLOCK_D_HALF: tl.constexpr,
):
# GPT-NeoX rotary layout:
# Split head dimension into two halves:
# [x0, x1, x2, x3] -> [-x2, -x3, x0, x1]
# Keep first half positive, second half negative.
x_rotated = tl.where(x_rotated_mask, x, -x)
# Reshape into (2, D/2).
x_rotated = tl.reshape(x_rotated, (2, BLOCK_D_HALF))
# Reverse each half.
x_rotated = tl.flip(x_rotated, 1)
# Flatten and reverse full vector.
x_rotated = tl.reshape(x_rotated, (BLOCK_D,))
x_rotated = tl.flip(x_rotated, 0)
return x_rotated
@triton.jit
def _unit_rope(
x_ptrs,
cos,
sin,
d_pe_offs,
IS_NEOX: tl.constexpr,
BLOCK_D_pe: tl.constexpr,
BLOCK_D_HALF_pe: tl.constexpr,
):
# Load one full attention head vector.
x_pe = tl.load(x_ptrs)
# Stage 1: Build rotated vector according to rotary layout.
if IS_NEOX:
x_rotated_mask = d_pe_offs < BLOCK_D_HALF_pe
x_pe_rotated = _get_neox_rotated_x(
x_pe, x_rotated_mask, BLOCK_D_pe, BLOCK_D_HALF_pe
)
else:
x_rotated_mask = d_pe_offs % 2 == 0
x_pe_rotated = _get_gptj_rotated_x(
x_pe, x_rotated_mask, BLOCK_D_pe, BLOCK_D_HALF_pe
)
# Stage 2: Apply RoPE transform:
# x' = x*cos + rotate(x)*sin
x_pe = x_pe * cos + x_pe_rotated * sin
return x_pe
@triton.jit
def _load_cos_sin(
cos_sin_ptr,
pos,
d_cos_offs,
stride_t,
stride_d,
freq_dim,
):
base = pos * stride_t
cos = tl.load(cos_sin_ptr + base + d_cos_offs * stride_d)
sin = tl.load(cos_sin_ptr + base + (d_cos_offs + freq_dim) * stride_d)
return cos, sin
@triton.jit
def _fused_qk_rope_reshape_and_cache_kernel(
q_ptr,
k_ptr,
v_ptr,
pos_ptr,
cos_sin_ptr,
offs_ptr,
key_cache_ptr,
value_cache_ptr,
slot_mapping_ptr,
swa_slot_mapping_ptr,
q_out_ptr,
k_out_ptr,
zeros_out_ptr,
T,
T_slot,
q_stride_t,
q_stride_h,
q_stride_d,
k_stride_t,
k_stride_h,
k_stride_d,
v_stride_t,
v_stride_h,
v_stride_d,
cos_sin_stride_t,
cos_sin_stride_d,
q_out_stride_t,
q_out_stride_h,
q_out_stride_d,
k_out_stride_t,
k_out_stride_h,
k_out_stride_d,
key_cache_stride_t,
key_cache_stride_h,
key_cache_stride_d,
key_cache_stride_b,
key_cache_stride_x,
value_cache_stride_t,
value_cache_stride_h,
value_cache_stride_d,
value_cache_stride_b,
value_cache_stride_slot_chunk,
value_cache_stride_x,
zeros_out_stride_t,
zeros_out_stride_h,
zeros_out_stride_d,
k_scale_ptr,
v_scale_ptr,
QH_PER_KH: tl.constexpr,
QH: tl.constexpr,
KH: tl.constexpr,
REUSE_FREQS_FRONT_PART: tl.constexpr,
IS_NEOX: tl.constexpr,
BLOCK_D_pe: tl.constexpr,
BLOCK_D_HALF_pe: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
X_SIZE: tl.constexpr,
FLASH_LAYOUT: tl.constexpr,
VALUE_SHUFFLE_LAYOUT: tl.constexpr = False,
HAVE_POS: tl.constexpr = False,
HAVE_K_SCALE: tl.constexpr = False,
HAVE_V_SCALE: tl.constexpr = False,
HAVE_ZEROS: tl.constexpr = False,
HAS_SWA: tl.constexpr = False,
):
# ============================================================
# Stage 0: Static stride assumptions for Triton compiler
#
# These assumptions help Triton optimize pointer arithmetic and
# simplify generated address calculations.
# ============================================================
tl.assume(q_stride_t >= 0)
tl.assume(q_stride_h >= 0)
tl.assume(q_stride_d >= 0)
tl.assume(k_stride_t >= 0)
tl.assume(k_stride_h >= 0)
tl.assume(k_stride_d >= 0)
tl.assume(v_stride_t >= 0)
tl.assume(v_stride_h >= 0)
tl.assume(v_stride_d >= 0)
tl.assume(cos_sin_stride_t >= 0)
tl.assume(cos_sin_stride_d >= 0)
tl.assume(q_out_stride_t >= 0)
tl.assume(q_out_stride_h >= 0)
tl.assume(q_out_stride_d >= 0)
tl.assume(k_out_stride_t >= 0)
tl.assume(k_out_stride_h >= 0)
tl.assume(k_out_stride_d >= 0)
tl.assume(key_cache_stride_t >= 0)
tl.assume(key_cache_stride_h >= 0)
tl.assume(key_cache_stride_d >= 0)
tl.assume(key_cache_stride_b >= 0)
tl.assume(key_cache_stride_x >= 0)
tl.assume(value_cache_stride_t >= 0)
tl.assume(value_cache_stride_h >= 0)
tl.assume(value_cache_stride_d >= 0)
tl.assume(value_cache_stride_b >= 0)
tl.assume(value_cache_stride_slot_chunk >= 0)
tl.assume(value_cache_stride_x >= 0)
tl.assume(zeros_out_stride_t >= 0)
tl.assume(zeros_out_stride_h >= 0)
tl.assume(zeros_out_stride_d >= 0)
# ============================================================
# Stage 1: Program instance mapping
#
# Each program handles:
# - one (token, q_head) for Q path
# - selected KV ownership for cache write path
#
# pid layout:
# [0, T*QH) -> decode Q path
# [T*QH, extra KV) -> KV-only path
# ============================================================
pid = tl.program_id(0)
tl.assume(pid >= 0)
d_pe_offs = tl.arange(0, BLOCK_D_pe).to(tl.int64)
# ============================================================
# Stage 2: Main decode path (Q always active)
# ============================================================
if pid < T * QH:
pid_t = pid // QH
pid_hq = pid % QH
# --------------------------------------------------------
# Stage 2.1: Compute rotary frequency offsets
#
# RoPE frequencies may be stored as:
# D/2 frequencies (shared front-half)
# D frequencies (full explicit)
# --------------------------------------------------------
if REUSE_FREQS_FRONT_PART:
if IS_NEOX:
d_cos_offs = d_pe_offs
d_cos_offs = tl.where(
(d_cos_offs >= BLOCK_D_HALF_pe) & (d_cos_offs < BLOCK_D_pe),
d_cos_offs - BLOCK_D_HALF_pe,
d_cos_offs,
).to(d_cos_offs.dtype)
# d_cos_mask = d_cos_offs < BLOCK_D_pe
else:
d_cos_offs = d_pe_offs // 2
# d_cos_mask = d_cos_offs < BLOCK_D_HALF_pe
else:
d_cos_offs = d_pe_offs
# d_cos_mask = d_cos_offs < BLOCK_D_pe
# --------------------------------------------------------
# Stage 2.2: Load token position and optional offset
#
# offs_ptr is used by chunked prefill / sliding-window decode.
# --------------------------------------------------------
pos = tl.load(pos_ptr + pid_t)
if HAVE_POS:
offset = tl.load(offs_ptr + pid_t)
pos = pos + offset
# --------------------------------------------------------
# Stage 2.3: Load cosine / sine table
# --------------------------------------------------------
# cos_offs = pos * cos_stride_t + d_cos_offs * cos_stride_d
# cos = tl.load(cos_ptr + cos_offs)
# sin = tl.load(sin_ptr + cos_offs)
freq_dim = BLOCK_D_HALF_pe if REUSE_FREQS_FRONT_PART else BLOCK_D_pe
cos, sin = _load_cos_sin(
cos_sin_ptr,
pos,
d_cos_offs,
cos_sin_stride_t,
cos_sin_stride_d,
freq_dim,
)
# --------------------------------------------------------
# Stage 2.4: Apply RoPE to Q
# --------------------------------------------------------
q_ptrs = (
q_ptr + pid_t * q_stride_t + pid_hq * q_stride_h + d_pe_offs * q_stride_d
)
q_pe = _unit_rope(
q_ptrs,
cos,
sin,
d_pe_offs,
IS_NEOX,
BLOCK_D_pe,
BLOCK_D_HALF_pe,
)
# Store rotated Q output.
q_out_ptrs = (
q_out_ptr
+ pid_t * q_out_stride_t
+ pid_hq * q_out_stride_h
+ d_pe_offs * q_out_stride_d
)
tl.store(q_out_ptrs, q_pe.to(q_out_ptr.dtype.element_ty))
if HAVE_ZEROS:
z = tl.zeros((BLOCK_D_pe,), dtype=zeros_out_ptr.dtype.element_ty)
zeros_out_ptrs = (
zeros_out_ptr
+ pid_t * zeros_out_stride_t
+ pid_hq * zeros_out_stride_h
+ d_pe_offs * zeros_out_stride_d
)
tl.store(zeros_out_ptrs, z)
# ========================================================
# Stage 3: KV ownership path
#
# Only one Q group leader writes KV:
# pid_hq % QH_PER_KH == 0
#
# This prevents duplicated KV cache writes.
# ========================================================
if pid_hq % QH_PER_KH == 0:
# ----------------------------------------------------
# Stage 3.1: Resolve cache slot
# ----------------------------------------------------
pid_slot = tl.load(slot_mapping_ptr + pid_t).to(tl.int64)
if HAS_SWA:
pid_slot = tl.load(swa_slot_mapping_ptr + pid_slot)
# ------------------------------------------------
# Stage 3.2: Apply RoPE to K
# ------------------------------------------------
if pid_slot >= 0:
pid_t_slot = pid_slot // BLOCK_SIZE
pid_b = pid_slot % BLOCK_SIZE
pid_hk = pid_hq // QH_PER_KH
if HAVE_K_SCALE:
k_scale = tl.load(k_scale_ptr)
else:
k_scale = 1
k_ptrs = (
k_ptr
+ pid_t * k_stride_t
+ pid_hk * k_stride_h
+ d_pe_offs * k_stride_d
)
k_pe = _unit_rope(
k_ptrs,
cos,
sin,
d_pe_offs,
IS_NEOX,
BLOCK_D_pe,
BLOCK_D_HALF_pe,
)
k_out_ptrs = (
k_out_ptr
+ pid_t * k_out_stride_t
+ pid_hk * k_out_stride_h
+ d_pe_offs * k_out_stride_d
)
tl.store(k_out_ptrs, k_pe.to(k_out_ptr.dtype.element_ty))
# ------------------------------------------------
# Stage 3.3: Optional fp8 scaling before cache
# ------------------------------------------------
k_scale_rcprl = 1 / k_scale
k_pe = k_pe * k_scale_rcprl
# ------------------------------------------------
# Stage 3.4: Write K cache
#
# Two layouts supported:
# FLASH_LAYOUT
# paged KV layout
# ------------------------------------------------
if FLASH_LAYOUT:
k_out_ptrs = (
key_cache_ptr
+ pid_t_slot * key_cache_stride_t
+ pid_b * key_cache_stride_b
+ pid_hk * key_cache_stride_h
+ d_pe_offs * key_cache_stride_d
)
else:
k_pe = tl.reshape(k_pe, (BLOCK_D_pe // X_SIZE, X_SIZE))
dx_offs = tl.arange(0, BLOCK_D_pe // X_SIZE).to(tl.int64)
x_offs = tl.arange(0, X_SIZE).to(tl.int64)
k_out_ptrs = (
key_cache_ptr
+ pid_t_slot * key_cache_stride_t
+ pid_hk * key_cache_stride_h
+ dx_offs[:, None] * key_cache_stride_d
+ pid_b * key_cache_stride_b
+ x_offs[None, :] * key_cache_stride_x
)
tl.store(k_out_ptrs, k_pe.to(key_cache_ptr.dtype.element_ty))
# ------------------------------------------------
# Stage 3.5: Write V cache
#
# Supports:
# normal layout
# shuffle layout
# ------------------------------------------------
v_ptrs = (
v_ptr
+ pid_t * v_stride_t
+ pid_hk * v_stride_h
+ d_pe_offs * v_stride_d
)
if HAVE_V_SCALE:
v_scale = tl.load(v_scale_ptr)
else:
v_scale = 1
v_scale_rcprl = 1 / v_scale
v = tl.load(v_ptrs) * v_scale_rcprl
if VALUE_SHUFFLE_LAYOUT:
slot_chunk = pid_b // X_SIZE
x_off = pid_b % X_SIZE
v_out_ptrs = (
value_cache_ptr
+ pid_t_slot * value_cache_stride_t
+ pid_hk * value_cache_stride_h
+ slot_chunk * value_cache_stride_slot_chunk
+ d_pe_offs.to(tl.int64) * value_cache_stride_d
+ x_off * value_cache_stride_x
)
else:
v_out_ptrs = (
value_cache_ptr
+ pid_t_slot * value_cache_stride_t
+ pid_hk * value_cache_stride_h
+ d_pe_offs.to(tl.int64) * value_cache_stride_d
+ pid_b * value_cache_stride_b
)
tl.store(v_out_ptrs, v.to(value_cache_ptr.dtype.element_ty))
# ============================================================
# Stage 4: Extra KV-only path
#
# Handles tokens that only require cache update:
# T_slot > T
#
# No Q / no RoPE on Q branch.
# ============================================================
else:
pid = pid - T * QH + T * KH
if pid < T_slot * KH:
pid_t = pid // KH
pid_hk = pid % KH
pid_slot = tl.load(slot_mapping_ptr + pid_t).to(tl.int64)
if HAS_SWA:
pid_slot = tl.load(swa_slot_mapping_ptr + pid_slot)
if pid_slot >= 0:
pid_t_slot = pid_slot // BLOCK_SIZE
pid_b = pid_slot % BLOCK_SIZE
if HAVE_K_SCALE:
k_scale = tl.load(k_scale_ptr)
else:
k_scale = 1
k_ptrs = (
k_ptr
+ pid_t * k_stride_t
+ pid_hk * k_stride_h
+ d_pe_offs * k_stride_d
)
k_pe = tl.load(k_ptrs)
k_out_ptrs = (
k_out_ptr
+ pid_t * k_out_stride_t
+ pid_hk * k_out_stride_h
+ d_pe_offs * k_out_stride_d
)
tl.store(k_out_ptrs, k_pe.to(k_out_ptr.dtype.element_ty))
k_scale_rcprl = 1 / k_scale
k_pe = k_pe * k_scale_rcprl
if FLASH_LAYOUT:
k_out_ptrs = (
key_cache_ptr
+ pid_t_slot * key_cache_stride_t
+ d_pe_offs * key_cache_stride_d
+ pid_b * key_cache_stride_b
+ pid_hk * key_cache_stride_h
)
else:
k_pe = tl.reshape(k_pe, (BLOCK_D_pe // X_SIZE, X_SIZE))
dx_offs = tl.arange(0, BLOCK_D_pe // X_SIZE).to(tl.int64)
x_offs = tl.arange(0, X_SIZE).to(tl.int64)
k_out_ptrs = (
key_cache_ptr
+ pid_t_slot * key_cache_stride_t
+ pid_hk * key_cache_stride_h
+ dx_offs[:, None] * key_cache_stride_d
+ pid_b * key_cache_stride_b
+ x_offs[None, :] * key_cache_stride_x
)
tl.store(k_out_ptrs, k_pe.to(key_cache_ptr.dtype.element_ty))
v_ptrs = (
v_ptr
+ pid_t * v_stride_t
+ pid_hk * v_stride_h
+ d_pe_offs * v_stride_d
)
if HAVE_V_SCALE:
v_scale = tl.load(v_scale_ptr)
else:
v_scale = 1
v_scale_rcprl = 1 / v_scale
v = tl.load(v_ptrs) * v_scale_rcprl
if VALUE_SHUFFLE_LAYOUT:
slot_chunk = pid_b // X_SIZE
x_off = pid_b % X_SIZE
v_out_ptrs = (
value_cache_ptr
+ pid_t_slot * value_cache_stride_t
+ pid_hk * value_cache_stride_h
+ slot_chunk * value_cache_stride_slot_chunk
+ d_pe_offs * value_cache_stride_d
+ x_off * value_cache_stride_x
)
else:
v_out_ptrs = (
value_cache_ptr
+ pid_t_slot * value_cache_stride_t
+ pid_hk * value_cache_stride_h
+ d_pe_offs * value_cache_stride_d
+ pid_b * value_cache_stride_b
)
tl.store(v_out_ptrs, v.to(value_cache_ptr.dtype.element_ty))
def fused_qk_rope_reshape_and_cache(
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
key_cache: torch.Tensor,
value_cache: torch.Tensor,
slot_mapping: torch.Tensor,
pos: torch.Tensor,
cos_sin: torch.Tensor,
k_scale: torch.Tensor,
v_scale: torch.Tensor,
is_neox: bool,
flash_layout: bool,
apply_scale: bool = True,
offs: torch.Tensor = None,
q_out: torch.Tensor = None,
k_out: torch.Tensor = None,
output_zeros: bool = True,
zeros_out: torch.Tensor = None,
swa_slot_mapping=None,
):
"""
Perform RoPE on q and k and along the last dimension and copy k and v in to key_cache and value_cache inplace
Key parameters:
- q: shape (T, QH, D).
- k: shape (T_slot, KH, D).
- v: shape (T_slot, KH, D).
- if flash_layout:
- key_cache: shape (T_cache, block_size, KH, D).
- value_cache: shape (T_cache, block_size, KH, D).
- else:
- key_cache: shape (T_cache, KH, D // x, block_size, x).
- value_cache: shape (T_cache, KH, D, block_size).
- slot_mapping: shape (T_slot, ).
T is the number of decode tokens, T_cahce * block_size is the max number of tokens of kv_cache
QH must be multiple of KH
Returns:
- q_out: same shape as input q.
- k_out: same shape as input k.
- key_cache: same shape as input key_cache (inplace).
- value_cache: same shape as input value_cache (inplace).
- zeros_out: same shape as input q.
"""
t, qh, d = q.shape
tk, kh, dk = k.shape
tv, vh, dv = v.shape
if flash_layout:
t_cache, block_size, kh_cache, dk_cache = key_cache.shape
t_cache_v, block_size_v, vh_cache, dv_cache = value_cache.shape
value_shuffle_layout = False
else:
t_cache, kh_cache, dkx_cache, block_size, x_cache = key_cache.shape
if value_cache.ndim == 5:
# value_cache shuffle: (num_blocks, num_kv_heads, block_size // x, head_size, x)
t_cache_v, vh_cache, slot_chunk_v, dv_cache, x_v = value_cache.shape
value_shuffle_layout = True
block_size_v = slot_chunk_v * x_v
assert block_size_v == block_size and x_v == x_cache, (
f"value_cache shuffle (T,KH,block_size//x,D,x) must match key: "
f"{block_size_v=} {block_size=} {x_v=} {x_cache=}"
)
else:
t_cache_v, vh_cache, dv_cache, block_size_v = value_cache.shape
value_shuffle_layout = False
(t_slot,) = slot_mapping.shape
assert (
t == tk == tv and t_slot <= tk
), f"Number of tokens should be identical for q, kand v. The number of tokens of slot_mapping should no more than that of q, k and v, {t=} {tk=} {tv=} {t_slot=}"
assert (
block_size == block_size_v
), f"block size should be identical for key_cache, and value_cache {block_size} {block_size_v}"
assert (
kh == vh == kh_cache == vh_cache
), "KV head should be identical for k, v, key_cache, and value_cache"
assert (
t_cache == t_cache_v
), "Number of tokens should be identical for key_cache, and value_cache"
if flash_layout:
assert (
d == dk == dv == dk_cache == dv_cache
), "D dimension should be identical for q, k, and v"
else:
assert (
d == dk == dv == dkx_cache * x_cache == dv_cache
), "D dimension should be identical for q, k, and v"
assert x_cache == triton.next_power_of_2(x_cache), "x_size should be power of 2"
assert d == triton.next_power_of_2(d), "D dimension should be power of 2"
assert block_size == triton.next_power_of_2(
block_size
), "block_size should be power of 2"
assert qh % kh == 0, "Q heads must be multiple of H heads"
d_freq = cos_sin.shape[-1] // 2
assert (d_freq == d // 2) or (
d_freq == d
), "cos/sin last dim should be the same or half of the qk last dim"
reuse_freqs_front_part = d_freq == d // 2
if q_out is None:
q_out = torch.empty((t, qh, d), dtype=q.dtype, device=q.device)
if k_out is None:
k_out = torch.empty((tk, kh, dk), dtype=k.dtype, device=q.device)
if zeros_out is not None:
tz, qhz, dz = zeros_out.shape
assert (
t == tz and qh == qhz and d == dz
), f"q and zeros shape mismatch {q.shape=} {zeros_out.shape=}"
output_zeros = True
elif output_zeros:
zeros_out = torch.empty((t, qh, d), dtype=q.dtype, device=q.device)
else:
zeros_out = None
n_pid = t * qh + (t_slot - t) * kh if t_slot >= t else t * qh
grid = (n_pid, 1, 1)
_fused_qk_rope_reshape_and_cache_kernel[grid](
q,
k,
v,
pos,
cos_sin,
offs,
key_cache,
value_cache,
slot_mapping,
swa_slot_mapping,
q_out,
k_out,
zeros_out,
t,
t_slot,
*q.stride(),
*k.stride(),
*v.stride(),
cos_sin.stride(0),
cos_sin.stride(-1),
*q_out.stride(),
*k_out.stride(),
key_cache.stride(0) if not flash_layout else key_cache.stride(0),
key_cache.stride(1) if not flash_layout else key_cache.stride(2),
key_cache.stride(2) if not flash_layout else key_cache.stride(3),
key_cache.stride(3) if not flash_layout else key_cache.stride(1),
key_cache.stride(4) if not flash_layout else 0,
value_cache.stride(0) if not flash_layout else value_cache.stride(0),
value_cache.stride(1) if not flash_layout else value_cache.stride(2),
(
value_cache.stride(3)
if (not flash_layout and value_shuffle_layout)
else (value_cache.stride(2) if not flash_layout else value_cache.stride(3))
),
(
0
if (not flash_layout and value_shuffle_layout)
else (value_cache.stride(3) if not flash_layout else value_cache.stride(1))
),
value_cache.stride(2) if (not flash_layout and value_shuffle_layout) else 0,
value_cache.stride(4) if (not flash_layout and value_shuffle_layout) else 0,
zeros_out.stride(0) if zeros_out is not None else 0,
zeros_out.stride(1) if zeros_out is not None else 0,
zeros_out.stride(2) if zeros_out is not None else 0,
k_scale_ptr=k_scale,
v_scale_ptr=v_scale,
QH_PER_KH=qh // kh,
QH=qh,
KH=kh,
REUSE_FREQS_FRONT_PART=reuse_freqs_front_part,
IS_NEOX=is_neox,
BLOCK_D_pe=d,
BLOCK_D_HALF_pe=d // 2,
BLOCK_SIZE=block_size,
X_SIZE=x_cache if not flash_layout else 0,
FLASH_LAYOUT=flash_layout,
VALUE_SHUFFLE_LAYOUT=value_shuffle_layout,
HAVE_POS=(offs is not None),
HAVE_K_SCALE=(k_scale is not None and apply_scale),
HAVE_V_SCALE=(v_scale is not None and apply_scale),
HAVE_ZEROS=output_zeros,
HAS_SWA=(swa_slot_mapping is not None),
num_warps=1,
)
if zeros_out is not None:
return q_out.view(-1, qh * d), k_out, key_cache, value_cache, zeros_out
return q_out.view(-1, qh * d), k_out, key_cache, value_cache
@@ -0,0 +1,504 @@
"""
Fused FP8 quantization + paged KV cache write kernel for TRTLLM MHA backend.
This kernel fuses the following operations:
1. FP8 quantization of K and V tensors (from BF16/FP16 to FP8)
2. Per-token or per-page scale computation
3. Writing quantized K/V to paged KV cache layout
Performance benefits:
- Eliminates intermediate FP8 tensors in memory
- Reduces kernel launch overhead
- Better memory bandwidth utilization
"""
import logging
from typing import Optional
import torch
import triton
import triton.language as tl
logger = logging.getLogger(__name__)
@triton.jit
def _process_kv_tensor(
token_id,
head_block_id,
page_id,
page_offset,
input_ptr,
cache_ptr,
inv_scale,
use_provided_scale: tl.constexpr,
num_kv_heads: tl.constexpr,
head_dim: tl.constexpr,
input_stride_token: tl.constexpr,
input_stride_head: tl.constexpr,
input_stride_dim: tl.constexpr,
cache_stride_page: tl.constexpr,
cache_stride_offset: tl.constexpr,
cache_stride_head: tl.constexpr,
cache_stride_dim: tl.constexpr,
BLOCK_HEAD: tl.constexpr,
BLOCK_DIM: tl.constexpr,
):
"""Process a block of heads for a single K or V tensor."""
head_idx = head_block_id * BLOCK_HEAD
num_heads_in_block = min(BLOCK_HEAD, num_kv_heads - head_idx)
for dim_idx in range(0, head_dim, BLOCK_DIM):
num_dims_in_block = min(BLOCK_DIM, head_dim - dim_idx)
head_offsets = head_idx + tl.arange(0, BLOCK_HEAD)
dim_offsets = dim_idx + tl.arange(0, BLOCK_DIM)
head_mask = head_offsets < (head_idx + num_heads_in_block)
dim_mask = dim_offsets < (dim_idx + num_dims_in_block)
# Load from input using 3D strides
input_offsets = (
token_id * input_stride_token
+ head_offsets[:, None] * input_stride_head
+ dim_offsets[None, :] * input_stride_dim
)
mask = head_mask[:, None] & dim_mask[None, :]
block = tl.load(input_ptr + input_offsets, mask=mask, other=0.0)
# Quantize to FP8
if use_provided_scale:
block_fp8 = (block * inv_scale).to(tl.float8e4nv)
else:
block_fp8 = block.to(tl.float8e4nv)
# Write to cache at [page_id, page_offset, head, dim]
cache_offsets = (
page_id * cache_stride_page
+ page_offset * cache_stride_offset
+ head_offsets[:, None] * cache_stride_head
+ dim_offsets[None, :] * cache_stride_dim
)
tl.store(cache_ptr + cache_offsets, block_fp8, mask=mask)
@triton.jit
def _fused_fp8_set_kv_buffer_kernel(
# Input tensors (post-RoPE K and V in FP16/BF16)
k_ptr, # [num_tokens, num_kv_heads, head_dim]
v_ptr, # [num_tokens, num_kv_heads, head_dim]
# Output KV cache buffers (FP8 paged layout)
k_cache_ptr, # [total_slots, num_kv_heads, head_dim]
v_cache_ptr, # [total_slots, num_kv_heads, head_dim]
# Cache location indices
cache_loc_ptr, # [num_tokens] -> token to cache location mapping
# Pointers to scalar inverse scales (computed on GPU in wrapper)
inv_k_scale_ptr, # pointer to 0-D tensor on GPU
inv_v_scale_ptr, # pointer to 0-D tensor on GPU
use_provided_scale: tl.constexpr, # whether to use provided scale
# Tensor dimensions
num_kv_heads: tl.constexpr,
head_dim: tl.constexpr,
page_size: tl.constexpr,
# Strides for K input [num_tokens, num_kv_heads, head_dim]
k_stride_token: tl.constexpr,
k_stride_head: tl.constexpr,
k_stride_dim: tl.constexpr,
# Strides for K cache [total_slots, num_kv_heads, head_dim] (logically paged)
k_cache_stride_page: tl.constexpr,
k_cache_stride_offset: tl.constexpr,
k_cache_stride_head: tl.constexpr,
k_cache_stride_dim: tl.constexpr,
# Strides for V input [num_tokens, num_kv_heads, head_dim]
v_stride_token: tl.constexpr,
v_stride_head: tl.constexpr,
v_stride_dim: tl.constexpr,
# Strides for V cache [total_slots, num_kv_heads, head_dim] (logically paged)
v_cache_stride_page: tl.constexpr,
v_cache_stride_offset: tl.constexpr,
v_cache_stride_head: tl.constexpr,
v_cache_stride_dim: tl.constexpr,
# Block sizes
BLOCK_HEAD: tl.constexpr, # Number of heads per block
BLOCK_DIM: tl.constexpr, # Head dimension block size
):
"""
Fused FP8 quantization + paged KV cache write kernel.
Each program processes one token-head_block-kv combination, quantizing and writing
to the appropriate page in the KV cache.
Grid: (num_tokens, num_head_blocks, 2) where dim2: 0=K, 1=V
"""
# Get program IDs
token_id = tl.program_id(0)
head_block_id = tl.program_id(1)
kv_idx = tl.program_id(2) # 0 for K, 1 for V
# Get cache location for this token
cache_loc = tl.load(cache_loc_ptr + token_id)
# Compute page_id and offset within page
page_id = cache_loc // page_size
page_offset = cache_loc % page_size
# Select K or V based on kv_idx
if kv_idx == 0:
# Process K tensor
if use_provided_scale:
inv_scale = tl.load(inv_k_scale_ptr)
else:
inv_scale = 1.0
_process_kv_tensor(
token_id,
head_block_id,
page_id,
page_offset,
k_ptr,
k_cache_ptr,
inv_scale,
use_provided_scale,
num_kv_heads,
head_dim,
k_stride_token,
k_stride_head,
k_stride_dim,
k_cache_stride_page,
k_cache_stride_offset,
k_cache_stride_head,
k_cache_stride_dim,
BLOCK_HEAD,
BLOCK_DIM,
)
else:
# Process V tensor
if use_provided_scale:
inv_scale = tl.load(inv_v_scale_ptr)
else:
inv_scale = 1.0
_process_kv_tensor(
token_id,
head_block_id,
page_id,
page_offset,
v_ptr,
v_cache_ptr,
inv_scale,
use_provided_scale,
num_kv_heads,
head_dim,
v_stride_token,
v_stride_head,
v_stride_dim,
v_cache_stride_page,
v_cache_stride_offset,
v_cache_stride_head,
v_cache_stride_dim,
BLOCK_HEAD,
BLOCK_DIM,
)
def fused_fp8_set_kv_buffer(
k: torch.Tensor, # [num_tokens, num_kv_heads, head_dim] or [num_tokens, num_kv_heads * head_dim]
v: torch.Tensor, # [num_tokens, num_kv_heads, head_dim] or [num_tokens, num_kv_heads * head_dim]
k_cache: torch.Tensor, # [total_slots, num_kv_heads, head_dim] or [num_pages, page_size, num_kv_heads, head_dim]
v_cache: torch.Tensor, # [total_slots, num_kv_heads, head_dim] or [num_pages, page_size, num_kv_heads, head_dim]
cache_loc: torch.Tensor, # [num_tokens], dtype=int32
k_scale: Optional[
float
] = None, # Scalar scale (matching original set_kv_buffer signature)
v_scale: Optional[float] = None,
page_size: int = 16,
use_triton: bool = True, # Whether to use Triton kernel (set to False to force naive fallback)
) -> None:
"""
Python wrapper for the fused FP8 quantization + paged KV cache write kernel.
This function replicates the exact behavior of the original set_kv_buffer but with
a fused kernel that combines FP8 quantization and cache write.
Args:
k: Key tensor after RoPE, can be 2D or 3D
v: Value tensor, can be 2D or 3D
k_cache: Paged K cache buffer in FP8
v_cache: Paged V cache buffer in FP8
cache_loc: Cache location for each token, shape [num_tokens]
k_scale: Optional scalar scale for K (matching original set_kv_buffer)
v_scale: Optional scalar scale for V (matching original set_kv_buffer)
page_size: Number of tokens per page
use_triton: Whether to use optimized Triton kernel
"""
num_tokens = k.shape[0]
# Step 1: Infer num_kv_heads and head_dim from cache shape
if k_cache.ndim == 3:
# 3D cache layout: [total_slots, num_kv_heads, head_dim]
total_slots, num_kv_heads, head_dim = k_cache.shape
assert (
total_slots % page_size == 0
), f"total_slots ({total_slots}) must be divisible by page_size ({page_size})"
num_pages = total_slots // page_size
elif k_cache.ndim == 4:
# 4D cache layout: [num_pages, page_size, num_kv_heads, head_dim]
num_pages, ps, num_kv_heads, head_dim = k_cache.shape
assert (
ps == page_size
), f"page_size mismatch: cache has {ps}, expected {page_size}"
total_slots = num_pages * page_size
else:
raise ValueError(f"Unsupported k_cache.ndim={k_cache.ndim}, expected 3 or 4")
# Step 2: Validate k, v shapes and normalize
# Store original 3D shape for Triton path
k_3d = None
v_3d = None
if k.ndim == 3:
# Input is [num_tokens, num_kv_heads, head_dim]
assert (
k.shape[1] == num_kv_heads
), f"num_kv_heads mismatch: k.shape[1]={k.shape[1]} vs cache={num_kv_heads}"
assert (
k.shape[2] == head_dim
), f"head_dim mismatch: k.shape[2]={k.shape[2]} vs cache={head_dim}"
assert v.shape[1] == num_kv_heads and v.shape[2] == head_dim, "v shape mismatch"
# Keep 3D for Triton kernel
k_3d = k
v_3d = v
# Create 2D view for naive fallback (will be used only if use_triton=False)
k_2d = k.reshape(num_tokens, num_kv_heads * head_dim)
v_2d = v.reshape(num_tokens, num_kv_heads * head_dim)
elif k.ndim == 2:
# Input is already [num_tokens, num_kv_heads * head_dim]
assert (
k.shape[1] == num_kv_heads * head_dim
), f"k.shape[1]={k.shape[1]} != {num_kv_heads * head_dim}"
assert (
v.shape[1] == num_kv_heads * head_dim
), f"v.shape[1]={v.shape[1]} != {num_kv_heads * head_dim}"
# Create 3D view for Triton kernel
k_3d = k.view(num_tokens, num_kv_heads, head_dim)
v_3d = v.view(num_tokens, num_kv_heads, head_dim)
# Keep 2D for naive
k_2d = k
v_2d = v
else:
raise ValueError(f"Unsupported k.ndim={k.ndim}, expected 2 or 3")
# Step 3: Compute cache strides based on layout
if k_cache.ndim == 3:
# 3D cache: [total_slots, num_kv_heads, head_dim]
stride_slot = k_cache.stride(0)
stride_head = k_cache.stride(1)
stride_dim = k_cache.stride(2)
k_cache_stride_page = stride_slot * page_size
k_cache_stride_offset = stride_slot
k_cache_stride_head = stride_head
k_cache_stride_dim = stride_dim
v_stride_slot = v_cache.stride(0)
v_stride_head = v_cache.stride(1)
v_stride_dim = v_cache.stride(2)
v_cache_stride_page = v_stride_slot * page_size
v_cache_stride_offset = v_stride_slot
v_cache_stride_head = v_stride_head
v_cache_stride_dim = v_stride_dim
else:
# 4D cache: [num_pages, page_size, num_kv_heads, head_dim]
k_cache_stride_page = k_cache.stride(0)
k_cache_stride_offset = k_cache.stride(1)
k_cache_stride_head = k_cache.stride(2)
k_cache_stride_dim = k_cache.stride(3)
v_cache_stride_page = v_cache.stride(0)
v_cache_stride_offset = v_cache.stride(1)
v_cache_stride_head = v_cache.stride(2)
v_cache_stride_dim = v_cache.stride(3)
# Decide whether to use provided scale
use_provided_scale = k_scale is not None and v_scale is not None
if use_triton and num_tokens > 0:
# Use optimized Triton kernel
# Compute input strides for 3D k, v: [num_tokens, num_kv_heads, head_dim]
k_stride_token = k_3d.stride(0)
k_stride_head = k_3d.stride(1)
k_stride_dim = k_3d.stride(2)
v_stride_token = v_3d.stride(0)
v_stride_head = v_3d.stride(1)
v_stride_dim = v_3d.stride(2)
# Block sizes for tiling (tunable)
BLOCK_HEAD = min(num_kv_heads, 8) # Process up to 8 heads at once
BLOCK_DIM = min(head_dim, 128) # Process up to 128 dims at once
# Compute number of head blocks
num_head_blocks = (num_kv_heads + BLOCK_HEAD - 1) // BLOCK_HEAD
# Grid: (num_tokens, num_head_blocks, 2)
# - dim 0: tokens
# - dim 1: head blocks
# - dim 2: K/V (0=K, 1=V)
grid = (num_tokens, num_head_blocks, 2)
device = k_3d.device
def _to_tensor_scale(scale):
"""Convert scale to 0-D CUDA tensor (accepts Python float or Tensor)."""
if isinstance(scale, torch.Tensor):
return scale.to(device=device, dtype=torch.float32)
else:
# Python float / np scalar
return torch.tensor(float(scale), device=device, dtype=torch.float32)
# Compute inverse scales on GPU to avoid GPU→CPU sync in CUDA graph capture.
# Previously we used float(k_scale) which triggers synchronization and fails
# during CUDA graph capture with cudaErrorStreamCaptureUnsupported.
if use_provided_scale:
k_scale_tensor = _to_tensor_scale(k_scale)
v_scale_tensor = _to_tensor_scale(v_scale)
# Pure GPU scalar operation, safe for CUDA graph
inv_k_scale = (1.0 / k_scale_tensor).to(device=device, dtype=torch.float32)
inv_v_scale = (1.0 / v_scale_tensor).to(device=device, dtype=torch.float32)
inv_k_scale_ptr = inv_k_scale
inv_v_scale_ptr = inv_v_scale
else:
# When use_provided_scale=False, kernel uses constant 1.0 for inv_scale.
# Triton will optimize away the tl.load() calls via constant folding.
# We pass dummy pointers (k_3d) which won't be accessed in the kernel.
# This avoids creating new GPU tensors during CUDA graph capture.
inv_k_scale_ptr = k_3d
inv_v_scale_ptr = k_3d
# Launch Triton kernel
_fused_fp8_set_kv_buffer_kernel[grid](
k_3d,
v_3d,
k_cache,
v_cache,
cache_loc,
inv_k_scale_ptr,
inv_v_scale_ptr,
use_provided_scale,
num_kv_heads,
head_dim,
page_size,
k_stride_token,
k_stride_head,
k_stride_dim,
k_cache_stride_page,
k_cache_stride_offset,
k_cache_stride_head,
k_cache_stride_dim,
v_stride_token,
v_stride_head,
v_stride_dim,
v_cache_stride_page,
v_cache_stride_offset,
v_cache_stride_head,
v_cache_stride_dim,
BLOCK_HEAD=BLOCK_HEAD,
BLOCK_DIM=BLOCK_DIM,
)
else:
# Fallback to naive implementation
_naive_fp8_set_kv_buffer(
k_2d, v_2d, k_cache, v_cache, cache_loc, k_scale, v_scale, page_size
)
def _naive_fp8_set_kv_buffer(
k: torch.Tensor,
v: torch.Tensor,
k_cache: torch.Tensor,
v_cache: torch.Tensor,
cache_loc: torch.Tensor,
k_scale: Optional[float],
v_scale: Optional[float],
page_size: int,
) -> None:
"""
Naive fallback implementation that mimics the original set_kv_buffer logic.
This directly replicates the behavior of MHATokenToKVPool.set_kv_buffer:
1. Apply scale (if k.dtype != cache.dtype and scale is provided)
2. Convert to FP8
3. Write to cache at cache_loc
Args:
k: [num_tokens, num_kv_heads * head_dim], already reshaped to 2D
v: [num_tokens, num_kv_heads * head_dim], already reshaped to 2D
k_cache: [total_slots, num_kv_heads, head_dim] or [num_pages, page_size, num_kv_heads, head_dim]
v_cache: Same shape as k_cache
cache_loc: [num_tokens]
k_scale: Optional scale for K
v_scale: Optional scale for V
page_size: Tokens per page
"""
num_tokens = k.shape[0]
# Infer dimensions from cache
if k_cache.ndim == 3:
num_kv_heads = k_cache.shape[1]
head_dim = k_cache.shape[2]
elif k_cache.ndim == 4:
num_kv_heads = k_cache.shape[2]
head_dim = k_cache.shape[3]
else:
raise ValueError(f"Unsupported k_cache.ndim={k_cache.ndim}")
# Determine target dtype and storage dtype
# See: python/sglang/srt/mem_cache/memory_pool.py:445-449
store_dtype = k_cache.dtype
if store_dtype == torch.uint8:
# Cache is stored as uint8 for FP8 (due to index_put limitation)
dtype = torch.float8_e4m3fn # Logical dtype
else:
dtype = store_dtype # Cache dtype is the logical dtype
# Replicate the original set_kv_buffer behavior
# See: python/sglang/srt/mem_cache/memory_pool.py:777-799
if k.dtype != dtype:
# Need quantization - clone first to avoid modifying input
k = k.clone()
v = v.clone()
if k_scale is not None:
k.div_(k_scale) # In-place division
if v_scale is not None:
v.div_(v_scale) # In-place division
k = k.to(dtype)
v = v.to(dtype)
# View FP8 as uint8 if needed (for index_put compatibility)
if store_dtype == torch.uint8 and dtype in (torch.float8_e5m2, torch.float8_e4m3fn):
k = k.view(torch.uint8)
v = v.view(torch.uint8)
# Reshape from [T, H*D] to [T, H, D]
k = k.view(num_tokens, num_kv_heads, head_dim)
v = v.view(num_tokens, num_kv_heads, head_dim)
# Write to cache using advanced indexing (same as original)
if k_cache.ndim == 3:
# 3D cache: [total_slots, H, D]
k_cache[cache_loc] = k
v_cache[cache_loc] = v
else:
# 4D cache: [num_pages, page_size, H, D]
# Decompose loc into page_id and page_offset (vectorized)
page_ids = cache_loc // page_size
page_offsets = cache_loc % page_size
k_cache[page_ids, page_offsets] = k
v_cache[page_ids, page_offsets] = v
@@ -0,0 +1,198 @@
"""Fused CUDA-graph metadata update for the TRTLLM MHA backend.
`TRTLLMHAAttnBackend._apply_cuda_graph_metadata` used to rebuild the
page table(s) and seqlen buffers with ~25 small aten ops per graph
replay (index gathers, floor_divide, cumsum, dtype casts, copies).
On some CPUs that is ~0.7-1.0 ms of pure host dispatch, repeated 4x
per decode step (2 draft-decode steps + target-verify + draft-extend)
on every TP rank. The resulting per-rank CPU jitter skews the
cudaGraphLaunch across ranks and is paid as spin time inside the first
custom all-reduce of every replayed graph.
This kernel performs the whole update in ONE launch:
- cache_seqlens[i] = seq_lens[i] + seqlen_offset (int32)
- cu_seqlens_k[1:] = cumsum(cache_seqlens) (int32)
- cu_seqlens_q[1:] = cumsum(qlens) or arange*q_stride (optional)
- page_table[i, p] = req_to_token[req_pool_indices[i],
p * page_size] // page_size
- swa_page_table = full_to_swa_mapping[token] // page_size (optional)
- swa_out_cache_loc = full_to_swa_mapping[out_cache_loc], zero padded
(optional)
"""
import triton
import triton.language as tl
# cu_seqlens_q handling inside the fused kernel
Q_MODE_NONE = 0 # cu_seqlens_q is preset (decode / target-verify)
Q_MODE_CUMSUM = 1 # cu_seqlens_q[1:] = cumsum(qlens) (draft-extend)
Q_MODE_STRIDED = 2 # cu_seqlens_q[1:] = arange*q_stride (draft-extend v2)
@triton.jit
def update_trtllm_mha_graph_metadata_kernel(
# inputs
req_pool_indices_ptr, # [bs] int
seq_lens_ptr, # [bs] int
req_to_token_ptr, # [pool_size, req_to_token_stride] int32
swa_mapping_ptr, # [full_size + page_size + 1] int64, or None
out_cache_loc_ptr, # [num_out_tokens] int64, or None
qlens_ptr, # [bs] int, or None (Q_MODE_CUMSUM only)
# outputs
cache_seqlens_ptr, # [bs] int32
cu_seqlens_k_ptr, # [bs + 1] int32
cu_seqlens_q_ptr, # [bs + 1] int32, or None
page_table_ptr, # [bs, page_table_stride] int32
swa_page_table_ptr, # [bs, swa_page_table_stride] int32, or None
swa_out_cache_loc_ptr, # [swa_out_len] int64, or None
# scalars
bs,
seqlen_offset, # added to seq_lens for cache_seqlens / cu_seqlens_k
max_seq_pages, # page-table columns to (re)write per row
q_stride, # Q_MODE_STRIDED stride
num_out_tokens, # valid prefix of out_cache_loc
swa_out_len, # full swa_out_cache_loc length (zero-padded tail)
req_to_token_stride,
page_table_stride,
swa_page_table_stride,
# constexpr
PAGE_SIZE: tl.constexpr,
HAS_SWA: tl.constexpr,
HAS_SWA_OUT: tl.constexpr,
Q_MODE: tl.constexpr,
PAGE_BLOCK: tl.constexpr,
BS_BLOCK: tl.constexpr,
):
pid = tl.program_id(axis=0)
if pid < bs:
# One program per batch row: cache_seqlens + page table row(s).
req_pool_index = tl.load(req_pool_indices_ptr + pid).to(tl.int64)
seqlen = (tl.load(seq_lens_ptr + pid) + seqlen_offset).to(tl.int32)
tl.store(cache_seqlens_ptr + pid, seqlen)
row_in = req_to_token_ptr + req_pool_index * req_to_token_stride
row_out = page_table_ptr + pid.to(tl.int64) * page_table_stride
if HAS_SWA:
swa_row_out = swa_page_table_ptr + pid.to(tl.int64) * swa_page_table_stride
for i in range(tl.cdiv(max_seq_pages, PAGE_BLOCK)):
page_idx = i * PAGE_BLOCK + tl.arange(0, PAGE_BLOCK)
mask = page_idx < max_seq_pages
token = tl.load(
row_in + page_idx.to(tl.int64) * PAGE_SIZE, mask=mask, other=0
)
tl.store(row_out + page_idx, token // PAGE_SIZE, mask=mask)
if HAS_SWA:
token64 = token.to(tl.int64)
# Real req_to_token slots are >=0; the token>=0 guard + other=-1 mirror
# the swa_out_cache_loc -1 sentinel (uniform handling, no wrap).
swa_token = tl.load(
swa_mapping_ptr + token64, mask=mask & (token64 >= 0), other=-1
)
swa_page = tl.where(swa_token < 0, -1, swa_token // PAGE_SIZE)
tl.store(swa_row_out + page_idx, swa_page.to(tl.int32), mask=mask)
elif pid == bs:
# Single program: cu_seqlens_k (+ optional cu_seqlens_q) cumsum.
offs = tl.arange(0, BS_BLOCK)
mask = offs < bs
seqlens = (tl.load(seq_lens_ptr + offs, mask=mask, other=0)).to(tl.int32)
seqlens = tl.where(mask, seqlens + seqlen_offset, 0)
tl.store(cu_seqlens_k_ptr + 1 + offs, tl.cumsum(seqlens, axis=0), mask=mask)
if Q_MODE == 1: # Q_MODE_CUMSUM
qlens = tl.load(qlens_ptr + offs, mask=mask, other=0).to(tl.int32)
qlens = tl.where(mask, qlens, 0)
tl.store(cu_seqlens_q_ptr + 1 + offs, tl.cumsum(qlens, axis=0), mask=mask)
if Q_MODE == 2: # Q_MODE_STRIDED
tl.store(
cu_seqlens_q_ptr + 1 + offs,
((offs + 1) * q_stride).to(tl.int32),
mask=mask,
)
else:
# Remaining programs: swa_out_cache_loc translate + zero padding.
if HAS_SWA_OUT:
out_idx = (pid - bs - 1) * PAGE_BLOCK + tl.arange(0, PAGE_BLOCK)
in_range = out_idx < swa_out_len
is_real = in_range & (out_idx < num_out_tokens)
loc = tl.load(out_cache_loc_ptr + out_idx, mask=is_real, other=0)
translated = tl.load(
swa_mapping_ptr + loc, mask=is_real & (loc >= 0), other=0
)
translated = tl.where(is_real & (loc < 0), -1, translated)
tl.store(swa_out_cache_loc_ptr + out_idx, translated, mask=in_range)
def update_trtllm_mha_graph_metadata(
*,
req_pool_indices,
seq_lens,
req_to_token,
cache_seqlens,
cu_seqlens_k,
page_table,
bs: int,
seqlen_offset: int,
max_seq_pages: int,
page_size: int,
swa_mapping=None,
swa_page_table=None,
out_cache_loc=None,
swa_out_cache_loc=None,
cu_seqlens_q=None,
qlens=None,
q_stride: int = 0,
q_mode: int = Q_MODE_NONE,
):
"""Launch the fused metadata update (one kernel for the whole replay init)."""
if bs == 0:
return
# Launch-block width: page-table columns each program writes per iteration
# (also the swa_out_cache_loc tile width). 512 keeps the per-program working
# set small enough to stay off the register-pressure / occupancy cliff while
# being wide enough to cover the static page-table width in few iterations.
PAGE_BLOCK = 512
has_swa = swa_page_table is not None
has_swa_out = swa_out_cache_loc is not None
swa_out_len = swa_out_cache_loc.shape[0] if has_swa_out else 0
if has_swa_out and out_cache_loc is not None:
num_out_tokens = min(swa_out_len, out_cache_loc.shape[0])
else:
num_out_tokens = 0
if num_out_tokens == 0:
# All loads are masked out; pass a valid dummy pointer for codegen.
out_cache_loc = swa_out_cache_loc
grid_extra = triton.cdiv(swa_out_len, PAGE_BLOCK) if has_swa_out else 0
grid = (bs + 1 + grid_extra,)
update_trtllm_mha_graph_metadata_kernel[grid](
req_pool_indices,
seq_lens,
req_to_token,
swa_mapping,
out_cache_loc,
qlens,
cache_seqlens,
cu_seqlens_k,
cu_seqlens_q,
page_table,
swa_page_table,
swa_out_cache_loc,
bs,
seqlen_offset,
max_seq_pages,
q_stride,
num_out_tokens,
swa_out_len,
req_to_token.stride(0),
page_table.stride(0),
swa_page_table.stride(0) if has_swa else 0,
PAGE_SIZE=page_size,
HAS_SWA=has_swa,
HAS_SWA_OUT=has_swa_out,
Q_MODE=q_mode,
PAGE_BLOCK=PAGE_BLOCK,
BS_BLOCK=triton.next_power_of_2(bs),
)
@@ -0,0 +1,138 @@
"""Device-side page-table builder for the trtllm_mha attention backend.
trtllm_mha builds its block (page) table from the global ``req_to_token`` pool.
Doing it with a host-max PyTorch gather forces a ``seq_lens.max().item()`` D2H
sync (the CPU must know the page-table width before launching). This kernel
instead derives the per-request page count from the device-side ``seq_lens``
tensor, so the build is sync-free: the grid/buffer use the static
``max_num_pages`` upper bound, while each program self-guards on the real length.
The kernel is MHA-owned (no dependency on the MLA kv-index kernels) and also
emits the SWA-translated block table in the same pass via the full->SWA lookup
table, so SWA hybrid models stay sync-free too.
"""
from typing import Optional
import torch
import triton
import triton.language as tl
# Tokens covered per CTA along the page-block (grid axis-1) dimension.
# Must be a multiple of page_size (asserted in build_trtllm_mha_page_table).
_MHA_KV_INDEX_BLOCK_TOKENS = 4096
# Triton kernels can only read module globals that are tl.constexpr instances.
_MHA_KV_INDEX_BLOCK_TOKENS_TL = tl.constexpr(_MHA_KV_INDEX_BLOCK_TOKENS)
def get_num_mha_kv_index_blocks(num_pages: int, page_size: int) -> int:
"""Grid axis-1 size: number of page-block CTAs spanning the widest sequence.
``num_pages`` is the per-row width of the page-table buffer (the static
``max_num_pages`` upper bound). One CTA handles ``_MHA_KV_INDEX_BLOCK_TOKENS
// page_size`` pages.
"""
pages_per_block = _MHA_KV_INDEX_BLOCK_TOKENS // page_size
return (num_pages + pages_per_block - 1) // pages_per_block
@triton.jit
def create_trtllm_mha_kv_indices_triton(
req_to_token_ptr, # [max_reqs, max_context_len], int32
req_pool_indices_ptr, # [bs]
seq_lens_ptr, # [bs], per-request KV length in tokens
full_to_swa_ptr, # full->SWA token-slot lookup table, or dummy when not SWA
page_table_ptr, # [bs, num_pages] int32 block ids (output)
swa_page_table_ptr, # [bs, num_pages] int32 SWA block ids (output), or dummy
full_to_swa_numel,
req_to_token_stride: tl.constexpr,
page_table_stride: tl.constexpr,
PAGE_SIZE: tl.constexpr,
HAS_SWA: tl.constexpr,
):
"""Fill ``page_table_ptr`` (and ``swa_page_table_ptr`` when ``HAS_SWA``).
Program ``(pid_req, pid_blk)`` writes the block ids of request ``pid_req`` for
the page-block ``pid_blk``. It reads the KV token slot at each page boundary
from ``req_to_token`` and converts it to a block id (``slot // PAGE_SIZE``).
Programs past the request's page count are guarded out, so the work (and the
DRAM traffic) is bounded by the device-side ``seq_lens`` — no host max needed.
The SWA lookup assumes valid (``>= 0``) slots, unlike
``translate_loc_from_full_to_swa``'s ``-1`` sentinel handling; page-boundary
reads stay within ``seq_len``, so slots are always valid here.
"""
PAGES_PER_BLOCK: tl.constexpr = _MHA_KV_INDEX_BLOCK_TOKENS_TL // PAGE_SIZE
pid_req = tl.program_id(0)
pid_blk = tl.program_id(1)
seq_len = tl.load(seq_lens_ptr + pid_req)
num_pages = tl.cdiv(seq_len, PAGE_SIZE)
num_page_blocks = tl.cdiv(seq_len, _MHA_KV_INDEX_BLOCK_TOKENS_TL)
if pid_blk >= num_page_blocks:
return
req_pool_index = tl.load(req_pool_indices_ptr + pid_req)
page_idx = tl.arange(0, PAGES_PER_BLOCK) + pid_blk * PAGES_PER_BLOCK
token_pos = page_idx.to(tl.int64) * PAGE_SIZE
mask = page_idx < num_pages
slot = tl.load(
req_to_token_ptr
+ req_pool_index.to(tl.int64) * req_to_token_stride
+ token_pos,
mask=mask,
)
out_off = pid_req * page_table_stride + page_idx
tl.store(page_table_ptr + out_off, (slot // PAGE_SIZE).to(tl.int32), mask=mask)
if HAS_SWA:
swa_index = tl.minimum(tl.maximum(slot, 0), full_to_swa_numel - 1)
swa_slot = tl.load(full_to_swa_ptr + swa_index.to(tl.int64), mask=mask)
tl.store(
swa_page_table_ptr + out_off,
(swa_slot // PAGE_SIZE).to(tl.int32),
mask=mask,
)
def build_trtllm_mha_page_table(
req_to_token: torch.Tensor,
req_pool_indices: torch.Tensor,
cache_seqlens: torch.Tensor,
page_table: torch.Tensor,
page_size: int,
swa_page_table: Optional[torch.Tensor] = None,
full_to_swa: Optional[torch.Tensor] = None,
) -> None:
"""Fill ``page_table`` (and ``swa_page_table`` when SWA) on-device, no D2H sync.
Computes the launch grid from the static page-table width and dispatches
``create_trtllm_mha_kv_indices_triton``. ``page_table`` (and, for SWA models,
``swa_page_table``) are written in place; the caller owns the buffers so the
cuda-graph path can reuse its pre-allocated tensors. SWA is enabled iff
``full_to_swa`` is provided, which then also requires ``swa_page_table``.
"""
has_swa = full_to_swa is not None
assert has_swa == (
swa_page_table is not None
), "full_to_swa and swa_page_table must be provided together"
assert (
_MHA_KV_INDEX_BLOCK_TOKENS % page_size == 0
), f"page_size={page_size} must divide _MHA_KV_INDEX_BLOCK_TOKENS={_MHA_KV_INDEX_BLOCK_TOKENS}"
bs, num_pages = page_table.shape
full_to_swa_numel = full_to_swa.numel() if has_swa else 0
create_trtllm_mha_kv_indices_triton[
(bs, get_num_mha_kv_index_blocks(num_pages, page_size))
](
req_to_token,
req_pool_indices,
cache_seqlens,
full_to_swa,
page_table,
swa_page_table,
full_to_swa_numel,
req_to_token.stride(0),
page_table.stride(0),
PAGE_SIZE=page_size,
HAS_SWA=has_swa,
)