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

243 lines
9.3 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.
from __future__ import annotations
import torch
from tokenspeed_kernel.ops.quantization import quantize_fp8_with_scale
from tokenspeed.runtime.layers.attention.configs.dsa import dsa_index_k_row_bytes
from tokenspeed.runtime.layers.attention.kv_cache.mla import (
MLATokenToKVPool,
_get_tensor_size_bytes,
)
_INDEX_K_FP8_GROUP_SIZE = 128
_INDEX_K_SCALE_BYTES = torch._utils._element_size(torch.float32)
class DSATokenToKVPool(MLATokenToKVPool):
def __init__(
self,
*args,
index_head_dim: int,
**kwargs,
):
self.index_head_dim = int(index_head_dim)
self.index_k_row_bytes = dsa_index_k_row_bytes(self.index_head_dim)
super().__init__(*args, **kwargs)
with self.memory_saver_adapter.region():
self.index_k_buffer = [
torch.zeros(
(self.size + self.page_size, self.index_k_row_bytes),
dtype=torch.uint8,
device=self.device,
)
for _ in range(self.layer_num)
]
self.index_k_data_ptrs = torch.tensor(
[buf.data_ptr() for buf in self.index_k_buffer],
dtype=torch.uint64,
device=self.device,
)
def _get_page_size_bytes(self):
index_size_bytes = self.index_k_row_bytes
return (
super()._get_page_size_bytes()
+ self.page_size * self.layer_num * index_size_bytes
)
def get_kv_size_bytes(self):
return super().get_kv_size_bytes() + _get_tensor_size_bytes(self.index_k_buffer)
def move_kv_cache(self, tgt_loc: torch.Tensor, src_loc: torch.Tensor):
super().move_kv_cache(tgt_loc, src_loc)
if tgt_loc.numel() == 0:
return
tgt_loc_flat = tgt_loc.view(-1).long()
src_loc_flat = src_loc.view(-1).long()
for buf in self.index_k_buffer:
# Packed FP8 index-K is block-split per page, so a single token's
# bytes are NOT a contiguous row; move the FP8 values and FP32
# scales through their block-split views instead.
fp8_view, scale_view = self._index_k_block_views(buf)
ps = self.page_size
tgt_page = tgt_loc_flat // ps
tgt_slot = tgt_loc_flat % ps
src_page = src_loc_flat // ps
src_slot = src_loc_flat % ps
fp8_view[tgt_page, tgt_slot] = fp8_view[src_page, src_slot]
scale_view[tgt_page, tgt_slot] = scale_view[src_page, src_slot]
def has_index_k_buffer(self) -> bool:
return True
def get_index_k_buffer(self, layer_id: int) -> torch.Tensor:
if self.layer_transfer_counter is not None:
self.layer_transfer_counter.wait_until(layer_id)
return self.index_k_buffer[layer_id]
def set_index_k_buffer(
self,
layer_id: int,
loc: torch.Tensor,
index_k: torch.Tensor,
) -> None:
if index_k.dtype != self.model_dtype:
index_k = index_k.to(self.model_dtype)
index_k = index_k.view(-1, self.index_head_dim)
self._set_index_k_buffer(layer_id, loc, index_k)
def _index_k_block_views(
self, buf: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor]:
"""Return per-page block-split views into a packed FP8 index-K buffer.
DeepGEMM's ``fp8_paged_mqa_logits`` expects each page of ``page_size``
tokens to be laid out as ``[page_size * head_dim FP8 values]`` followed
by ``[page_size * num_groups FP32 scales]`` (block-split), NOT a
per-token ``[fp8 | scale]`` interleave. The two ``as_strided`` views
below alias the same storage as ``buf`` so writes land in place.
Args:
buf: Packed FP8 index-K buffer of shape ``[num_slots, row_bytes]``
and dtype ``uint8``, where ``row_bytes == head_dim +
scale_bytes`` and ``num_slots`` is a multiple of
``page_size``.
Returns:
``(fp8_view, scale_view)`` where ``fp8_view`` has shape
``[num_pages, page_size, head_dim]`` (FP8 e4m3) and ``scale_view``
has shape ``[num_pages, page_size, num_groups]`` (float32), both
indexed as ``view[page, slot_in_page]``.
"""
ps = self.page_size
hd = self.index_head_dim
ng = hd // _INDEX_K_FP8_GROUP_SIZE
row = hd + ng * _INDEX_K_SCALE_BYTES
num_pages = buf.shape[0] // ps
page_bytes = ps * row
flat = buf.reshape(-1)
fp8_view = torch.as_strided(
flat.view(torch.float8_e4m3fn),
(num_pages, ps, hd),
(page_bytes, hd, 1),
)
scale_view = torch.as_strided(
flat.view(torch.float32),
(num_pages, ps, ng),
(page_bytes // 4, ng, 1),
(ps * hd) // 4,
)
return fp8_view, scale_view
def gather_index_k(
self, layer_id: int, slots: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor]:
"""Gather per-token FP8 index-K values and scales from the cache.
The packed FP8 index-K buffer is stored block-split per page (see
:meth:`_index_k_block_views`), so the non-paged prefill
scoring kernel (``fp8_mqa_logits``), which consumes contiguous
``(k_fp8, k_scale)`` tensors, must gather token rows through the
block-split views rather than indexing raw rows.
Args:
layer_id: Layer whose index-K cache to read.
slots: 1D int tensor of global token slot indices to gather.
Returns:
``(k_fp8, k_scale)`` where ``k_fp8`` has shape
``[num_slots, head_dim]`` (FP8 e4m3) and ``k_scale`` has shape
``[num_slots, num_groups]`` (float32).
"""
buf = self.get_index_k_buffer(layer_id)
fp8_view, scale_view = self._index_k_block_views(buf)
slots = slots.to(torch.long)
page = slots // self.page_size
slot_in_page = slots % self.page_size
k_fp8 = fp8_view[page, slot_in_page]
k_scale = scale_view[page, slot_in_page]
return k_fp8, k_scale
def _set_index_k_buffer(
self,
layer_id: int,
loc: torch.Tensor,
index_k: torch.Tensor,
) -> None:
buf = self.index_k_buffer[layer_id]
index_k_fp8, index_k_scale = quantize_fp8_with_scale(
index_k,
granularity="token_group",
group_size=_INDEX_K_FP8_GROUP_SIZE,
scale_encoding="float32",
)
fp8_view, scale_view = self._index_k_block_views(buf)
loc = loc.to(torch.long)
page = loc // self.page_size
slot_in_page = loc % self.page_size
fp8_view[page, slot_in_page] = index_k_fp8.view(-1, self.index_head_dim)
scale_view[page, slot_in_page] = index_k_scale.view(
-1, self.index_head_dim // _INDEX_K_FP8_GROUP_SIZE
)
def get_contiguous_buf_infos(self):
data_ptrs, data_lens, item_lens = super().get_contiguous_buf_infos()
data_ptrs = list(data_ptrs)
data_lens = list(data_lens)
item_lens = list(item_lens)
for buf in self.index_k_buffer:
data_ptrs.append(buf.data_ptr())
data_lens.append(buf.nbytes)
item_lens.append(buf[0].nbytes * self.page_size)
return data_ptrs, data_lens, item_lens
def get_layerwise_buf_info_offsets(self, start_idx=0):
offsets = super().get_layerwise_buf_info_offsets(start_idx)
if self.quant_method == "per_token_head":
base_count = 3 * self.layer_num
else:
base_count = self.layer_num
return [
layer_offsets + [start_idx + base_count + layer_id]
for layer_id, layer_offsets in enumerate(offsets)
]
def get_cpu_copy(self, token_indices: list[int]) -> torch.Tensor:
del token_indices
raise NotImplementedError(
"DSA KV cache offload is not implemented; sparse/indexer cache "
"buffers require page-aware layout handling."
)
def load_cpu_copy(
self, kv_cache_cpu: torch.Tensor, token_indices: list[int]
) -> None:
del kv_cache_cpu, token_indices
raise NotImplementedError(
"DSA KV cache reload is not implemented; sparse/indexer cache "
"buffers require page-aware layout handling."
)