Files
2026-07-13 12:24:33 +08:00

972 lines
31 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Standard
from typing import List
import random
# Third Party
import pytest
import torch
# First Party
from lmcache.v1.memory_allocators.pin_memory_allocator import PinMemoryAllocator
if not torch.cuda.is_available():
pytest.skip(
"CUDA is not available, skipping the test",
allow_module_level=True,
)
# First Party
if torch.cuda.is_available():
try:
# First Party
import lmcache.c_ops as lmc_ops
except ImportError:
lmc_ops = None
else:
lmc_ops = None
# Mock c_ops when not available
if lmc_ops is None:
class MockEngineKVFormat:
NL_X_TWO_NB_BS_NH_HS = 0
NL_X_NB_TWO_BS_NH_HS = 1
NL_X_NB_BS_HS = 2
NL_X_TWO_NB_NH_BS_HS = 3
NL_X_NB_TWO_NH_BS_HS = 4
class MockTransferDirection:
H2D = 0
D2H = 1
class MockCOps:
EngineKVFormat = MockEngineKVFormat
GPUKVFormat = MockEngineKVFormat
TransferDirection = MockTransferDirection
lmc_ops = MockCOps()
# Local
from .utils import (
check_mem_obj_equal,
check_paged_kv_cache_equal,
generate_kv_cache_paged,
generate_kv_cache_paged_list_tensors,
)
def _tuple_kv_to_blob(
kv_tensors,
) -> torch.Tensor:
k_temp = []
v_temp = []
for kv_layer in kv_tensors:
k_temp.append(kv_layer[0])
v_temp.append(kv_layer[1])
k_tensor_blob = torch.stack(k_temp)
v_tensor_blob = torch.stack(v_temp)
# kv_tensors: [num_layer, 2, num_tok, num_kv_head, head_size]
kv_tensors_flatten = torch.stack((k_tensor_blob, v_tensor_blob))
kv_tensors_flatten = kv_tensors_flatten.permute([1, 0, 2, 3, 4])
return kv_tensors_flatten
def _slice_kv_at(
start_idx: int,
kv_tensors: torch.Tensor,
chunk_size: int,
) -> List[torch.Tensor]:
return [
x.contiguous()
for x in list(
torch.split(
kv_tensors[:, :, start_idx:, ...],
chunk_size,
dim=2,
)
)
]
@pytest.mark.parametrize("num_tokens", [256, 500, 1024, 8000])
def test_extract_and_load_back(num_tokens):
device = "cuda"
num_blocks = 1000
block_size = 16
num_heads = 8
head_size = 128
num_layers = 32
dtype = torch.bfloat16
kv_cache = generate_kv_cache_paged(num_blocks, device, block_size, dtype)
slot_mapping = random.sample(range(0, num_blocks * block_size), num_tokens)
slot_mapping = torch.tensor(slot_mapping, device=device)
pinned_cpu_size = 4 * 1024 * 1024 * 1024 # 4GB
mem_allocator = PinMemoryAllocator(pinned_cpu_size)
# Old extract
kv_tuple_list = []
memory_obj_old_list = []
chunk_size = 256
start_event = torch.cuda.Event(enable_timing=True)
end_event = torch.cuda.Event(enable_timing=True)
start_event.record()
for layer_id in range(num_layers):
key_cache = kv_cache[layer_id][0].reshape(-1, num_heads, head_size)
value_cache = kv_cache[layer_id][1].reshape(-1, num_heads, head_size)
kv_tuple_list.append((key_cache[slot_mapping], value_cache[slot_mapping]))
kv_blob = _tuple_kv_to_blob(kv_tuple_list)
kv_chunked = _slice_kv_at(0, kv_blob, chunk_size)
for chunk_id, chunk in enumerate(kv_chunked):
mem_obj_shape = torch.Size(
[2, num_layers, chunk.shape[2], num_heads * head_size]
)
memory_obj_old = mem_allocator.allocate(mem_obj_shape, dtype)
chunk = chunk.contiguous()
for layer_id in range(num_layers):
memory_obj_old.tensor[0, layer_id].copy_(
chunk[layer_id, 0].reshape(-1, 1024)
)
memory_obj_old.tensor[1, layer_id].copy_(
chunk[layer_id, 1].reshape(-1, 1024)
)
memory_obj_old_list.append(memory_obj_old)
end_event.record()
torch.cuda.synchronize()
elapsed_time_ms = start_event.elapsed_time(end_event)
print("Old extract time: ", elapsed_time_ms / 1000)
# New extract (zero-copy kernels)
memory_obj_new_list = []
start_event = torch.cuda.Event(enable_timing=True)
end_event = torch.cuda.Event(enable_timing=True)
start_event.record()
slot_mapping_chunked = torch.split(slot_mapping, chunk_size)
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
mem_obj_shape = torch.Size(
[2, num_layers, len(slot_mapping_temp), num_heads * head_size]
)
memory_obj_new = mem_allocator.allocate(mem_obj_shape, dtype)
for layer_id in range(num_layers):
lmc_ops.load_and_reshape_flash(
memory_obj_new.tensor,
kv_cache[layer_id][0],
kv_cache[layer_id][1],
slot_mapping_temp,
layer_id,
)
memory_obj_new_list.append(memory_obj_new)
end_event.record()
# wait for all the operations to finish
torch.cuda.synchronize()
elapsed_time_ms = start_event.elapsed_time(end_event)
print("New extract time: ", elapsed_time_ms / 1000)
check_mem_obj_equal(
memory_obj_old_list,
memory_obj_new_list,
)
# Generate new paged kv_cache
kv_cache_new = generate_kv_cache_paged(num_blocks, device, block_size, dtype)
# New load back (zero-copy kernels)
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
memory_obj_new = memory_obj_new_list[chunk_id]
for layer_id in range(num_layers):
lmc_ops.reshape_and_cache_back_flash(
memory_obj_new.tensor,
kv_cache_new[layer_id][0],
kv_cache_new[layer_id][1],
slot_mapping_temp,
layer_id,
)
check_paged_kv_cache_equal(
kv_cache,
kv_cache_new,
slot_mapping,
)
mem_allocator.close()
@pytest.mark.parametrize("num_tokens", [256, 500, 1024, 8000])
@pytest.mark.parametrize(
"engine_kv_format",
[
lmc_ops.EngineKVFormat.NL_X_TWO_NB_BS_NH_HS, # vllm non-MLA flash attention
lmc_ops.EngineKVFormat.NL_X_NB_TWO_BS_NH_HS, # vllm non-MLA flash infer
],
)
def test_multi_layer_kernel(num_tokens, engine_kv_format):
device = "cuda"
num_blocks = 1000
block_size = 16
num_heads = 8
head_size = 128
chunk_size = 256
num_layers = 32
dtype = torch.bfloat16
kv_cache = generate_kv_cache_paged_list_tensors(
num_blocks, device, block_size, dtype, engine_kv_format=engine_kv_format
)
# old deprecated kernels only handle vllm non-MLA flash attention format
if engine_kv_format == lmc_ops.EngineKVFormat.NL_X_NB_TWO_BS_NH_HS:
kv_cache_force_old = [
kv_layer.clone().permute(1, 0, 2, 3, 4).contiguous()
for kv_layer in kv_cache
]
else:
kv_cache_force_old = kv_cache
page_buffer_size = num_blocks * block_size
slot_mapping = random.sample(range(0, page_buffer_size), num_tokens)
slot_mapping = torch.tensor(slot_mapping, device=device)
pinned_cpu_size = 4 * 1024 * 1024 * 1024 # 4GB
mem_allocator = PinMemoryAllocator(pinned_cpu_size)
# layer by layer extract
memory_obj_old_list = []
start_event = torch.cuda.Event(enable_timing=True)
end_event = torch.cuda.Event(enable_timing=True)
start_event.record()
slot_mapping_chunked = torch.split(slot_mapping, chunk_size)
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
mem_obj_shape = torch.Size(
[2, num_layers, len(slot_mapping_temp), num_heads * head_size]
)
memory_obj_old = mem_allocator.allocate(mem_obj_shape, dtype)
for layer_id in range(num_layers):
lmc_ops.load_and_reshape_flash(
memory_obj_old.tensor,
kv_cache_force_old[layer_id][0],
kv_cache_force_old[layer_id][1],
slot_mapping_temp,
layer_id,
)
memory_obj_old_list.append(memory_obj_old)
end_event.record()
# wait for all the operations to finish
torch.cuda.synchronize()
elapsed_time_ms = start_event.elapsed_time(end_event)
print("Old extract time: ", elapsed_time_ms / 1000)
# New extract with multi layer kernel
kv_cache_pointers = torch.empty(
num_layers, dtype=torch.int64, device="cpu", pin_memory=True
)
for i in range(num_layers):
kv_cache_pointers[i] = kv_cache[i].data_ptr()
memory_obj_new_list = []
start_event = torch.cuda.Event(enable_timing=True)
end_event = torch.cuda.Event(enable_timing=True)
start_event.record()
slot_mapping_chunked = torch.split(slot_mapping, chunk_size)
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
mem_obj_shape = torch.Size(
[2, num_layers, len(slot_mapping_temp), num_heads * head_size]
)
memory_obj_new = mem_allocator.allocate(mem_obj_shape, dtype)
lmc_ops.multi_layer_kv_transfer(
memory_obj_new.tensor,
kv_cache_pointers,
slot_mapping_temp,
kv_cache[0].device,
page_buffer_size,
lmc_ops.TransferDirection.D2H,
engine_kv_format,
block_size,
)
memory_obj_new_list.append(memory_obj_new)
end_event.record()
# wait for all the operations to finish
torch.cuda.synchronize()
elapsed_time_ms = start_event.elapsed_time(end_event)
print("New extract time: ", elapsed_time_ms / 1000)
check_mem_obj_equal(
memory_obj_old_list,
memory_obj_new_list,
)
# Generate new paged kv_cache
kv_cache_new = generate_kv_cache_paged_list_tensors(
num_blocks, device, block_size, dtype, engine_kv_format=engine_kv_format
)
kv_cache_pointers_new = torch.empty(
num_layers, dtype=torch.int64, device="cpu", pin_memory=True
)
for i in range(num_layers):
kv_cache_pointers_new[i] = kv_cache_new[i].data_ptr()
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
memory_obj_new = memory_obj_new_list[chunk_id]
lmc_ops.multi_layer_kv_transfer(
memory_obj_new.tensor,
kv_cache_pointers_new,
slot_mapping_temp,
kv_cache_new[0].device,
page_buffer_size,
lmc_ops.TransferDirection.H2D,
engine_kv_format,
block_size,
)
check_paged_kv_cache_equal(
kv_cache,
kv_cache_new,
slot_mapping,
engine_kv_format=engine_kv_format,
)
mem_allocator.close()
@pytest.mark.parametrize("num_tokens", [256, 500, 1024, 8000])
@pytest.mark.parametrize("head_size", [576, 66]) # Use 68 for dsv32 (132x int8)
@pytest.mark.parametrize(
"engine_kv_format",
[lmc_ops.EngineKVFormat.NL_X_NB_BS_HS], # vllm MLA
)
def test_multi_layer_kernel_use_mla(num_tokens, head_size, engine_kv_format):
device = "cuda"
num_blocks = 1000
block_size = 64
chunk_size = 256
dtype = torch.bfloat16
num_layers = 32
kv_cache = generate_kv_cache_paged_list_tensors(
num_blocks,
device,
block_size,
dtype,
num_layers,
head_size,
engine_kv_format=engine_kv_format,
)
slot_mapping = random.sample(range(0, num_blocks * block_size), num_tokens)
slot_mapping = torch.tensor(slot_mapping, device=device)
pinned_cpu_size = 4 * 1024 * 1024 * 1024 # 4GB
mem_allocator = PinMemoryAllocator(pinned_cpu_size)
# layer by layer extract
memory_obj_old_list = []
start_event = torch.cuda.Event(enable_timing=True)
end_event = torch.cuda.Event(enable_timing=True)
start_event.record()
slot_mapping_chunked = torch.split(slot_mapping, chunk_size)
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
mem_obj_shape = torch.Size([1, num_layers, len(slot_mapping_temp), head_size])
memory_obj_old = mem_allocator.allocate(mem_obj_shape, dtype)
for layer_id in range(num_layers):
for token_idx, slot_idx in enumerate(slot_mapping_temp):
slot_idx = slot_idx.item()
block_idx = slot_idx // block_size
block_offset = slot_idx % block_size
memory_obj_old.tensor[0][layer_id][token_idx] = kv_cache[layer_id][
block_idx
][block_offset]
memory_obj_old_list.append(memory_obj_old)
end_event.record()
# wait for all the operations to finish
torch.cuda.synchronize()
elapsed_time_ms = start_event.elapsed_time(end_event)
print("Old extract time: ", elapsed_time_ms / 1000)
# New extract with multi layer kernel
kv_cache_pointers = torch.empty(
num_layers, dtype=torch.int64, device="cpu", pin_memory=True
)
for i in range(num_layers):
kv_cache_pointers[i] = kv_cache[i].data_ptr()
memory_obj_new_list = []
start_event = torch.cuda.Event(enable_timing=True)
end_event = torch.cuda.Event(enable_timing=True)
start_event.record()
slot_mapping_chunked = torch.split(slot_mapping, chunk_size)
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
mem_obj_shape = torch.Size([1, num_layers, len(slot_mapping_temp), head_size])
memory_obj_new = mem_allocator.allocate(mem_obj_shape, dtype)
lmc_ops.multi_layer_kv_transfer(
memory_obj_new.tensor,
kv_cache_pointers,
slot_mapping_temp,
kv_cache[0].device,
0,
lmc_ops.TransferDirection.D2H,
engine_kv_format,
block_size,
)
memory_obj_new_list.append(memory_obj_new)
end_event.record()
# wait for all the operations to finish
torch.cuda.synchronize()
elapsed_time_ms = start_event.elapsed_time(end_event)
print("New extract time: ", elapsed_time_ms / 1000)
for left_mem_obj, right_mem_obj in zip(
memory_obj_old_list, memory_obj_new_list, strict=False
):
left_kv, right_kv = left_mem_obj.tensor[0], right_mem_obj.tensor[0]
right_kv = right_kv.to(left_kv.device)
assert len(left_kv.shape) == 3
assert len(right_kv.shape) == 3
assert (left_kv[:, :, :] == right_kv[:, :, :]).all()
# Generate new paged kv_cache
kv_cache_new = generate_kv_cache_paged_list_tensors(
num_blocks,
device,
block_size,
dtype,
num_layers,
head_size,
engine_kv_format=engine_kv_format,
)
kv_cache_pointers_new = torch.empty(
num_layers, dtype=torch.int64, device="cpu", pin_memory=True
)
for i in range(num_layers):
kv_cache_pointers_new[i] = kv_cache_new[i].data_ptr()
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
memory_obj_new = memory_obj_new_list[chunk_id]
lmc_ops.multi_layer_kv_transfer(
memory_obj_new.tensor,
kv_cache_pointers_new,
slot_mapping_temp,
kv_cache_new[0].device,
0,
lmc_ops.TransferDirection.H2D,
engine_kv_format,
block_size,
)
for left_kv, right_kv in zip(kv_cache, kv_cache_new, strict=False):
assert len(left_kv.shape) == 3
assert len(right_kv.shape) == 3
left_reshaped = left_kv.reshape(
left_kv.shape[0] * left_kv.shape[1], left_kv.shape[2]
)
right_reshaped = right_kv.reshape(
right_kv.shape[0] * right_kv.shape[1], right_kv.shape[2]
)
assert (left_reshaped[slot_mapping, :] == right_reshaped[slot_mapping, :]).all()
mem_allocator.close()
@pytest.mark.parametrize("num_tokens", [256, 500, 1024, 8000])
@pytest.mark.parametrize("token_major", [True, False])
@pytest.mark.parametrize(
"engine_kv_format",
[
lmc_ops.EngineKVFormat.NL_X_TWO_NB_BS_NH_HS, # vllm non-MLA flash attention
lmc_ops.EngineKVFormat.NL_X_NB_TWO_BS_NH_HS, # vllm non-MLA flash infer
],
)
def test_single_layer_kernel(num_tokens, token_major, engine_kv_format):
device = "cuda"
num_layers = 32
num_blocks = 1000
block_size = 16
num_heads = 8
head_size = 128
hidden_dim_size = num_heads * head_size
dtype = torch.bfloat16
kv_cache = generate_kv_cache_paged_list_tensors(
num_blocks, device, block_size, dtype, engine_kv_format=engine_kv_format
)
kv_cache_new = generate_kv_cache_paged_list_tensors(
num_blocks, device, block_size, dtype, engine_kv_format=engine_kv_format
)
slot_mapping = random.sample(range(0, num_blocks * block_size), num_tokens)
slot_mapping = torch.tensor(slot_mapping, device=device)
if token_major:
tmp_gpu_buffer = torch.empty(
(num_tokens, 2, hidden_dim_size), dtype=dtype, device=device
)
else:
tmp_gpu_buffer = torch.empty(
(2, num_tokens, hidden_dim_size), dtype=dtype, device=device
)
for layer_id in range(num_layers):
lmc_ops.single_layer_kv_transfer(
tmp_gpu_buffer,
kv_cache[layer_id],
slot_mapping,
lmc_ops.TransferDirection.D2H,
engine_kv_format,
token_major,
)
lmc_ops.single_layer_kv_transfer(
tmp_gpu_buffer,
kv_cache_new[layer_id],
slot_mapping,
lmc_ops.TransferDirection.H2D,
engine_kv_format,
token_major,
)
check_paged_kv_cache_equal(
kv_cache,
kv_cache_new,
slot_mapping,
engine_kv_format=engine_kv_format,
)
@pytest.mark.parametrize("num_tokens", [256, 500, 1024])
@pytest.mark.parametrize(
"engine_kv_format",
[
lmc_ops.EngineKVFormat.NL_X_TWO_NB_NH_BS_HS, # vllm HND flash attention
lmc_ops.EngineKVFormat.NL_X_NB_TWO_NH_BS_HS, # vllm HND flash infer
],
)
def test_multi_layer_kernel_hnd(num_tokens, engine_kv_format):
"""Round-trip test for HND multi-layer kernel: D2H then H2D."""
device = "cuda"
num_blocks = 200
block_size = 16
num_heads = 8
head_size = 128
chunk_size = 256
num_layers = 4
dtype = torch.bfloat16
kv_cache = generate_kv_cache_paged_list_tensors(
num_blocks,
device,
block_size,
dtype,
num_layers=num_layers,
head_size=head_size,
engine_kv_format=engine_kv_format,
)
page_buffer_size = num_blocks * block_size
slot_mapping = random.sample(range(0, page_buffer_size), num_tokens)
slot_mapping = torch.tensor(slot_mapping, device=device)
pinned_cpu_size = 512 * 1024 * 1024 # 512MB
mem_allocator = PinMemoryAllocator(pinned_cpu_size)
# D2H: extract from paged cache into flat memory
kv_cache_pointers = torch.empty(
num_layers, dtype=torch.int64, device="cpu", pin_memory=True
)
for i in range(num_layers):
kv_cache_pointers[i] = kv_cache[i].data_ptr()
memory_obj_list = []
slot_mapping_chunked = torch.split(slot_mapping, chunk_size)
for slot_mapping_temp in slot_mapping_chunked:
mem_obj_shape = torch.Size(
[2, num_layers, len(slot_mapping_temp), num_heads * head_size]
)
memory_obj = mem_allocator.allocate(mem_obj_shape, dtype)
lmc_ops.multi_layer_kv_transfer(
memory_obj.tensor,
kv_cache_pointers,
slot_mapping_temp,
kv_cache[0].device,
page_buffer_size,
lmc_ops.TransferDirection.D2H,
engine_kv_format,
block_size,
head_size=head_size,
)
memory_obj_list.append(memory_obj)
torch.cuda.synchronize()
# H2D: write back into a fresh paged cache
kv_cache_new = generate_kv_cache_paged_list_tensors(
num_blocks,
device,
block_size,
dtype,
num_layers=num_layers,
head_size=head_size,
engine_kv_format=engine_kv_format,
)
kv_cache_pointers_new = torch.empty(
num_layers, dtype=torch.int64, device="cpu", pin_memory=True
)
for i in range(num_layers):
kv_cache_pointers_new[i] = kv_cache_new[i].data_ptr()
for chunk_id, slot_mapping_temp in enumerate(slot_mapping_chunked):
lmc_ops.multi_layer_kv_transfer(
memory_obj_list[chunk_id].tensor,
kv_cache_pointers_new,
slot_mapping_temp,
kv_cache_new[0].device,
page_buffer_size,
lmc_ops.TransferDirection.H2D,
engine_kv_format,
block_size,
head_size=head_size,
)
torch.cuda.synchronize()
check_paged_kv_cache_equal(
kv_cache,
kv_cache_new,
slot_mapping,
engine_kv_format=engine_kv_format,
)
mem_allocator.close()
@pytest.mark.parametrize("num_tokens", [256, 500, 1024])
@pytest.mark.parametrize("token_major", [True, False])
@pytest.mark.parametrize(
"engine_kv_format",
[
lmc_ops.EngineKVFormat.NL_X_TWO_NB_NH_BS_HS, # vllm HND flash attention
lmc_ops.EngineKVFormat.NL_X_NB_TWO_NH_BS_HS, # vllm HND flash infer
],
)
def test_single_layer_kernel_hnd(num_tokens, token_major, engine_kv_format):
"""Round-trip test for HND single-layer kernel: D2H then H2D per layer."""
device = "cuda"
num_layers = 4
num_blocks = 200
block_size = 16
num_heads = 8
head_size = 128
hidden_dim_size = num_heads * head_size
dtype = torch.bfloat16
kv_cache = generate_kv_cache_paged_list_tensors(
num_blocks,
device,
block_size,
dtype,
num_layers=num_layers,
head_size=head_size,
engine_kv_format=engine_kv_format,
)
kv_cache_new = generate_kv_cache_paged_list_tensors(
num_blocks,
device,
block_size,
dtype,
num_layers=num_layers,
head_size=head_size,
engine_kv_format=engine_kv_format,
)
slot_mapping = random.sample(range(0, num_blocks * block_size), num_tokens)
slot_mapping = torch.tensor(slot_mapping, device=device)
if token_major:
tmp_gpu_buffer = torch.empty(
(num_tokens, 2, hidden_dim_size), dtype=dtype, device=device
)
else:
tmp_gpu_buffer = torch.empty(
(2, num_tokens, hidden_dim_size), dtype=dtype, device=device
)
for layer_id in range(num_layers):
lmc_ops.single_layer_kv_transfer(
tmp_gpu_buffer,
kv_cache[layer_id],
slot_mapping,
lmc_ops.TransferDirection.D2H,
engine_kv_format,
token_major,
)
lmc_ops.single_layer_kv_transfer(
tmp_gpu_buffer,
kv_cache_new[layer_id],
slot_mapping,
lmc_ops.TransferDirection.H2D,
engine_kv_format,
token_major,
)
check_paged_kv_cache_equal(
kv_cache,
kv_cache_new,
slot_mapping,
engine_kv_format=engine_kv_format,
)
def test_lmcache_memcpy_async():
# Register 4 memory regions and try to launch copy
chunk_size = 1 << 26
num_chunks = 4
dtype = torch.bfloat16
elements_per_chunk = chunk_size // dtype.itemsize
big_cpu_tensor = torch.rand(
elements_per_chunk * num_chunks, dtype=dtype, device="cpu"
)
big_gpu_tensor = torch.rand(
elements_per_chunk * num_chunks, dtype=dtype, device="cuda"
)
def check_gpu_and_cpu_equal(
gpu_tensor: torch.Tensor,
cpu_tensor: torch.Tensor,
):
cpu_tensor_from_gpu = gpu_tensor.to("cpu")
assert torch.allclose(cpu_tensor_from_gpu, cpu_tensor, atol=1e-5)
rt = torch.cuda.cudart()
# Register the cpu memory
ptr = big_cpu_tensor.data_ptr()
for i in range(num_chunks):
rt.cudaHostRegister(ptr + i * chunk_size, chunk_size, 0)
# Launch default cuda copy
with pytest.raises(RuntimeError):
big_gpu_tensor.copy_(big_cpu_tensor, non_blocking=True)
torch.cuda.synchronize()
# Launc default cuda copy for a small page
big_gpu_tensor[: elements_per_chunk // 2].copy_(
big_cpu_tensor[: elements_per_chunk // 2], non_blocking=True
)
torch.cuda.synchronize()
check_gpu_and_cpu_equal(
big_gpu_tensor[: elements_per_chunk // 2],
big_cpu_tensor[: elements_per_chunk // 2],
)
# Positions to copy
# - 0.25 chunk -- 0.75 chunk
# - 0.75 chunk -- 1.25 chunk
# - 1.75 chunk -- 3.25 chunk
# - whole 4 chunks
starts = [chunk_size // 4, (3 * chunk_size) // 4, (7 * chunk_size) // 4, 0]
ends = [
(3 * chunk_size) // 4,
(5 * chunk_size) // 4,
(13 * chunk_size) // 4,
chunk_size * num_chunks,
]
# H2D copy
for start, end in zip(starts, ends, strict=False):
# should not be equal before copy
with pytest.raises(AssertionError):
check_gpu_and_cpu_equal(
big_gpu_tensor[start // dtype.itemsize : end // dtype.itemsize],
big_cpu_tensor[start // dtype.itemsize : end // dtype.itemsize],
)
lmc_ops.lmcache_memcpy_async(
big_gpu_tensor.data_ptr() + start,
big_cpu_tensor.data_ptr() + start,
end - start,
lmc_ops.TransferDirection.H2D,
start,
chunk_size,
)
torch.cuda.synchronize()
check_gpu_and_cpu_equal(
big_gpu_tensor[start // dtype.itemsize : end // dtype.itemsize],
big_cpu_tensor[start // dtype.itemsize : end // dtype.itemsize],
)
# Reset the data in gpu
big_gpu_tensor = torch.rand(
elements_per_chunk * num_chunks, dtype=dtype, device="cuda"
)
# D2H copy
for start, end in zip(starts, ends, strict=False):
# copy from gpu to cpu
with pytest.raises(AssertionError):
check_gpu_and_cpu_equal(
big_gpu_tensor[start // dtype.itemsize : end // dtype.itemsize],
big_cpu_tensor[start // dtype.itemsize : end // dtype.itemsize],
)
lmc_ops.lmcache_memcpy_async(
big_cpu_tensor.data_ptr() + start,
big_gpu_tensor.data_ptr() + start,
end - start,
lmc_ops.TransferDirection.D2H,
start,
chunk_size,
)
torch.cuda.synchronize()
check_gpu_and_cpu_equal(
big_gpu_tensor[start // dtype.itemsize : end // dtype.itemsize],
big_cpu_tensor[start // dtype.itemsize : end // dtype.itemsize],
)
# Unregister the cpu memory
ptr = big_cpu_tensor.data_ptr()
for i in range(num_chunks):
rt.cudaHostUnregister(ptr + i * chunk_size)
def test_lmcache_memcpy_async_int8_hidden132():
"""Test lmcache_memcpy_async with int8 dtype and hidden_size=132.
hidden_size=132 means each token occupies 132 bytes, which is NOT a
multiple of 4 (uint32 size). This tests that the kernel correctly handles
non-uint32-aligned data sizes when splitting copies across registered
memory chunk boundaries.
The KV buffer shape is (2, num_layers, num_tokens, hidden_size), so the
total bytes = 2 * num_layers * num_tokens * 132. We pick values such that
the copy spans across at least one PIN_CHUNK_SIZE (64 MB) boundary.
"""
# Use PIN_CHUNK_SIZE = 64 MB as the registration granularity
chunk_size = 1 << 26 # 64 MB
num_chunks = 2
dtype = torch.int8
hidden_size = 132 # NOT a multiple of 4 — tests uint32 alignment handling
# Total bytes must span at least one chunk boundary.
# We allocate num_chunks * chunk_size bytes worth of int8 elements.
total_bytes = chunk_size * num_chunks
total_elements = total_bytes # int8: 1 byte per element
cpu_tensor = torch.randint(-128, 127, (total_elements,), dtype=dtype, device="cpu")
gpu_tensor = torch.zeros(total_elements, dtype=dtype, device="cuda")
rt = torch.cuda.cudart()
# Register the cpu memory in PIN_CHUNK_SIZE chunks
ptr = cpu_tensor.data_ptr()
for i in range(num_chunks):
rt.cudaHostRegister(ptr + i * chunk_size, chunk_size, 0)
def check_equal(gpu_t: torch.Tensor, cpu_t: torch.Tensor):
assert torch.equal(gpu_t.cpu(), cpu_t), "GPU and CPU tensors are not equal"
# Test cases: copy ranges that cross chunk boundaries with non-4-byte-aligned
# sizes, simulating real KV cache transfers where hidden_size=132 (int8).
#
# Each "KV row" is hidden_size=132 bytes. We pick copy sizes that are
# multiples of 132 but NOT multiples of 4, so the copy boundary falls on
# a non-uint32-aligned offset within a registered chunk.
#
# num_tokens=3: 3 * 132 = 396 bytes (396 % 4 == 0, but 132 % 4 != 0)
# num_tokens=1: 1 * 132 = 132 bytes (132 % 4 == 0)
# num_tokens=5: 5 * 132 = 660 bytes (660 % 4 == 0)
# Use an offset that puts the copy right across the 64MB boundary.
boundary = chunk_size # 64 MB boundary in bytes
# Place the copy so it straddles the chunk boundary:
# start = boundary - 2 * hidden_size, length = 4 * hidden_size
# This means 2 * hidden_size bytes before the boundary and 2 * hidden_size
# bytes after — each segment is 264 bytes, not a multiple of 4*hidden_size.
test_cases = [
# (start_bytes, num_hidden_rows)
(boundary - 2 * hidden_size, 4), # straddles boundary, 528 bytes
(boundary - hidden_size, 3), # straddles boundary, 396 bytes
(boundary - 3 * hidden_size, 6), # straddles boundary, 792 bytes
(0, 1), # single row at start, 132 bytes
(total_bytes - hidden_size, 1), # single row at end, 132 bytes
]
for start_bytes, num_rows in test_cases:
nbytes = num_rows * hidden_size
end_bytes = start_bytes + nbytes
assert end_bytes <= total_bytes, (
f"Test case out of bounds: start={start_bytes}, nbytes={nbytes}"
)
# Reset gpu_tensor slice to zeros so we can detect the copy
gpu_tensor[start_bytes:end_bytes].zero_()
# H2D copy
lmc_ops.lmcache_memcpy_async(
gpu_tensor.data_ptr() + start_bytes,
cpu_tensor.data_ptr() + start_bytes,
nbytes,
lmc_ops.TransferDirection.H2D,
start_bytes,
chunk_size,
)
torch.cuda.synchronize()
check_equal(
gpu_tensor[start_bytes:end_bytes],
cpu_tensor[start_bytes:end_bytes],
)
# D2H copy: write known values to GPU, copy back to CPU, verify
gpu_src = torch.randint(-128, 127, (total_elements,), dtype=dtype, device="cuda")
cpu_dst = torch.zeros(total_elements, dtype=dtype, device="cpu")
# Register cpu_dst for D2H
ptr_dst = cpu_dst.data_ptr()
for i in range(num_chunks):
rt.cudaHostRegister(ptr_dst + i * chunk_size, chunk_size, 0)
for start_bytes, num_rows in test_cases:
nbytes = num_rows * hidden_size
end_bytes = start_bytes + nbytes
cpu_dst[start_bytes:end_bytes].zero_()
lmc_ops.lmcache_memcpy_async(
cpu_dst.data_ptr() + start_bytes,
gpu_src.data_ptr() + start_bytes,
nbytes,
lmc_ops.TransferDirection.D2H,
start_bytes,
chunk_size,
)
torch.cuda.synchronize()
check_equal(
gpu_src[start_bytes:end_bytes],
cpu_dst[start_bytes:end_bytes],
)
# Unregister memory
ptr = cpu_tensor.data_ptr()
for i in range(num_chunks):
rt.cudaHostUnregister(ptr + i * chunk_size)
ptr_dst = cpu_dst.data_ptr()
for i in range(num_chunks):
rt.cudaHostUnregister(ptr_dst + i * chunk_size)