Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

1078 lines
34 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPDX-License-Identifier: MIT AND Apache-2.0
# SPDX-FileCopyrightText: Copyright (c) 2026 LightSeek Foundation
# SPDX-FileCopyrightText: Copyright 2023-2024 SGLang Team
#
# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Triton implementation of KVStore transfer kernels."""
from __future__ import annotations
import os
import torch
from tokenspeed_kernel._triton import tl, triton
from tokenspeed_kernel.platform import current_platform
_PER_LAYER_GRID_CAP = int(os.environ.get("TOKENSPEED_KV_GRID_CAP", "64"))
_ALL_LAYER_GRID_CAP = int(os.environ.get("TOKENSPEED_KV_ALL_LAYER_GRID_CAP", "32"))
_is_nvidia = current_platform().is_nvidia
__all__ = [
"fused_fp8_set_kv_buffer",
"gather_page_table_with_padding",
"store_kv_cache",
"transfer_kv_all_layer",
"transfer_kv_all_layer_mla",
"transfer_kv_per_layer",
"transfer_kv_per_layer_mla",
]
# -----------------------------------------------------------------------------
# Per-Layer KV Cache Scatter
# -----------------------------------------------------------------------------
@triton.jit
def _store_kv_cache_kernel(
k_src_ptr,
v_src_ptr,
k_dst_ptr,
v_dst_ptr,
loc_ptr,
k_src_token_stride,
v_src_token_stride,
k_dst_row_stride,
v_dst_row_stride,
n_kv_per_token: tl.constexpr,
BLOCK: tl.constexpr,
):
"""Scatter rows of k_src/v_src into k_dst/v_dst at indices loc_ptr.
Stride-aware: leading axis of src/dst can have any stride; the only
requirement is ``stride(-1) == 1`` so we can use linear addressing on
the flattened head_dim×num_kv_heads axis.
"""
is_v = tl.program_id(0)
row = tl.program_id(1)
dst_row = tl.load(loc_ptr + row).to(tl.int64)
offsets = tl.arange(0, BLOCK)
mask = offsets < n_kv_per_token
if is_v == 1:
src = tl.load(
v_src_ptr + row * v_src_token_stride + offsets, mask=mask, other=0
)
tl.store(v_dst_ptr + dst_row * v_dst_row_stride + offsets, src, mask=mask)
else:
src = tl.load(
k_src_ptr + row * k_src_token_stride + offsets, mask=mask, other=0
)
tl.store(k_dst_ptr + dst_row * k_dst_row_stride + offsets, src, mask=mask)
def store_kv_cache(
k_src: torch.Tensor,
v_src: torch.Tensor,
k_dst: torch.Tensor,
v_dst: torch.Tensor,
loc: torch.Tensor,
) -> None:
"""Fused per-token KV cache scatter for one layer.
Replaces ``k_dst[loc] = k_src; v_dst[loc] = v_src`` with a single triton
launch handling both k and v rows. The last dim of all four tensors must
be contiguous (stride == 1); the leading axis may have any stride — this
lets src tensors come from a qkv-split view directly (no contiguous copy
required).
"""
n_tokens = k_src.shape[0]
if n_tokens == 0:
return
n_kv_k = k_src.numel() // n_tokens
n_kv_v = v_src.numel() // n_tokens
assert (
n_kv_k == n_kv_v
), f"k/v must share per-token element count, got {n_kv_k} vs {n_kv_v}"
assert k_src.stride(-1) == 1 and v_src.stride(-1) == 1
assert k_dst.stride(-1) == 1 and v_dst.stride(-1) == 1
k_src_stride = k_src.stride(0) if k_src.dim() > 1 else k_src.shape[-1]
v_src_stride = v_src.stride(0) if v_src.dim() > 1 else v_src.shape[-1]
k_dst_stride = k_dst.stride(0) if k_dst.dim() > 1 else k_dst.shape[-1]
v_dst_stride = v_dst.stride(0) if v_dst.dim() > 1 else v_dst.shape[-1]
block = triton.next_power_of_2(n_kv_k)
_store_kv_cache_kernel[(2, n_tokens)](
k_src,
v_src,
k_dst,
v_dst,
loc,
k_src_stride,
v_src_stride,
k_dst_stride,
v_dst_stride,
n_kv_k,
BLOCK=block,
)
# -----------------------------------------------------------------------------
# FP8 KV Cache Write
# -----------------------------------------------------------------------------
@triton.jit
def _process_fp8_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,
):
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)
mask = head_mask[:, None] & dim_mask[None, :]
input_offsets = (
token_id * input_stride_token
+ head_offsets[:, None] * input_stride_head
+ dim_offsets[None, :] * input_stride_dim
)
block = tl.load(input_ptr + input_offsets, mask=mask, other=0.0)
if use_provided_scale:
block_fp8 = (block * inv_scale).to(tl.float8e4nv)
else:
block_fp8 = block.to(tl.float8e4nv)
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(
k_ptr,
v_ptr,
k_cache_ptr,
v_cache_ptr,
cache_loc_ptr,
inv_k_scale_ptr,
inv_v_scale_ptr,
use_provided_scale: tl.constexpr,
num_kv_heads: tl.constexpr,
head_dim: tl.constexpr,
page_size: tl.constexpr,
k_stride_token: tl.constexpr,
k_stride_head: tl.constexpr,
k_stride_dim: tl.constexpr,
k_cache_stride_page: tl.constexpr,
k_cache_stride_offset: tl.constexpr,
k_cache_stride_head: tl.constexpr,
k_cache_stride_dim: tl.constexpr,
v_stride_token: tl.constexpr,
v_stride_head: tl.constexpr,
v_stride_dim: tl.constexpr,
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_HEAD: tl.constexpr,
BLOCK_DIM: tl.constexpr,
):
token_id = tl.program_id(0)
head_block_id = tl.program_id(1)
kv_idx = tl.program_id(2)
cache_loc = tl.load(cache_loc_ptr + token_id).to(tl.int64)
page_id = cache_loc // page_size
page_offset = cache_loc % page_size
if kv_idx == 0:
if use_provided_scale:
inv_scale = tl.load(inv_k_scale_ptr)
else:
inv_scale = 1.0
_process_fp8_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:
if use_provided_scale:
inv_scale = tl.load(inv_v_scale_ptr)
else:
inv_scale = 1.0
_process_fp8_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,
v: torch.Tensor,
k_cache: torch.Tensor,
v_cache: torch.Tensor,
cache_loc: torch.Tensor,
k_scale: float | torch.Tensor | None = None,
v_scale: float | torch.Tensor | None = None,
page_size: int = 16,
) -> None:
"""Quantize K/V tensors to FP8 and scatter them into a paged KV cache.
Args:
k: Key tensor with shape ``[num_tokens, num_kv_heads, head_dim]`` or
``[num_tokens, num_kv_heads * head_dim]``.
v: Value tensor with the same shape convention as ``k``.
k_cache: Destination K cache, either flattened slots
``[total_slots, num_kv_heads, head_dim]`` or paged layout
``[num_pages, page_size, num_kv_heads, head_dim]``.
v_cache: Destination V cache with the same shape convention as
``k_cache``.
cache_loc: Cache slot index for each input token.
k_scale: Optional scalar K scale. When provided with ``v_scale``, K is
divided by this scale before FP8 conversion.
v_scale: Optional scalar V scale. When provided with ``k_scale``, V is
divided by this scale before FP8 conversion.
page_size: Number of tokens per cache page.
"""
num_tokens = k.shape[0]
if num_tokens == 0:
return
if k_cache.ndim == 3:
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})"
elif k_cache.ndim == 4:
_, ps, num_kv_heads, head_dim = k_cache.shape
assert (
ps == page_size
), f"page_size mismatch: cache has {ps}, expected {page_size}"
else:
raise ValueError(f"Unsupported k_cache.ndim={k_cache.ndim}, expected 3 or 4")
if k.ndim == 3:
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"
k_3d = k
v_3d = v
elif k.ndim == 2:
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}"
k_3d = k.view(num_tokens, num_kv_heads, head_dim)
v_3d = v.view(num_tokens, num_kv_heads, head_dim)
else:
raise ValueError(f"Unsupported k.ndim={k.ndim}, expected 2 or 3")
if k_cache.ndim == 3:
k_cache_stride_page = k_cache.stride(0) * page_size
k_cache_stride_offset = k_cache.stride(0)
k_cache_stride_head = k_cache.stride(1)
k_cache_stride_dim = k_cache.stride(2)
v_cache_stride_page = v_cache.stride(0) * page_size
v_cache_stride_offset = v_cache.stride(0)
v_cache_stride_head = v_cache.stride(1)
v_cache_stride_dim = v_cache.stride(2)
else:
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)
use_provided_scale = k_scale is not None and v_scale is not None
block_head = min(num_kv_heads, 8)
block_dim = min(head_dim, 128)
num_head_blocks = (num_kv_heads + block_head - 1) // block_head
grid = (num_tokens, num_head_blocks, 2)
device = k_3d.device
def _to_tensor_scale(scale):
if isinstance(scale, torch.Tensor):
return scale.to(device=device, dtype=torch.float32)
return torch.tensor(float(scale), device=device, dtype=torch.float32)
if use_provided_scale:
k_scale_tensor = _to_tensor_scale(k_scale)
v_scale_tensor = _to_tensor_scale(v_scale)
inv_k_scale_ptr = (1.0 / k_scale_tensor).to(device=device, dtype=torch.float32)
inv_v_scale_ptr = (1.0 / v_scale_tensor).to(device=device, dtype=torch.float32)
else:
inv_k_scale_ptr = k_3d
inv_v_scale_ptr = k_3d
_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_3d.stride(0),
k_3d.stride(1),
k_3d.stride(2),
k_cache_stride_page,
k_cache_stride_offset,
k_cache_stride_head,
k_cache_stride_dim,
v_3d.stride(0),
v_3d.stride(1),
v_3d.stride(2),
v_cache_stride_page,
v_cache_stride_offset,
v_cache_stride_head,
v_cache_stride_dim,
BLOCK_HEAD=block_head,
BLOCK_DIM=block_dim,
)
# -----------------------------------------------------------------------------
# Page Table Gather
# -----------------------------------------------------------------------------
@triton.jit
def _gather_page_table_with_padding_kernel(
req_to_page_ptr,
req_pool_indices_ptr,
seq_lens_ptr,
out_ptr,
src_stride0,
out_stride0,
max_num_pages: tl.constexpr,
page_size: tl.constexpr,
dummy_slot: tl.constexpr,
BLOCK_COLS: tl.constexpr,
):
pid_row = tl.program_id(0)
pid_col = tl.program_id(1)
sl = tl.load(seq_lens_ptr + pid_row).to(tl.int32)
n_pages = (sl + page_size - 1) // page_size
col_offsets = pid_col * BLOCK_COLS + tl.arange(0, BLOCK_COLS)
in_bounds = col_offsets < max_num_pages
valid = col_offsets < n_pages
req_idx = tl.load(req_pool_indices_ptr + pid_row).to(tl.int64)
src_addr = req_to_page_ptr + req_idx * src_stride0 + col_offsets
gathered = tl.load(src_addr, mask=valid & in_bounds, other=dummy_slot)
out_addr = out_ptr + pid_row * out_stride0 + col_offsets
tl.store(out_addr, gathered, mask=in_bounds)
def gather_page_table_with_padding(
req_to_page: torch.Tensor,
req_pool_indices: torch.Tensor,
seq_lens: torch.Tensor,
out: torch.Tensor,
*,
bs: int,
max_num_pages: int,
page_size: int,
dummy_slot: int = 0,
) -> None:
"""Gather active request page tables and clear padding columns.
Args:
req_to_page: Source page table with request rows.
req_pool_indices: Request row indices to gather, shape ``[bs]``.
seq_lens: Per-request KV lengths, shape ``[bs]``.
out: Destination page table, shape ``[max_bs, max_num_pages]``.
bs: Number of active rows to gather.
max_num_pages: Number of destination page-table columns.
page_size: Number of tokens per page.
dummy_slot: Value written into padding columns.
"""
block_cols = 128
grid = (bs, triton.cdiv(max_num_pages, block_cols))
_gather_page_table_with_padding_kernel[grid](
req_to_page,
req_pool_indices,
seq_lens,
out,
req_to_page.stride(0),
out.stride(0),
max_num_pages,
page_size,
dummy_slot,
BLOCK_COLS=block_cols,
num_warps=4,
)
# -----------------------------------------------------------------------------
# KV Cache Transfer
# -----------------------------------------------------------------------------
@triton.jit
def _kv_transfer_per_layer_capped_kernel(
k_cache_dst_ptr,
v_cache_dst_ptr,
indices_dst_ptr,
k_cache_src_ptr,
v_cache_src_ptr,
indices_src_ptr,
kv_cache_src_stride,
kv_cache_dst_stride,
length,
BLOCK_SIZE: tl.constexpr,
):
"""Grid-capped variant: each program strides over multiple indices."""
pid = tl.program_id(0)
nprog = tl.num_programs(0)
offs = tl.arange(0, BLOCK_SIZE)
for i in range(pid, length, nprog):
pos_src = tl.load(indices_src_ptr + i).to(tl.int64)
pos_dst = tl.load(indices_dst_ptr + i).to(tl.int64)
src_offset = pos_src * kv_cache_src_stride
dst_offset = pos_dst * kv_cache_dst_stride
k_src = tl.load(k_cache_src_ptr + src_offset + offs)
tl.store(k_cache_dst_ptr + dst_offset + offs, k_src)
v_src = tl.load(v_cache_src_ptr + src_offset + offs)
tl.store(v_cache_dst_ptr + dst_offset + offs, v_src)
@triton.jit
def _kv_transfer_per_layer_kernel(
k_cache_dst_ptr,
v_cache_dst_ptr,
indices_dst_ptr,
k_cache_src_ptr,
v_cache_src_ptr,
indices_src_ptr,
kv_cache_src_stride,
kv_cache_dst_stride,
BLOCK_SIZE: tl.constexpr,
):
"""
Transfer KV cache entries for one layer based on src/dst indices.
Each program handles one index pair (src_idx -> dst_idx) and copies
BLOCK_SIZE elements at a time.
"""
pid = tl.program_id(0)
# Load src and dst positions
pos_src = tl.load(indices_src_ptr + pid).to(tl.int64)
pos_dst = tl.load(indices_dst_ptr + pid).to(tl.int64)
# Calculate base offsets in elements (not bytes, since we use element-based pointers)
src_offset = pos_src * kv_cache_src_stride
dst_offset = pos_dst * kv_cache_dst_stride
# Copy K cache
offs = tl.arange(0, BLOCK_SIZE)
k_src = tl.load(k_cache_src_ptr + src_offset + offs)
tl.store(k_cache_dst_ptr + dst_offset + offs, k_src)
# Copy V cache
v_src = tl.load(v_cache_src_ptr + src_offset + offs)
tl.store(v_cache_dst_ptr + dst_offset + offs, v_src)
@triton.jit
def _kv_transfer_all_layer_kernel(
k_ptr_dst_ptr,
v_ptr_dst_ptr,
indices_dst_ptr,
k_ptr_src_ptr,
v_ptr_src_ptr,
indices_src_ptr,
length,
num_layers: tl.constexpr,
kv_cache_src_stride_words,
kv_cache_dst_stride_words,
total_words,
WORDS_PER_CHUNK: tl.constexpr,
NUM_CHUNKS: tl.constexpr,
):
"""
Transfer KV cache entries for all layers based on src/dst indices.
Mirror the JIT kernel's execution model: each program iterates over index
pairs and copies all layers for that pair in 128-byte chunks.
"""
pid = tl.program_id(0)
num_programs = tl.num_programs(0)
word_offsets = tl.arange(0, WORDS_PER_CHUNK)
for idx in range(pid, length, num_programs):
pos_src = tl.load(indices_src_ptr + idx).to(tl.int64)
pos_dst = tl.load(indices_dst_ptr + idx).to(tl.int64)
src_slot_offset = pos_src * kv_cache_src_stride_words
dst_slot_offset = pos_dst * kv_cache_dst_stride_words
for layer in range(num_layers):
k_cache_src_ptr = tl.load(k_ptr_src_ptr + layer).to(
tl.pointer_type(tl.uint32)
)
v_cache_src_ptr = tl.load(v_ptr_src_ptr + layer).to(
tl.pointer_type(tl.uint32)
)
k_cache_dst_ptr = tl.load(k_ptr_dst_ptr + layer).to(
tl.pointer_type(tl.uint32)
)
v_cache_dst_ptr = tl.load(v_ptr_dst_ptr + layer).to(
tl.pointer_type(tl.uint32)
)
for chunk in range(NUM_CHUNKS):
chunk_offsets = chunk * WORDS_PER_CHUNK + word_offsets
mask = chunk_offsets < total_words
src_offsets = src_slot_offset + chunk_offsets
dst_offsets = dst_slot_offset + chunk_offsets
src_offsets = tl.max_contiguous(
tl.multiple_of(src_offsets, 4), WORDS_PER_CHUNK
)
dst_offsets = tl.max_contiguous(
tl.multiple_of(dst_offsets, 4), WORDS_PER_CHUNK
)
k_src = tl.load(
k_cache_src_ptr + src_offsets,
mask=mask,
other=0,
cache_modifier=".cg",
)
v_src = tl.load(
v_cache_src_ptr + src_offsets,
mask=mask,
other=0,
cache_modifier=".cg",
)
tl.store(
k_cache_dst_ptr + dst_offsets,
k_src,
mask=mask,
cache_modifier=".cs",
)
tl.store(
v_cache_dst_ptr + dst_offsets,
v_src,
mask=mask,
cache_modifier=".cs",
)
@triton.jit
def _load_cs_u32(ptrs):
return tl.inline_asm_elementwise(
"ld.global.cs.b32 $0, [$1];",
"=r,l",
[ptrs],
dtype=tl.uint32,
is_pure=True,
pack=1,
)
@triton.jit
def _store_cs_u32(values, ptrs):
return tl.inline_asm_elementwise(
"st.global.cs.b32 [$2], $1; mov.b32 $0, $1;",
"=r,r,l",
[values, ptrs],
dtype=tl.uint32,
is_pure=False,
pack=1,
)
@triton.jit
def _kv_transfer_all_layer_cs32_kernel(
k_ptr_dst_ptr,
v_ptr_dst_ptr,
indices_dst_ptr,
k_ptr_src_ptr,
v_ptr_src_ptr,
indices_src_ptr,
length,
num_layers: tl.constexpr,
kv_cache_src_stride_words,
kv_cache_dst_stride_words,
NUM_CHUNKS: tl.constexpr,
):
pid = tl.program_id(0)
num_programs = tl.num_programs(0)
lane_offsets = tl.arange(0, 32)
for idx in range(pid, length, num_programs):
pos_src = tl.load(indices_src_ptr + idx).to(tl.int64)
pos_dst = tl.load(indices_dst_ptr + idx).to(tl.int64)
src_slot_offset = pos_src * kv_cache_src_stride_words
dst_slot_offset = pos_dst * kv_cache_dst_stride_words
for layer in range(num_layers):
k_cache_src_ptr = tl.load(k_ptr_src_ptr + layer).to(
tl.pointer_type(tl.uint32)
)
v_cache_src_ptr = tl.load(v_ptr_src_ptr + layer).to(
tl.pointer_type(tl.uint32)
)
k_cache_dst_ptr = tl.load(k_ptr_dst_ptr + layer).to(
tl.pointer_type(tl.uint32)
)
v_cache_dst_ptr = tl.load(v_ptr_dst_ptr + layer).to(
tl.pointer_type(tl.uint32)
)
for chunk in range(NUM_CHUNKS):
chunk_offsets = chunk * 32 + lane_offsets
src_offsets = src_slot_offset + chunk_offsets
dst_offsets = dst_slot_offset + chunk_offsets
k_src = _load_cs_u32(k_cache_src_ptr + src_offsets)
v_src = _load_cs_u32(v_cache_src_ptr + src_offsets)
_store_cs_u32(k_src, k_cache_dst_ptr + dst_offsets)
_store_cs_u32(v_src, v_cache_dst_ptr + dst_offsets)
def _next_power_of_two(x: int) -> int:
"""Return the smallest power of two >= x."""
if x <= 0:
return 1
return 1 << (x - 1).bit_length()
def _recommended_program_count(
*,
length: int,
element_size: int,
num_layers: int,
device: torch.device,
) -> int:
# Each program copies one indexed token across all layers, so the amount of
# work scales with both slot size and layer count.
bytes_per_index = element_size * num_layers * 2
if bytes_per_index <= 16 * 1024:
programs_per_sm = 8
elif bytes_per_index <= 64 * 1024:
programs_per_sm = 4
else:
programs_per_sm = 2
sm_count = torch.cuda.get_device_properties(device).multi_processor_count
return max(1, min(length, sm_count * programs_per_sm))
def transfer_kv_per_layer(
src_k: torch.Tensor,
dst_k: torch.Tensor,
src_v: torch.Tensor,
dst_v: torch.Tensor,
src_indices: torch.Tensor,
dst_indices: torch.Tensor,
item_size: int,
) -> None:
"""
Transfer KV cache entries for one layer based on src/dst indices.
Args:
src_k: Source K cache tensor [num_slots, num_heads, head_dim]
dst_k: Destination K cache tensor [num_slots, num_heads, head_dim]
src_v: Source V cache tensor [num_slots, num_heads, head_dim]
dst_v: Destination V cache tensor [num_slots, num_heads, head_dim]
src_indices: Source indices tensor [length]
dst_indices: Destination indices tensor [length]
item_size: Number of bytes per cache slot
"""
if item_size % src_k.element_size() != 0:
raise ValueError("item_size must be divisible by the KV cache element size.")
element_dim = item_size // src_k.element_size()
length = src_indices.numel()
if length == 0:
return
# Flatten to 2D view: [num_slots, element_dim]
k_cache_src_flat = src_k.view(-1, element_dim)
v_cache_src_flat = src_v.view(-1, element_dim)
k_cache_dst_flat = dst_k.view(-1, element_dim)
v_cache_dst_flat = dst_v.view(-1, element_dim)
# Strides in elements
kv_cache_src_stride = k_cache_src_flat.stride(0)
kv_cache_dst_stride = k_cache_dst_flat.stride(0)
# BLOCK_SIZE is in elements, must be power of two and cover element_dim
block_size = _next_power_of_two(element_dim)
cap = _PER_LAYER_GRID_CAP
if cap > 0 and length > cap:
_kv_transfer_per_layer_capped_kernel[(cap,)](
k_cache_dst_flat,
v_cache_dst_flat,
dst_indices,
k_cache_src_flat,
v_cache_src_flat,
src_indices,
kv_cache_src_stride,
kv_cache_dst_stride,
length,
BLOCK_SIZE=block_size,
)
return
grid = (length,)
_kv_transfer_per_layer_kernel[grid](
k_cache_dst_flat,
v_cache_dst_flat,
dst_indices,
k_cache_src_flat,
v_cache_src_flat,
src_indices,
kv_cache_src_stride,
kv_cache_dst_stride,
BLOCK_SIZE=block_size,
)
@triton.jit
def _kv_transfer_per_layer_mla_kernel(
cache_dst_ptr,
indices_dst_ptr,
cache_src_ptr,
indices_src_ptr,
cache_src_stride,
cache_dst_stride,
ELEMENT_DIM: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
):
pid = tl.program_id(0)
pos_src = tl.load(indices_src_ptr + pid).to(tl.int64)
pos_dst = tl.load(indices_dst_ptr + pid).to(tl.int64)
offs = tl.arange(0, BLOCK_SIZE)
mask = offs < ELEMENT_DIM
src = tl.load(cache_src_ptr + pos_src * cache_src_stride + offs, mask=mask)
tl.store(cache_dst_ptr + pos_dst * cache_dst_stride + offs, src, mask=mask)
@triton.jit
def _kv_transfer_all_layer_mla_kernel(
ptr_dst_ptr,
indices_dst_ptr,
ptr_src_ptr,
indices_src_ptr,
length,
num_layers: tl.constexpr,
cache_src_stride_words,
cache_dst_stride_words,
total_words,
WORDS_PER_CHUNK: tl.constexpr,
NUM_CHUNKS: tl.constexpr,
):
pid = tl.program_id(0)
num_programs = tl.num_programs(0)
word_offsets = tl.arange(0, WORDS_PER_CHUNK)
for idx in range(pid, length, num_programs):
pos_src = tl.load(indices_src_ptr + idx).to(tl.int64)
pos_dst = tl.load(indices_dst_ptr + idx).to(tl.int64)
src_slot_offset = pos_src * cache_src_stride_words
dst_slot_offset = pos_dst * cache_dst_stride_words
for layer in range(num_layers):
cache_src_ptr = tl.load(ptr_src_ptr + layer).to(tl.pointer_type(tl.uint32))
cache_dst_ptr = tl.load(ptr_dst_ptr + layer).to(tl.pointer_type(tl.uint32))
for chunk in range(NUM_CHUNKS):
chunk_offsets = chunk * WORDS_PER_CHUNK + word_offsets
mask = chunk_offsets < total_words
src_offsets = src_slot_offset + chunk_offsets
dst_offsets = dst_slot_offset + chunk_offsets
src_offsets = tl.max_contiguous(
tl.multiple_of(src_offsets, 4), WORDS_PER_CHUNK
)
dst_offsets = tl.max_contiguous(
tl.multiple_of(dst_offsets, 4), WORDS_PER_CHUNK
)
src = tl.load(
cache_src_ptr + src_offsets,
mask=mask,
other=0,
cache_modifier=".cg",
)
tl.store(
cache_dst_ptr + dst_offsets,
src,
mask=mask,
cache_modifier=".cs",
)
def transfer_kv_per_layer_mla(
src: torch.Tensor,
dst: torch.Tensor,
src_indices: torch.Tensor,
dst_indices: torch.Tensor,
item_size: int,
block_quota: int | None = None,
) -> None:
del block_quota
if item_size % src.element_size() != 0:
raise ValueError("item_size must be divisible by the MLA cache element size.")
element_dim = item_size // src.element_size()
length = src_indices.numel()
if length == 0:
return
cache_src_flat = src.view(-1, element_dim)
cache_dst_flat = dst.view(-1, element_dim)
block_size = _next_power_of_two(element_dim)
_kv_transfer_per_layer_mla_kernel[(length,)](
cache_dst_flat,
dst_indices,
cache_src_flat,
src_indices,
cache_src_flat.stride(0),
cache_dst_flat.stride(0),
ELEMENT_DIM=element_dim,
BLOCK_SIZE=block_size,
)
def transfer_kv_all_layer_mla(
src_layers: torch.Tensor,
dst_layers: torch.Tensor,
src_indices: torch.Tensor,
dst_indices: torch.Tensor,
item_size: int,
num_layers: int,
block_quota: int | None = None,
) -> None:
del block_quota
length = src_indices.numel()
if length == 0:
return
if item_size % 4 != 0:
raise ValueError(
"Triton MLA all-layer kernel requires item_size to be a multiple of "
"4 bytes."
)
words_per_chunk = 32
total_words = item_size // 4
num_chunks = triton.cdiv(total_words, words_per_chunk)
grid = (
_recommended_program_count(
length=length,
element_size=item_size,
num_layers=num_layers,
device=src_indices.device,
),
)
_kv_transfer_all_layer_mla_kernel[grid](
dst_layers,
dst_indices,
src_layers,
src_indices,
length,
num_layers=num_layers,
cache_src_stride_words=item_size // 4,
cache_dst_stride_words=item_size // 4,
total_words=total_words,
WORDS_PER_CHUNK=words_per_chunk,
NUM_CHUNKS=num_chunks,
num_warps=1,
num_stages=1,
)
def transfer_kv_all_layer(
src_k_layers: torch.Tensor,
dst_k_layers: torch.Tensor,
src_v_layers: torch.Tensor,
dst_v_layers: torch.Tensor,
src_indices: torch.Tensor,
dst_indices: torch.Tensor,
item_size: int,
num_layers: int,
) -> None:
"""
Transfer KV cache entries for all layers based on src/dst indices.
Args:
src_k_layers: Tensor of source K cache pointers per layer [num_layers]
dst_k_layers: Tensor of destination K cache pointers per layer [num_layers]
src_v_layers: Tensor of source V cache pointers per layer [num_layers]
dst_v_layers: Tensor of destination V cache pointers per layer [num_layers]
src_indices: Source indices tensor [length]
dst_indices: Destination indices tensor [length]
item_size: Number of bytes per cache slot
num_layers: Number of layers to copy
"""
length = src_indices.numel()
if length == 0:
return
if item_size % 4 != 0:
raise ValueError(
"Triton KV cache all-layer kernel requires item_size to be a multiple of 4 bytes."
)
words_per_chunk = 32
total_words = item_size // 4
num_chunks = triton.cdiv(total_words, words_per_chunk)
num_programs = _recommended_program_count(
length=length,
element_size=item_size,
num_layers=num_layers,
device=src_indices.device,
)
if _ALL_LAYER_GRID_CAP > 0:
num_programs = min(num_programs, _ALL_LAYER_GRID_CAP)
grid = (num_programs,)
if _is_nvidia and total_words % words_per_chunk == 0:
_kv_transfer_all_layer_cs32_kernel[grid](
dst_k_layers,
dst_v_layers,
dst_indices,
src_k_layers,
src_v_layers,
src_indices,
length,
num_layers=num_layers,
kv_cache_src_stride_words=item_size // 4,
kv_cache_dst_stride_words=item_size // 4,
NUM_CHUNKS=num_chunks,
num_warps=1,
num_stages=1,
)
return
_kv_transfer_all_layer_kernel[grid](
dst_k_layers,
dst_v_layers,
dst_indices,
src_k_layers,
src_v_layers,
src_indices,
length,
num_layers=num_layers,
kv_cache_src_stride_words=item_size // 4,
kv_cache_dst_stride_words=item_size // 4,
total_words=total_words,
WORDS_PER_CHUNK=words_per_chunk,
NUM_CHUNKS=num_chunks,
num_warps=1,
num_stages=1,
)