Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

578 lines
20 KiB
Python

import sys
import pytest
import torch
from sglang.jit_kernel.hisparse import (
load_cache_to_device_buffer_dsv4_mla,
load_cache_to_device_buffer_mla,
transfer_cache_dsv4_mla,
)
from sglang.srt.utils import is_cuda, is_hip, is_npu, is_xpu
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
register_amd_ci(est_time=30, stage="stage-b", runner_config="1-gpu-small-amd")
register_cuda_ci(est_time=10, stage="base-b-kernel-unit", runner_config="1-gpu-large")
register_cuda_ci(est_time=120, suite="nightly-kernel-1-gpu", nightly=True)
pytestmark = pytest.mark.skipif(
not torch.cuda.is_available()
or is_npu()
or is_xpu()
or not (is_cuda() or is_hip()),
reason="HiSparse JIT tests require CUDA/ROCm.",
)
DEVICE = "cuda"
DTYPE = torch.float32
KV_DIM = 8
HOT_BUFFER_SIZE = 4
PADDED_BUFFER_SIZE = HOT_BUFFER_SIZE + 1
HOST_CACHE_SIZE = 16
DEVICE_CACHE_SIZE = 16
ITEM_SIZE_BYTES = KV_DIM * torch.empty((), dtype=DTYPE).element_size()
DSV4_PAGE_SIZE = 64
DSV4_VALUE_BYTES = 576
DSV4_SCALE_BYTES = 8
DSV4_ITEM_BYTES = DSV4_VALUE_BYTES + DSV4_SCALE_BYTES
DSV4_PAGE_BYTES = ((DSV4_ITEM_BYTES * DSV4_PAGE_SIZE + 575) // 576) * 576
DSV4_SCALE_OFFSET = DSV4_VALUE_BYTES * DSV4_PAGE_SIZE
def _host_cache() -> torch.Tensor:
host_cache = torch.empty(
(HOST_CACHE_SIZE, 1, KV_DIM), dtype=DTYPE, device="cpu", pin_memory=True
)
host_cache.copy_(torch.arange(host_cache.numel(), dtype=DTYPE).view_as(host_cache))
return host_cache
def _dsv4_token_pattern(seed: int) -> tuple[torch.Tensor, torch.Tensor]:
value = (
(torch.arange(DSV4_VALUE_BYTES, dtype=torch.int16) + seed)
.remainder(256)
.to(torch.uint8)
)
scale = (
(torch.arange(DSV4_SCALE_BYTES, dtype=torch.int16) + seed + 17)
.remainder(256)
.to(torch.uint8)
)
return value, scale
def _write_dsv4_token(cache: torch.Tensor, loc: int, seed: int) -> None:
page = loc // DSV4_PAGE_SIZE
offset = loc % DSV4_PAGE_SIZE
value, scale = _dsv4_token_pattern(seed)
cache[page, offset * DSV4_VALUE_BYTES : (offset + 1) * DSV4_VALUE_BYTES].copy_(
value.to(cache.device)
)
scale_start = DSV4_SCALE_OFFSET + offset * DSV4_SCALE_BYTES
cache[page, scale_start : scale_start + DSV4_SCALE_BYTES].copy_(
scale.to(cache.device)
)
def _read_dsv4_token(cache: torch.Tensor, loc: int) -> torch.Tensor:
page = loc // DSV4_PAGE_SIZE
offset = loc % DSV4_PAGE_SIZE
value = cache[page, offset * DSV4_VALUE_BYTES : (offset + 1) * DSV4_VALUE_BYTES]
scale_start = DSV4_SCALE_OFFSET + offset * DSV4_SCALE_BYTES
scale = cache[page, scale_start : scale_start + DSV4_SCALE_BYTES]
return torch.cat([value, scale])
def _dsv4_ptrs(cache: torch.Tensor) -> torch.Tensor:
return torch.tensor([cache.data_ptr()], dtype=torch.uint64, device=DEVICE)
def _run_kernel(
*,
top_k_tokens: torch.Tensor,
device_buffer_tokens: torch.Tensor,
host_cache_locs: torch.Tensor,
device_buffer_locs: torch.Tensor,
host_cache: torch.Tensor,
device_buffer: torch.Tensor,
lru_slots: torch.Tensor,
seq_len: int | None = None,
seq_lens: torch.Tensor | None = None,
seq_lens_dtype: torch.dtype = torch.int32,
req_pool_indices: torch.Tensor | None = None,
num_real_reqs: int | None = None,
) -> torch.Tensor:
batch_size = top_k_tokens.shape[0]
if req_pool_indices is None:
req_pool_indices = torch.arange(batch_size, dtype=torch.int64, device=DEVICE)
if seq_lens is None:
seq_lens = torch.full(
(batch_size,), seq_len, dtype=seq_lens_dtype, device=DEVICE
)
if num_real_reqs is None:
num_real_reqs = batch_size
out = torch.full_like(top_k_tokens, -1)
load_cache_to_device_buffer_mla(
top_k_tokens=top_k_tokens,
device_buffer_tokens=device_buffer_tokens,
host_cache_locs=host_cache_locs,
device_buffer_locs=device_buffer_locs,
host_cache=host_cache,
device_buffer=device_buffer,
top_k_device_locs=out,
req_pool_indices=req_pool_indices,
seq_lens=seq_lens,
lru_slots=lru_slots,
item_size_bytes=ITEM_SIZE_BYTES,
num_top_k=top_k_tokens.shape[1],
hot_buffer_size=HOT_BUFFER_SIZE,
page_size=1,
block_size=256,
num_real_reqs=torch.tensor([num_real_reqs], dtype=torch.int32, device=DEVICE),
)
torch.cuda.synchronize()
return out
def _make_state(
device_buffer_locs_rows: list[list[int]],
device_buffer_tokens_rows: list[list[int]],
newest_tokens: list[int],
):
host_cache = _host_cache()
device_buffer = torch.full(
(DEVICE_CACHE_SIZE, 1, KV_DIM), -1, dtype=DTYPE, device=DEVICE
)
device_buffer_locs = torch.tensor(
device_buffer_locs_rows, dtype=torch.int32, device=DEVICE
)
device_buffer_tokens = torch.tensor(
device_buffer_tokens_rows, dtype=torch.int32, device=DEVICE
)
lru_slots = (
torch.arange(HOT_BUFFER_SIZE, dtype=torch.int16, device=DEVICE)
.view(1, -1)
.repeat(device_buffer_locs.shape[0], 1)
)
host_cache_locs = (
torch.arange(HOST_CACHE_SIZE, dtype=torch.int64, device=DEVICE)
.view(1, -1)
.repeat(device_buffer_locs.shape[0], 1)
)
# Slots 0..3 participate in LRU; slot 4 is the reserved newest slot.
for rid, newest_token in enumerate(newest_tokens):
for slot, token in enumerate(device_buffer_tokens_rows[rid][:HOT_BUFFER_SIZE]):
if token >= 0:
device_buffer[device_buffer_locs[rid, slot]].copy_(
host_cache[token].to(DEVICE, non_blocking=True)
)
device_buffer[device_buffer_locs[rid, HOT_BUFFER_SIZE]].copy_(
host_cache[newest_token].to(DEVICE, non_blocking=True)
)
torch.cuda.synchronize()
return {
"host_cache": host_cache,
"device_buffer": device_buffer,
"device_buffer_locs": device_buffer_locs,
"device_buffer_tokens": device_buffer_tokens,
"lru_slots": lru_slots,
"host_cache_locs": host_cache_locs,
}
@pytest.mark.skipif(is_hip(), reason="DSV4 paged-layout HiSparse test is CUDA-only.")
def test_transfer_cache_dsv4_mla_copies_paged_token() -> None:
src_cache = torch.zeros((2, DSV4_PAGE_BYTES), dtype=torch.uint8, device=DEVICE)
dst_cache = torch.zeros(
(2, DSV4_PAGE_BYTES), dtype=torch.uint8, device="cpu", pin_memory=True
)
src_loc = DSV4_PAGE_SIZE + 6
dst_loc = DSV4_PAGE_SIZE + 1
_write_dsv4_token(src_cache, src_loc, seed=41)
transfer_cache_dsv4_mla(
src_ptrs=_dsv4_ptrs(src_cache),
dst_ptrs=_dsv4_ptrs(dst_cache),
src_indices=torch.tensor([src_loc], dtype=torch.int64, device=DEVICE),
dst_indices=torch.tensor([dst_loc], dtype=torch.int64, device=DEVICE),
)
torch.cuda.synchronize()
assert torch.equal(
_read_dsv4_token(dst_cache, dst_loc).to(DEVICE),
_read_dsv4_token(src_cache, src_loc),
)
@pytest.mark.skipif(is_hip(), reason="DSV4 paged-layout HiSparse test is CUDA-only.")
def test_dsv4_swap_in_reads_paged_host_layout() -> None:
host_cache = torch.zeros(
(2, DSV4_PAGE_BYTES), dtype=torch.uint8, device="cpu", pin_memory=True
)
device_buffer = torch.zeros((2, DSV4_PAGE_BYTES), dtype=torch.uint8, device=DEVICE)
host_loc = DSV4_PAGE_SIZE + 1
swap_loc = DSV4_PAGE_SIZE + 12
_write_dsv4_token(host_cache, host_loc, seed=41)
top_k_tokens = torch.tensor([[3]], dtype=torch.int32, device=DEVICE)
device_buffer_tokens = torch.full(
(1, PADDED_BUFFER_SIZE), -1, dtype=torch.int32, device=DEVICE
)
host_cache_locs = torch.zeros((1, 8), dtype=torch.int64, device=DEVICE)
host_cache_locs[0, 3] = host_loc
device_buffer_locs = torch.tensor(
[[swap_loc, swap_loc + 1, swap_loc + 2, swap_loc + 3, swap_loc + 4]],
dtype=torch.int32,
device=DEVICE,
)
lru_slots = torch.arange(HOT_BUFFER_SIZE, dtype=torch.int16, device=DEVICE).view(
1, -1
)
out = torch.full_like(top_k_tokens, -1)
load_cache_to_device_buffer_dsv4_mla(
top_k_tokens=top_k_tokens,
device_buffer_tokens=device_buffer_tokens,
host_cache_locs=host_cache_locs,
device_buffer_locs=device_buffer_locs,
host_cache=host_cache,
device_buffer=device_buffer,
top_k_device_locs=out,
req_pool_indices=torch.tensor([0], dtype=torch.int64, device=DEVICE),
seq_lens=torch.tensor([8], dtype=torch.int32, device=DEVICE),
lru_slots=lru_slots,
item_size_bytes=DSV4_ITEM_BYTES,
num_top_k=1,
hot_buffer_size=HOT_BUFFER_SIZE,
page_size=1,
block_size=256,
num_real_reqs=torch.tensor([1], dtype=torch.int32, device=DEVICE),
)
torch.cuda.synchronize()
assert out.item() == swap_loc
assert torch.equal(
_read_dsv4_token(device_buffer, swap_loc),
_read_dsv4_token(host_cache, host_loc).to(DEVICE),
)
def _long_case():
# One-request baseline used by the stateful cases below:
# req 0 LRU slots : [0, 1, 2, 3]
# req 0 cached tokens : slot0->1, slot1->4, slot2->2, slot3->5
# req 0 physical locs : slot0->9, slot1->7, slot2->3, slot3->5
# req 0 newest slot : slot4/newest -> token 7 at physical loc 11
return _make_state([[9, 7, 3, 5, 11]], [[1, 4, 2, 5, -1]], [7])
@pytest.mark.parametrize("seq_lens_dtype", [torch.int32, torch.int64])
def test_load_cache_to_device_buffer_fast_path(seq_lens_dtype: torch.dtype) -> None:
host_cache = _host_cache()
device_buffer = torch.arange(
DEVICE_CACHE_SIZE * KV_DIM, dtype=DTYPE, device=DEVICE
).view(DEVICE_CACHE_SIZE, 1, KV_DIM)
device_buffer_before = device_buffer.clone()
device_buffer_locs = torch.tensor(
[[13, 9, 5, 1, 15]], dtype=torch.int32, device=DEVICE
)
device_buffer_tokens = torch.tensor(
[[10, 11, 12, 13, -1]], dtype=torch.int32, device=DEVICE
)
device_buffer_tokens_before = device_buffer_tokens.clone()
lru_slots = torch.tensor([[0, 1, 2, 3]], dtype=torch.int16, device=DEVICE)
lru_slots_before = lru_slots.clone()
# Short-sequence layout:
# token position 0 -> physical loc 13
# token position 1 -> physical loc 9
# token position 2 -> physical loc 5
#
# seq_len <= HOT_BUFFER_SIZE should skip host loads and LRU mutations,
# so top_k_tokens acts like direct indexing into device_buffer_locs.
out = _run_kernel(
top_k_tokens=torch.tensor([[2, 0, 1]], dtype=torch.int32, device=DEVICE),
device_buffer_tokens=device_buffer_tokens,
host_cache_locs=torch.arange(
HOST_CACHE_SIZE, dtype=torch.int64, device=DEVICE
).view(1, -1),
device_buffer_locs=device_buffer_locs,
host_cache=host_cache,
device_buffer=device_buffer,
lru_slots=lru_slots,
seq_len=3,
seq_lens_dtype=seq_lens_dtype,
)
assert torch.equal(out.cpu(), torch.tensor([[5, 13, 9]], dtype=torch.int32))
assert torch.equal(device_buffer_tokens.cpu(), device_buffer_tokens_before.cpu())
assert torch.equal(lru_slots.cpu(), lru_slots_before.cpu())
assert torch.equal(device_buffer.cpu(), device_buffer_before.cpu())
def test_load_cache_to_device_buffer_hits_newest_and_updates_lru() -> None:
state = _long_case()
# Query [4, 2, 7]:
# 4 hits slot1 -> loc 7
# 2 hits slot2 -> loc 3
# 7 is the newest token -> reserved newest loc 11
#
# Hits move to the MRU tail, so [0, 1, 2, 3] becomes [0, 3, 1, 2].
out = _run_kernel(
top_k_tokens=torch.tensor([[4, 2, 7]], dtype=torch.int32, device=DEVICE),
seq_len=8,
**state,
)
assert torch.equal(out.cpu(), torch.tensor([[7, 3, 11]], dtype=torch.int32))
assert torch.equal(
state["device_buffer_tokens"].cpu(),
torch.tensor([[1, 4, 2, 5, -1]], dtype=torch.int32),
)
assert torch.equal(
state["lru_slots"].cpu(), torch.tensor([[0, 3, 1, 2]], dtype=torch.int16)
)
def test_load_cache_to_device_buffer_miss_uses_updated_lru_slot() -> None:
state = _long_case()
# Step 1: touch tokens [4, 2], so LRU becomes [0, 3, 1, 2].
# Step 2: query token 6, which is a miss.
# The kernel should reuse the new LRU head slot0, whose physical loc is 9.
# This round has no regular hits, so the freshly loaded miss slot ends up at the tail.
_run_kernel(
top_k_tokens=torch.tensor([[4, 2]], dtype=torch.int32, device=DEVICE),
seq_len=8,
**state,
)
out = _run_kernel(
top_k_tokens=torch.tensor([[6]], dtype=torch.int32, device=DEVICE),
seq_len=8,
**state,
)
assert torch.equal(out.cpu(), torch.tensor([[9]], dtype=torch.int32))
assert torch.equal(
state["device_buffer_tokens"].cpu(),
torch.tensor([[6, 4, 2, 5, -1]], dtype=torch.int32),
)
assert torch.equal(
state["lru_slots"].cpu(), torch.tensor([[3, 1, 2, 0]], dtype=torch.int16)
)
assert torch.equal(state["device_buffer"][9].cpu(), state["host_cache"][6])
def test_load_cache_to_device_buffer_multiple_misses_copy_all_slots() -> None:
state = _make_state(
[[9, 7, 3, 5, 11]],
[[0, 1, 2, 3, -1]],
[8],
)
out = _run_kernel(
top_k_tokens=torch.tensor([[4, 5, 6, 7]], dtype=torch.int32, device=DEVICE),
seq_len=9,
**state,
)
assert torch.equal(out.cpu(), torch.tensor([[9, 7, 3, 5]], dtype=torch.int32))
assert torch.equal(
state["device_buffer_tokens"].cpu(),
torch.tensor([[4, 5, 6, 7, -1]], dtype=torch.int32),
)
assert torch.equal(
state["lru_slots"].cpu(), torch.tensor([[0, 1, 2, 3]], dtype=torch.int16)
)
for token, loc in zip([4, 5, 6, 7], [9, 7, 3, 5]):
assert torch.equal(
state["device_buffer"][loc].cpu(), state["host_cache"][token]
)
def test_load_cache_to_device_buffer_batched_with_padding() -> None:
state = _make_state(
[
[9, 7, 3, 5, 11],
[12, 10, 8, 6, 14],
[15, 4, 2, 1, 13],
],
[
[1, 4, 2, 5, -1],
[0, 1, 2, 3, -1],
[9, 8, 7, 6, -1],
],
[7, 4, 5],
)
padded_tokens_before = state["device_buffer_tokens"][2].clone()
padded_lru_before = state["lru_slots"][2].clone()
# req 0: long path
# cached tokens/locs : 1@9, 4@7, 2@3, 5@5, newest 7@11
# query [4, 6, 7] : hit loc 7, miss into slot0/loc 9, newest loc 11
# LRU update : remaining evictables [2, 3], then miss [0], then hit [1]
# : [0, 1, 2, 3] -> [2, 3, 0, 1]
#
# req 1: fast path
# seq_len = 3 <= HOT_BUFFER_SIZE, so [2, 1, 0] maps directly to locs [8, 10, 12]
#
# req 2: padded block
# num_real_reqs = 2 means this row must be ignored entirely.
out = _run_kernel(
top_k_tokens=torch.tensor(
[[4, 6, 7], [2, 1, 0], [9, 8, 7]], dtype=torch.int32, device=DEVICE
),
seq_lens=torch.tensor([8, 3, 8], dtype=torch.int32, device=DEVICE),
num_real_reqs=2,
**state,
)
assert torch.equal(
out.cpu(),
torch.tensor([[7, 9, 11], [8, 10, 12], [-1, -1, -1]], dtype=torch.int32),
)
assert torch.equal(
state["device_buffer_tokens"][:2].cpu(),
torch.tensor([[6, 4, 2, 5, -1], [0, 1, 2, 3, -1]], dtype=torch.int32),
)
assert torch.equal(
state["lru_slots"][:2].cpu(),
torch.tensor([[2, 3, 0, 1], [0, 1, 2, 3]], dtype=torch.int16),
)
assert torch.equal(
state["device_buffer_tokens"][2].cpu(), padded_tokens_before.cpu()
)
assert torch.equal(state["lru_slots"][2].cpu(), padded_lru_before.cpu())
assert torch.equal(state["device_buffer"][9].cpu(), state["host_cache"][6])
def test_load_cache_to_device_buffer_dsv4_mla_miss_copy_layout() -> None:
# Both the host cache and the device buffer use the page-padded C4 layout,
# matching DeepSeekV4PagedHostPool, the backup/write path, and the swap-in
# kernel on both CUDA and ROCm. The miss copy must read the host source with
# paged addressing (get_pointer_paged), not a linear per-item stride.
num_pages = (HOST_CACHE_SIZE + DSV4_PAGE_SIZE - 1) // DSV4_PAGE_SIZE
state = _long_case()
host_cache = torch.zeros(
(num_pages, DSV4_PAGE_BYTES),
dtype=torch.uint8,
device="cpu",
pin_memory=True,
)
for token in range(HOST_CACHE_SIZE):
_write_dsv4_token(host_cache, token, seed=token + 1)
device_buffer = torch.full(
(num_pages, DSV4_PAGE_BYTES),
0xFF,
dtype=torch.uint8,
device=DEVICE,
)
out = torch.full((1, 1), -1, dtype=torch.int32, device=DEVICE)
# Token 6 is a miss in _long_case(), so it should be copied into evict slot 0,
# whose physical device loc is 9.
load_cache_to_device_buffer_dsv4_mla(
top_k_tokens=torch.tensor([[6]], dtype=torch.int32, device=DEVICE),
device_buffer_tokens=state["device_buffer_tokens"],
host_cache_locs=state["host_cache_locs"],
device_buffer_locs=state["device_buffer_locs"],
host_cache=host_cache,
device_buffer=device_buffer,
top_k_device_locs=out,
req_pool_indices=torch.tensor([0], dtype=torch.int64, device=DEVICE),
seq_lens=torch.tensor([8], dtype=torch.int32, device=DEVICE),
lru_slots=state["lru_slots"],
item_size_bytes=DSV4_ITEM_BYTES,
num_top_k=1,
hot_buffer_size=HOT_BUFFER_SIZE,
page_size=DSV4_PAGE_SIZE,
block_size=256,
num_real_reqs=torch.tensor([1], dtype=torch.int32, device=DEVICE),
)
torch.cuda.synchronize()
assert torch.equal(out.cpu(), torch.tensor([[9]], dtype=torch.int32))
# host_cache_locs[token=6] == 6 in _long_case(); evict slot 0 -> device loc 9.
assert torch.equal(
_read_dsv4_token(device_buffer, 9).cpu(),
_read_dsv4_token(host_cache, 6),
)
@pytest.mark.skipif(
not is_hip(), reason="Covers a ROCm wavefront64 LRU writeback regression."
)
def test_load_cache_to_device_buffer_rocm_large_lru_writeback() -> None:
top_k = 2048
hot_buffer_size = 4096
seq_len = 7299
kv_dim = 4
item_size_bytes = kv_dim * torch.empty((), dtype=DTYPE).element_size()
top_k_tokens = torch.cat(
[
torch.arange(1000, 2000, dtype=torch.int32),
torch.arange(5000, 6048, dtype=torch.int32),
]
).view(1, -1)
device_buffer_tokens = torch.arange(hot_buffer_size, dtype=torch.int32).view(1, -1)
device_buffer_locs = torch.arange(hot_buffer_size + 1, dtype=torch.int32).view(
1, -1
)
lru_slots = torch.arange(hot_buffer_size, dtype=torch.int16).view(1, -1)
host_cache_locs = torch.arange(seq_len, dtype=torch.int64).view(1, -1)
top_k_tokens = top_k_tokens.to(DEVICE)
device_buffer_tokens = device_buffer_tokens.to(DEVICE)
device_buffer_locs = device_buffer_locs.to(DEVICE)
lru_slots = lru_slots.to(DEVICE)
host_cache_locs = host_cache_locs.to(DEVICE)
host_cache = torch.empty((seq_len, 1, kv_dim), dtype=DTYPE, pin_memory=True)
host_cache.zero_()
device_buffer = torch.empty(
(hot_buffer_size + 1, 1, kv_dim), dtype=DTYPE, device=DEVICE
)
out = torch.full_like(top_k_tokens, -1)
load_cache_to_device_buffer_mla(
top_k_tokens=top_k_tokens,
device_buffer_tokens=device_buffer_tokens,
host_cache_locs=host_cache_locs,
device_buffer_locs=device_buffer_locs,
host_cache=host_cache,
device_buffer=device_buffer,
top_k_device_locs=out,
req_pool_indices=torch.tensor([0], dtype=torch.int64, device=DEVICE),
seq_lens=torch.tensor([seq_len], dtype=torch.int32, device=DEVICE),
lru_slots=lru_slots,
item_size_bytes=item_size_bytes,
num_top_k=top_k,
hot_buffer_size=hot_buffer_size,
page_size=1,
block_size=1024,
num_real_reqs=torch.tensor([1], dtype=torch.int32, device=DEVICE),
)
torch.cuda.synchronize()
expected_lru = torch.cat(
[
torch.arange(2048, 4096, dtype=torch.int16),
torch.arange(0, 1000, dtype=torch.int16),
torch.arange(2000, 2048, dtype=torch.int16),
torch.arange(1000, 2000, dtype=torch.int16),
]
)
assert torch.equal(lru_slots.cpu().view(-1), expected_lru)
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-v", "-s"]))