[HiCache] TMA-staged host<->device KV transfer kernel (sm_90+) (#40278)
This commit is contained in:
@@ -24,8 +24,11 @@ from sgl_kernel import transfer_kv_all_layer, transfer_kv_per_layer
|
||||
from sglang.kernels.jit.benchmark import marker
|
||||
from sglang.kernels.jit.benchmark.utils import get_benchmark_range
|
||||
from sglang.kernels.ops.kvcache.hicache import (
|
||||
transfer_hicache_all_layer,
|
||||
transfer_hicache_one_layer,
|
||||
DEFAULT_BLOCK_QUOTA,
|
||||
TMA_BLOCK_QUOTA,
|
||||
_default_unroll,
|
||||
_jit_hicache_module,
|
||||
_jit_hicache_tma_module,
|
||||
)
|
||||
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
|
||||
|
||||
@@ -108,15 +111,19 @@ def sglang_jit_transfer_one(
|
||||
indices_src: torch.Tensor,
|
||||
element_dim: int,
|
||||
) -> None:
|
||||
"""SGL JIT Kernel for single layer transfer."""
|
||||
transfer_hicache_one_layer(
|
||||
k_cache_dst,
|
||||
v_cache_dst,
|
||||
"""SGL JIT register kernel for single layer transfer (bypasses TMA routing)."""
|
||||
element_size = element_dim * k_cache_dst.element_size()
|
||||
_jit_hicache_module(
|
||||
element_size=element_size,
|
||||
unroll=_default_unroll(element_size),
|
||||
block_quota=DEFAULT_BLOCK_QUOTA,
|
||||
).launch_one(
|
||||
k_cache_dst.view(-1, element_dim),
|
||||
v_cache_dst.view(-1, element_dim),
|
||||
indices_dst,
|
||||
k_cache_src,
|
||||
v_cache_src,
|
||||
k_cache_src.view(-1, element_dim),
|
||||
v_cache_src.view(-1, element_dim),
|
||||
indices_src,
|
||||
element_dim=element_dim,
|
||||
)
|
||||
|
||||
|
||||
@@ -153,17 +160,58 @@ def sglang_jit_transfer_all(
|
||||
stride_bytes: int,
|
||||
element_size: int,
|
||||
) -> None:
|
||||
"""SGL JIT Kernel for all layer transfer."""
|
||||
transfer_hicache_all_layer(
|
||||
"""SGL JIT register kernel for all layer transfer (bypasses TMA routing)."""
|
||||
_jit_hicache_module(
|
||||
element_size=element_size,
|
||||
unroll=_default_unroll(element_size),
|
||||
block_quota=DEFAULT_BLOCK_QUOTA,
|
||||
).launch_all(
|
||||
k_ptrs_dst,
|
||||
v_ptrs_dst,
|
||||
indices_dst,
|
||||
k_ptrs_src,
|
||||
v_ptrs_src,
|
||||
indices_src,
|
||||
kv_cache_src_stride_bytes=stride_bytes,
|
||||
kv_cache_dst_stride_bytes=stride_bytes,
|
||||
element_size=element_size,
|
||||
stride_bytes,
|
||||
stride_bytes,
|
||||
)
|
||||
|
||||
|
||||
def sglang_tma_transfer_one(
|
||||
k_cache_dst: torch.Tensor,
|
||||
v_cache_dst: torch.Tensor,
|
||||
indices_dst: torch.Tensor,
|
||||
k_cache_src: torch.Tensor,
|
||||
v_cache_src: torch.Tensor,
|
||||
indices_src: torch.Tensor,
|
||||
) -> None:
|
||||
"""SGL TMA staging kernel for single layer transfer."""
|
||||
_jit_hicache_tma_module(block_quota=TMA_BLOCK_QUOTA).launch_one(
|
||||
k_cache_dst, v_cache_dst, indices_dst, k_cache_src, v_cache_src, indices_src
|
||||
)
|
||||
|
||||
|
||||
def sglang_tma_transfer_all(
|
||||
k_ptrs_dst: torch.Tensor,
|
||||
v_ptrs_dst: torch.Tensor,
|
||||
indices_dst: torch.Tensor,
|
||||
k_ptrs_src: torch.Tensor,
|
||||
v_ptrs_src: torch.Tensor,
|
||||
indices_src: torch.Tensor,
|
||||
stride_bytes: int,
|
||||
element_size: int,
|
||||
) -> None:
|
||||
"""SGL TMA staging kernel for all layer transfer."""
|
||||
_jit_hicache_tma_module(block_quota=TMA_BLOCK_QUOTA).launch_all(
|
||||
k_ptrs_dst,
|
||||
v_ptrs_dst,
|
||||
indices_dst,
|
||||
k_ptrs_src,
|
||||
v_ptrs_src,
|
||||
indices_src,
|
||||
stride_bytes,
|
||||
stride_bytes,
|
||||
element_size,
|
||||
)
|
||||
|
||||
|
||||
@@ -191,6 +239,13 @@ ELEMENT_SIZE_RANGE = get_benchmark_range(
|
||||
LINE_VALS = ["aot", "jit", "torch"]
|
||||
if DISABLE_TORCH:
|
||||
LINE_VALS.remove("torch")
|
||||
# The TMA staging kernel needs sm_90+ (cp.async.bulk); skip the line elsewhere.
|
||||
if (
|
||||
torch.cuda.is_available()
|
||||
and torch.version.hip is None
|
||||
and torch.cuda.get_device_capability()[0] >= 9
|
||||
):
|
||||
LINE_VALS.insert(2, "tma")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
@@ -246,6 +301,17 @@ def benchmark_one_layer_h2d(element_size: int, batch_size: int, provider: str):
|
||||
)
|
||||
for i in range(NUM_LAYERS)
|
||||
],
|
||||
"tma": lambda: [
|
||||
sglang_tma_transfer_one(
|
||||
k_cache_dst[i],
|
||||
v_cache_dst[i],
|
||||
indices_dst_gpu,
|
||||
k_cache_src[i],
|
||||
v_cache_src[i],
|
||||
indices_src_gpu,
|
||||
)
|
||||
for i in range(NUM_LAYERS)
|
||||
],
|
||||
"torch": lambda: [
|
||||
pytorch_transfer(
|
||||
k_cache_dst[i],
|
||||
@@ -329,6 +395,16 @@ def benchmark_all_layer_d2h(element_size: int, batch_size: int, provider: str):
|
||||
element_bytes,
|
||||
element_bytes,
|
||||
),
|
||||
"tma": lambda: sglang_tma_transfer_all(
|
||||
k_ptrs_dst,
|
||||
v_ptrs_dst,
|
||||
indices_dst_gpu,
|
||||
k_ptrs_src,
|
||||
v_ptrs_src,
|
||||
indices_src_gpu,
|
||||
element_bytes,
|
||||
element_bytes,
|
||||
),
|
||||
"torch": lambda: [
|
||||
pytorch_transfer(
|
||||
k_caches_dst[i],
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.kvcache.hicache import _jit_hicache_tma_module
|
||||
from sglang.test.ci.ci_register import register_cuda_ci
|
||||
|
||||
register_cuda_ci(est_time=60, stage="base-b-kernel-unit", runner_config="1-gpu-large")
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not torch.cuda.is_available()
|
||||
or torch.version.hip is not None
|
||||
or torch.cuda.get_device_capability()[0] < 9,
|
||||
reason="HiCache TMA kernel requires SM90+",
|
||||
)
|
||||
|
||||
POOL_TOKENS = 8192
|
||||
NUM_LAYERS = 3
|
||||
ROW_DIM = 256 # 512-byte bf16 rows: below the register kernel's 128 B unit width x 4
|
||||
|
||||
|
||||
def _token_indices(num_tokens: int, page_size: int, dtype: torch.dtype, seed: int):
|
||||
gen = torch.Generator().manual_seed(seed)
|
||||
pages = torch.randperm(POOL_TOKENS // page_size, generator=gen)[
|
||||
: num_tokens // page_size
|
||||
]
|
||||
idx = (pages[:, None] * page_size + torch.arange(page_size)).reshape(-1)
|
||||
return idx.to(device="cuda", dtype=dtype)
|
||||
|
||||
|
||||
def _fill(t: torch.Tensor, seed: int) -> None:
|
||||
t.view(torch.int16).copy_(
|
||||
torch.randint(
|
||||
0,
|
||||
30000,
|
||||
t.shape,
|
||||
dtype=torch.int16,
|
||||
generator=torch.Generator().manual_seed(seed),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _host_view(layout: str, layer: int):
|
||||
if layout == "layer_first":
|
||||
return torch.empty(POOL_TOKENS, ROW_DIM, dtype=torch.bfloat16, pin_memory=True)
|
||||
# page_first: [tokens, layers, dim]; a per-layer view has strided rows
|
||||
return torch.empty(
|
||||
POOL_TOKENS, NUM_LAYERS, ROW_DIM, dtype=torch.bfloat16, pin_memory=True
|
||||
)[:, layer]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("host_layout", ["layer_first", "page_first"])
|
||||
@pytest.mark.parametrize("index_dtype", [torch.int64, torch.int32])
|
||||
@pytest.mark.parametrize("page_size", [128, 1])
|
||||
def test_one_layer_roundtrip(
|
||||
host_layout: str, index_dtype: torch.dtype, page_size: int
|
||||
) -> None:
|
||||
"""H2D then D2H of one layer; page runs take the single-op paths (bulk copy,
|
||||
tensor-map box, bulk store), scattered rows take the per-row paths, and the
|
||||
odd token count leaves a partial tail chunk."""
|
||||
module = _jit_hicache_tma_module(block_quota=2)
|
||||
num_tokens = 2048 + (96 if page_size == 1 else 0)
|
||||
k_host, v_host = _host_view(host_layout, 1), _host_view(host_layout, 2)
|
||||
k_dev = torch.zeros(POOL_TOKENS, ROW_DIM, dtype=torch.bfloat16, device="cuda")
|
||||
v_dev = torch.zeros_like(k_dev)
|
||||
_fill(k_host, 1)
|
||||
_fill(v_host, 2)
|
||||
host_idx = _token_indices(num_tokens, page_size, index_dtype, seed=3)
|
||||
dev_idx = _token_indices(num_tokens, page_size, index_dtype, seed=4)
|
||||
|
||||
module.launch_one(k_dev, v_dev, dev_idx, k_host, v_host, host_idx)
|
||||
torch.cuda.synchronize()
|
||||
assert torch.equal(k_dev[dev_idx.long()].cpu(), k_host[host_idx.cpu().long()])
|
||||
assert torch.equal(v_dev[dev_idx.long()].cpu(), v_host[host_idx.cpu().long()])
|
||||
untouched = torch.ones(POOL_TOKENS, dtype=torch.bool, device="cuda")
|
||||
untouched[dev_idx.long()] = False
|
||||
assert not k_dev[untouched].any() and not v_dev[untouched].any()
|
||||
|
||||
_fill(k_dev, 5)
|
||||
_fill(v_dev, 6)
|
||||
k_host.zero_()
|
||||
v_host.zero_()
|
||||
module.launch_one(k_host, v_host, host_idx, k_dev, v_dev, dev_idx)
|
||||
torch.cuda.synchronize()
|
||||
assert torch.equal(k_host[host_idx.cpu().long()], k_dev[dev_idx.long()].cpu())
|
||||
assert torch.equal(v_host[host_idx.cpu().long()], v_dev[dev_idx.long()].cpu())
|
||||
|
||||
|
||||
def _ptr_table(tensors) -> torch.Tensor:
|
||||
return torch.tensor(
|
||||
[t.data_ptr() for t in tensors], dtype=torch.uint64, device="cuda"
|
||||
)
|
||||
|
||||
|
||||
def test_all_layer_tables_lf_to_pf() -> None:
|
||||
"""All-layer D2H through per-layer pointer tables into a page-first host pool
|
||||
(strided destination rows), the write-back shape."""
|
||||
module = _jit_hicache_tma_module(block_quota=2)
|
||||
k_dev = [
|
||||
torch.empty(POOL_TOKENS, ROW_DIM, dtype=torch.bfloat16, device="cuda")
|
||||
for _ in range(NUM_LAYERS)
|
||||
]
|
||||
v_dev = [torch.empty_like(k_dev[0]) for _ in range(NUM_LAYERS)]
|
||||
for i, t in enumerate(k_dev + v_dev):
|
||||
_fill(t, 10 + i)
|
||||
k_host = torch.zeros(
|
||||
POOL_TOKENS, NUM_LAYERS, ROW_DIM, dtype=torch.bfloat16, pin_memory=True
|
||||
)
|
||||
v_host = torch.zeros_like(k_host).pin_memory()
|
||||
host_idx = _token_indices(2048, 128, torch.int64, seed=7)
|
||||
dev_idx = _token_indices(2048, 128, torch.int64, seed=8)
|
||||
row_bytes = ROW_DIM * 2
|
||||
|
||||
module.launch_all(
|
||||
_ptr_table([k_host[:, l] for l in range(NUM_LAYERS)]),
|
||||
_ptr_table([v_host[:, l] for l in range(NUM_LAYERS)]),
|
||||
host_idx,
|
||||
_ptr_table(k_dev),
|
||||
_ptr_table(v_dev),
|
||||
dev_idx,
|
||||
row_bytes,
|
||||
NUM_LAYERS * row_bytes,
|
||||
row_bytes,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
for l in range(NUM_LAYERS):
|
||||
assert torch.equal(k_host[host_idx.cpu(), l], k_dev[l][dev_idx].cpu())
|
||||
assert torch.equal(v_host[host_idx.cpu(), l], v_dev[l][dev_idx].cpu())
|
||||
untouched = torch.ones(POOL_TOKENS, dtype=torch.bool)
|
||||
untouched[host_idx.cpu()] = False
|
||||
assert not k_host[untouched].any() and not v_host[untouched].any()
|
||||
|
||||
|
||||
def test_mla_single_buffer() -> None:
|
||||
"""MLA rows (576 x bf16 = 1152 B, not a multiple of 128 B) through the
|
||||
single-buffer entry points, one layer and all layers."""
|
||||
module = _jit_hicache_tma_module(block_quota=2)
|
||||
dim = 576
|
||||
dev = [
|
||||
torch.empty(POOL_TOKENS, dim, dtype=torch.bfloat16, device="cuda")
|
||||
for _ in range(NUM_LAYERS)
|
||||
]
|
||||
for i, t in enumerate(dev):
|
||||
_fill(t, 20 + i)
|
||||
host = torch.zeros(
|
||||
POOL_TOKENS, NUM_LAYERS, dim, dtype=torch.bfloat16, pin_memory=True
|
||||
)
|
||||
host_idx = _token_indices(1024, 128, torch.int64, seed=9)
|
||||
dev_idx = _token_indices(1024, 128, torch.int64, seed=10)
|
||||
row_bytes = dim * 2
|
||||
|
||||
module.launch_all_mla(
|
||||
_ptr_table([host[:, l] for l in range(NUM_LAYERS)]),
|
||||
host_idx,
|
||||
_ptr_table(dev),
|
||||
dev_idx,
|
||||
row_bytes,
|
||||
NUM_LAYERS * row_bytes,
|
||||
row_bytes,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
for l in range(NUM_LAYERS):
|
||||
assert torch.equal(host[host_idx.cpu(), l], dev[l][dev_idx].cpu())
|
||||
|
||||
dev[0].zero_()
|
||||
module.launch_one_mla(dev[0], dev_idx, host[:, 0], host_idx)
|
||||
torch.cuda.synchronize()
|
||||
assert torch.equal(dev[0][dev_idx].cpu(), host[host_idx.cpu(), 0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v", "-s"]))
|
||||
Reference in New Issue
Block a user