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

224 lines
7.2 KiB
Python
Executable File

# 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.
import torch
import triton
import triton.language as tl
from tokenspeed.runtime.distributed.process_group_manager import (
process_group_manager as pg_manager,
)
from tokenspeed.runtime.layers.attention.configs.base import BaseAttnConfig
from tokenspeed.runtime.utils import get_available_gpu_memory
@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):
offset = tl.arange(0, BLOCK_SIZE) + 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)
# --- Page table helpers (shared across attention backends) ---
def build_page_table(
req_pool_indices: torch.Tensor,
req_to_page: torch.Tensor,
page_size: int,
max_seq_len_k: int,
) -> torch.Tensor:
"""Build page table from req_to_page.
req_to_page: [req_pool_size+1, max_pages] containing page IDs.
Returns: [bs, max_pages_needed] page table slice.
"""
max_pages = (max_seq_len_k + page_size - 1) // page_size
return req_to_page[req_pool_indices, :max_pages]
def update_page_table_inplace(
page_table_buf: torch.Tensor,
req_pool_indices: torch.Tensor,
req_to_page: torch.Tensor,
page_size: int,
max_seq_len_k: int,
):
"""Copy page table from req_to_page into pre-allocated CUDA graph buffer."""
max_pages = (max_seq_len_k + page_size - 1) // page_size
page_table_buf[:, :max_pages].copy_(req_to_page[req_pool_indices, :max_pages])
def token_indices_from_pages(
req_pool_indices: torch.Tensor,
token_positions: torch.Tensor,
req_to_page: torch.Tensor,
page_size: int,
) -> torch.Tensor:
"""Convert token positions to KV slot indices using req_to_page.
token_positions: [bs, num_tokens] — token offsets within each request.
Returns: [bs, num_tokens] — KV cache slot IDs (page_id * page_size + offset).
"""
page_indices = token_positions // page_size
offsets = token_positions % page_size
page_ids = req_to_page[req_pool_indices].gather(1, page_indices)
return page_ids * page_size + offsets
# --- Page-based memory profiling ---
def profile_available_cache_memory_bytes(
attn_config: BaseAttnConfig,
gpu_id: int,
tp_size: int,
gpu_memory_utilization: float,
total_gpu_memory: int,
world_group=None,
) -> int:
cpu_group = (
pg_manager.get_process_group("gloo", world_group)
if world_group is not None
else None
)
available_gpu_memory = get_available_gpu_memory(
attn_config.device,
gpu_id,
distributed=tp_size > 1,
cpu_group=cpu_group,
)
cache_memory = available_gpu_memory - total_gpu_memory * (
1 - gpu_memory_utilization
)
return int(cache_memory * (1 << 30))
def profile_max_num_pages(
attn_config: BaseAttnConfig,
gpu_id: int,
tp_size: int,
gpu_memory_utilization: float,
page_size: int,
num_attention_layers: int,
total_gpu_memory: int,
world_group=None,
draft_attn_config: BaseAttnConfig | None = None,
draft_num_attention_layers: int | None = None,
cache_cell_size: int | None = None,
draft_cache_cell_size: int | None = None,
):
cache_memory = profile_available_cache_memory_bytes(
attn_config,
gpu_id,
tp_size,
gpu_memory_utilization,
total_gpu_memory,
world_group,
)
if cache_cell_size is None:
cell_size = attn_config.cache_cell_size() * num_attention_layers
else:
cell_size = cache_cell_size
if draft_attn_config is not None:
if draft_cache_cell_size is None:
cell_size += (
draft_attn_config.cache_cell_size() * draft_num_attention_layers
)
else:
cell_size += draft_cache_cell_size
if cell_size <= 0:
raise ValueError(f"KV cache cell size must be positive, got {cell_size}")
max_num_token = cache_memory // cell_size
max_num_pages = (max_num_token + page_size - 1) // page_size
return max_num_pages
def profile_cache_budget(
attn_config: BaseAttnConfig,
gpu_id: int,
tp_size: int,
mem_fraction_static: float,
page_size: int,
num_attention_layers: int,
total_gpu_memory: int,
mamba_memory_per_chunk: int,
mamba_ratio: float,
world_group=None,
draft_attn_config: BaseAttnConfig | None = None,
draft_num_attention_layers: int | None = None,
) -> tuple[int, int]:
"""Profile GPU memory and split between KV pages and mamba chunks.
Returns:
(kv_max_num_pages, mamba_pool_total_chunks)
"""
total_cache_memory = profile_available_cache_memory_bytes(
attn_config,
gpu_id,
tp_size,
mem_fraction_static,
total_gpu_memory,
world_group,
)
cell_size = attn_config.cache_cell_size() * num_attention_layers
if draft_attn_config is not None:
cell_size += draft_attn_config.cache_cell_size() * draft_num_attention_layers
kv_memory = int(total_cache_memory / (1 + mamba_ratio))
mamba_memory = total_cache_memory - kv_memory
kv_cell_size = cell_size * page_size
kv_max_num_pages = int(kv_memory // kv_cell_size)
mamba_pool_total_chunks = max(int(mamba_memory // mamba_memory_per_chunk), 2)
return kv_max_num_pages, mamba_pool_total_chunks