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
224 lines
7.2 KiB
Python
Executable File
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
|