Files
lightseekorg--tokenspeed/python/tokenspeed/runtime/execution/cache_loc_kernel.py
T
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

446 lines
16 KiB
Python

# 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 kernels for computing cache locations and updating page tables.
"""
import torch
import triton
import triton.language as tl
from tokenspeed.runtime.utils import get_colorful_logger
logger = get_colorful_logger(__name__)
@triton.jit
def update_req_to_page_kernel(
# Input pointers
req_pool_indices_ptr, # [batch_size]
new_occupied_pages_ptr, # [total_pages] - flattened
new_occupied_pages_num_ptr, # [batch_size]
pages_copy_starts_ptr, # [batch_size]
cumsum_pages_ptr, # [batch_size] - cumulative sum of new_occupied_pages_num
# Output pointer
req_to_page_ptr, # [req_pool_size+1, context_len]
# Scalars
context_len: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
):
"""
Update req_to_page table with new occupied pages.
Each program handles one request in the batch.
"""
req_idx = tl.program_id(0)
# Load request metadata
req_pool_idx = tl.load(req_pool_indices_ptr + req_idx)
num_pages = tl.load(new_occupied_pages_num_ptr + req_idx)
copy_start = tl.load(pages_copy_starts_ptr + req_idx)
# Get offset into flattened new_occupied_pages
offset_idx = tl.where(req_idx > 0, req_idx - 1, 0)
pages_offset = tl.load(cumsum_pages_ptr + offset_idx)
pages_offset = tl.where(req_idx > 0, pages_offset, 0)
# Process pages in blocks
num_blocks = tl.cdiv(num_pages, BLOCK_SIZE)
for block_idx in range(num_blocks):
block_start = block_idx * BLOCK_SIZE
# Compute page indices within this block
page_offsets = block_start + tl.arange(0, BLOCK_SIZE)
mask = page_offsets < num_pages
# Load new page IDs
page_ptrs = new_occupied_pages_ptr + pages_offset + page_offsets
new_page_ids = tl.load(page_ptrs, mask=mask, other=0)
# Compute target positions in req_to_page
target_positions = copy_start + page_offsets
# Store to req_to_page[req_pool_idx, target_positions]
output_ptrs = req_to_page_ptr + req_pool_idx * context_len + target_positions
tl.store(output_ptrs, new_page_ids, mask=mask)
def update_req_to_page(
req_to_page: torch.Tensor,
req_pool_indices: torch.Tensor,
new_occupied_pages: torch.Tensor,
new_occupied_pages_num: torch.Tensor,
pages_copy_starts: torch.Tensor,
) -> None:
"""
Update req_to_page table with new occupied pages using Triton kernel.
Args:
req_to_page: Request to page table [req_pool_size+1, context_len]
req_pool_indices: Request pool indices [batch_size]
new_occupied_pages: New page IDs [total_pages] - flattened
new_occupied_pages_num: Number of new pages per request [batch_size]
pages_copy_starts: Start position in req_to_page for each request [batch_size]
"""
batch_size = req_pool_indices.shape[0]
context_len = req_to_page.shape[1]
if new_occupied_pages.shape[0] == 0:
return
# Compute cumulative sum for offset calculation.
cumsum_pages = torch.cumsum(new_occupied_pages_num, dim=0)
# Launch kernel - one program per request
BLOCK_SIZE = 128
grid = (batch_size,)
update_req_to_page_kernel[grid](
req_pool_indices,
new_occupied_pages,
new_occupied_pages_num,
pages_copy_starts,
cumsum_pages,
req_to_page,
context_len=context_len,
BLOCK_SIZE=BLOCK_SIZE,
)
@triton.jit
def compute_out_cache_loc_kernel(
# Input pointers
req_pool_indices_ptr, # [batch_size]
input_lengths_ptr, # [batch_size] or None for uniform mode
cache_start_ptr, # [batch_size]
req_to_pages_ptr, # [req_pool_size+1, max_pages]
cumsum_lengths_ptr, # [batch_size] or None for uniform mode
# Output pointer
out_cache_loc_ptr, # [total_tokens]
# Scalars
uniform_input_length, # used when input_lengths_ptr is None
page_size: tl.constexpr,
max_pages: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
):
"""
Unified kernel to compute out_cache_loc for both prefill and decode.
For each token in each request, compute:
position = cache_start[req_idx] + token_offset_in_seq
page_idx = position // page_size
offset_in_page = position % page_size
page_id = req_to_pages[req_pool_idx, page_idx]
out_cache_loc = page_id * page_size + offset_in_page
For decode, input_lengths are all 1.
For prefill, input_lengths vary.
When all requests share the same input_length (the multi-step drafter
case), callers pass ``input_lengths_ptr=None`` (and ``cumsum_lengths_ptr=None``)
together with ``uniform_input_length`` set to the shared length. Triton
specializes the kernel on the None-ness of the pointers at JIT time and
dead-code-eliminates the corresponding GMEM reads.
"""
# Program ID represents which request we're processing
req_idx = tl.program_id(0)
# Load request metadata.
req_pool_idx = tl.load(req_pool_indices_ptr + req_idx)
valid_cache_len = tl.load(cache_start_ptr + req_idx)
if input_lengths_ptr is not None:
input_length = tl.load(input_lengths_ptr + req_idx)
# Always load from cumsum, use 0 index for first request to ensure type consistency
offset_idx = tl.where(req_idx > 0, req_idx - 1, 0)
output_offset = tl.load(cumsum_lengths_ptr + offset_idx)
# Zero out offset for first request
output_offset = tl.where(req_idx > 0, output_offset, 0)
else:
input_length = uniform_input_length
output_offset = req_idx * uniform_input_length
# Process tokens in blocks
num_blocks = tl.cdiv(input_length, BLOCK_SIZE)
for block_idx in range(num_blocks):
block_start = block_idx * BLOCK_SIZE
# Compute token offsets within this block
token_offsets = block_start + tl.arange(0, BLOCK_SIZE)
mask = token_offsets < input_length
# Compute logical positions
positions = valid_cache_len + token_offsets
# Compute page indices and offsets
page_indices = positions // page_size
overflow = page_indices >= max_pages
# Clamp to last valid page to avoid OOB GMEM read.
page_indices = tl.minimum(page_indices, max_pages - 1)
offsets_in_page = positions % page_size
# Load page IDs from req_to_pages
# req_to_pages is [req_pool_size+1, max_pages]
page_ptrs = req_to_pages_ptr + req_pool_idx * max_pages + page_indices
page_ids = tl.load(page_ptrs, mask=mask, other=0)
# Compute physical cache locations
cache_locs = page_ids * page_size + offsets_in_page
# For overflow tokens, route to slot 0 (a fixed safe dummy target that
# never aliases a real request's KV data). This avoids using a dynamic
# req_to_pages[0][0] load whose value can change at runtime and corrupt
# other requests' KV cache or trigger IndexKernel out-of-bounds.
cache_locs = tl.where(overflow, 0, cache_locs)
# Store to output
output_ptrs = out_cache_loc_ptr + output_offset + token_offsets
tl.store(output_ptrs, cache_locs, mask=mask)
def compute_out_cache_loc(
out_cache_loc_ptr,
req_pool_indices: torch.Tensor, # [batch_size]
input_lengths: torch.Tensor, # [batch_size]
cache_start: torch.Tensor, # [batch_size]
req_to_pages: torch.Tensor, # [req_pool_size+1, max_pages]
page_size: int,
) -> None:
batch_size = req_pool_indices.shape[0]
max_pages = req_to_pages.shape[1]
cumsum_lengths = torch.cumsum(input_lengths, dim=0)
BLOCK_SIZE = 128
grid = (batch_size,)
compute_out_cache_loc_kernel[grid](
req_pool_indices,
input_lengths,
cache_start,
req_to_pages,
cumsum_lengths,
out_cache_loc_ptr,
0, # uniform_input_length unused when input_lengths_ptr is not None
page_size=page_size,
max_pages=max_pages,
BLOCK_SIZE=BLOCK_SIZE,
)
@triton.jit
def fused_decode_input_prep_kernel(
# Inputs
req_pool_indices_ptr, # [batch_size]
valid_cache_lengths_ptr, # [req_pool_size+1]
req_to_pages_ptr, # [req_pool_size+1, max_pages]
# Outputs
out_cache_loc_ptr, # [batch_size * uniform_input_length]
positions_ptr, # [batch_size * uniform_input_length]
seq_lens_out_ptr, # [batch_size]
# Scalars
uniform_input_length,
page_size: tl.constexpr,
max_pages: tl.constexpr,
BLOCK_SIZE: tl.constexpr,
):
"""One launch fuses the decode-uniform path's four small kernels.
Replaces:
valid_cache_lengths.index_select(0, req_pool_indices)
compute_out_cache_loc_uniform
compute_position_triton (decode branch)
torch.add(input_lengths, valid_cache_lengths, out=seq_lens)
Each program handles one request. We do one GMEM read of
`valid_cache_lengths[pool_idx]` and reuse it for the seq_lens write,
the position writes, and the out_cache_loc page-table lookup.
"""
req_idx = tl.program_id(0)
pool_idx = tl.load(req_pool_indices_ptr + req_idx)
cache_start = tl.load(valid_cache_lengths_ptr + pool_idx)
# seq_lens[req_idx] = cache_start + uniform_input_length
tl.store(seq_lens_out_ptr + req_idx, cache_start + uniform_input_length)
output_offset = req_idx * uniform_input_length
num_blocks = tl.cdiv(uniform_input_length, BLOCK_SIZE)
for block_idx in range(num_blocks):
block_start = block_idx * BLOCK_SIZE
token_offsets = block_start + tl.arange(0, BLOCK_SIZE)
mask = token_offsets < uniform_input_length
positions_local = cache_start + token_offsets
page_indices = positions_local // page_size
overflow = page_indices >= max_pages
# Clamp to last valid page to avoid OOB GMEM read.
page_indices = tl.minimum(page_indices, max_pages - 1)
offsets_in_page = positions_local % page_size
page_ptrs = req_to_pages_ptr + pool_idx * max_pages + page_indices
page_ids = tl.load(page_ptrs, mask=mask, other=0)
cache_locs = page_ids * page_size + offsets_in_page
# Route overflow tokens to slot 0 (fixed safe dummy target).
cache_locs = tl.where(overflow, 0, cache_locs)
tl.store(
out_cache_loc_ptr + output_offset + token_offsets,
cache_locs,
mask=mask,
)
tl.store(
positions_ptr + output_offset + token_offsets,
positions_local,
mask=mask,
)
def fused_decode_input_prep(
out_cache_loc_ptr,
positions_ptr,
seq_lens_out_ptr,
req_pool_indices: torch.Tensor, # [batch_size]
valid_cache_lengths: torch.Tensor, # [req_pool_size+1]
uniform_input_length: int,
req_to_pages: torch.Tensor, # [req_pool_size+1, max_pages]
page_size: int,
) -> None:
"""Decode-only fast path: one Triton launch writes out_cache_loc,
positions, and seq_lens, reading `valid_cache_lengths[pool_idx]`
directly so the per-iter indexSelect + add are gone too.
"""
batch_size = req_pool_indices.shape[0]
max_pages = req_to_pages.shape[1]
BLOCK_SIZE = 128
grid = (batch_size,)
fused_decode_input_prep_kernel[grid](
req_pool_indices,
valid_cache_lengths,
req_to_pages,
out_cache_loc_ptr,
positions_ptr,
seq_lens_out_ptr,
uniform_input_length,
page_size=page_size,
max_pages=max_pages,
BLOCK_SIZE=BLOCK_SIZE,
)
def compute_out_cache_loc_uniform(
out_cache_loc_ptr,
req_pool_indices: torch.Tensor, # [batch_size]
uniform_input_length: int,
cache_start: torch.Tensor, # [batch_size]
req_to_pages: torch.Tensor, # [req_pool_size+1, max_pages]
page_size: int,
) -> None:
"""Specialized entry point when every request has the same ``input_length``.
Skips the per-call ``torch.full`` + ``cumsum`` host-side work and the
corresponding GMEM reads inside the kernel. Used by the multi-step drafter
where each request decodes exactly ``spec_num_steps - 1`` tokens.
"""
batch_size = req_pool_indices.shape[0]
max_pages = req_to_pages.shape[1]
BLOCK_SIZE = 128
grid = (batch_size,)
compute_out_cache_loc_kernel[grid](
req_pool_indices,
None, # input_lengths_ptr is None → kernel uses uniform_input_length
cache_start,
req_to_pages,
None, # cumsum_lengths_ptr is None → kernel computes offset analytically
out_cache_loc_ptr,
uniform_input_length,
page_size=page_size,
max_pages=max_pages,
BLOCK_SIZE=BLOCK_SIZE,
)
def update_block_table(forward_op, device, req_to_page):
def flatten_and_to_device(data, dtype=torch.int32):
if not data:
return torch.tensor([], dtype=dtype, device=device)
# Flatten one level if data is a list of lists
if isinstance(data[0], (list, tuple)):
flat = [x for inner in data for x in inner]
else:
flat = data
if not flat:
return torch.tensor([], dtype=dtype, device=device)
tensor = torch.tensor(flat, dtype=dtype, device="cpu", pin_memory=True)
return tensor.to(device, non_blocking=True)
# sizes[i] is the number of newly allocated pages for request i.
if all(n == 0 for n in forward_op.sizes):
return
max_pages = req_to_page.shape[1]
# Clamp a request that would overflow req_to_page instead of crashing the
# engine. Happens when MTP accept-rate collapse keeps a request alive past
# context_len; its KV drops but it will be finished shortly.
sizes = list(forward_op.sizes)
begins = list(forward_op.begins)
# new_occupied_pages is a list-of-lists [batch, size_i] of page ids;
# take a shallow copy so we can trim the offending request's row.
new_occupied_pages = [list(row) for row in forward_op.new_occupied_pages]
request_ids = list(forward_op.request_ids)
for i, (begin, size) in enumerate(zip(begins, sizes)):
if begin + size > max_pages:
clamped = max(0, max_pages - begin)
logger.warning(
"page copy would exceed req_to_page capacity for req %s: "
"begin=%s + size=%s = %s > req_to_page.shape[1]=%s; "
"clamping size to %s to avoid engine crash. The request is past "
"its context-length bound and will be finished by the length "
"check; KV writes after this point are dropped.",
request_ids[i] if i < len(request_ids) else "?",
begin,
size,
begin + size,
max_pages,
clamped,
)
sizes[i] = clamped
# Keep new_occupied_pages[i] consistent with the clamped size so
# the kernel's cumsum-based offsets stay aligned across the batch.
new_occupied_pages[i] = new_occupied_pages[i][:clamped]
new_occupied_pages_num = flatten_and_to_device(sizes, dtype=torch.int32)
pages_copy_starts = flatten_and_to_device(begins, dtype=torch.int32)
new_occupied_pages_t = flatten_and_to_device(new_occupied_pages, dtype=torch.int32)
request_pool_indices = flatten_and_to_device(
forward_op.request_pool_indices, dtype=torch.int64
)
update_req_to_page(
req_to_page=req_to_page,
req_pool_indices=request_pool_indices,
new_occupied_pages=new_occupied_pages_t,
new_occupied_pages_num=new_occupied_pages_num,
pages_copy_starts=pages_copy_starts,
)