[Kernel] Skip KV writes to reserved padding slots (#32477)

Co-authored-by: Andrew Gu <andrew@thinkingmachines.ai>
This commit is contained in:
Leon Gao
2026-07-29 09:58:18 +08:00
committed by GitHub
co-authored by Andrew Gu
parent d86492fea0
commit ee678910f7
3 changed files with 68 additions and 7 deletions
@@ -25,6 +25,7 @@ struct StoreKVCacheParams {
int64_t stride_indices; int64_t stride_indices;
uint32_t batch_size; uint32_t batch_size;
int64_t size_limit; int64_t size_limit;
int64_t reserved_skip_index;
}; };
constexpr uint32_t kNumWarps = 4; constexpr uint32_t kNumWarps = 4;
@@ -97,7 +98,7 @@ __global__ void store_kvcache(const __grid_constant__ StoreKVCacheParams params)
const auto& [ const auto& [
k_input, v_input, k_cache, v_cache, indices, // ptr k_input, v_input, k_cache, v_cache, indices, // ptr
stride_k, stride_v, stride_cache, stride_indices, batch_size, // size stride_k, stride_v, stride_cache, stride_indices, batch_size, // size
size_limit // bound size_limit, reserved_skip_index // bounds and reserved sink
] = params; ] = params;
if (item_id >= batch_size) return; if (item_id >= batch_size) return;
@@ -113,7 +114,9 @@ __global__ void store_kvcache(const __grid_constant__ StoreKVCacheParams params)
const auto k_dst = pointer::offset(k_cache, index * stride_cache, split_id * kSplitSize); const auto k_dst = pointer::offset(k_cache, index * stride_cache, split_id * kSplitSize);
const auto v_dst = pointer::offset(v_cache, index * stride_cache, split_id * kSplitSize); const auto v_dst = pointer::offset(v_cache, index * stride_cache, split_id * kSplitSize);
copy_kv_warp<kSplitSize>(k_src, v_src, k_dst, v_dst); if (index != reserved_skip_index) {
copy_kv_warp<kSplitSize>(k_src, v_src, k_dst, v_dst);
}
PDLTriggerSecondary<kUsePDL>(); PDLTriggerSecondary<kUsePDL>();
} }
@@ -145,7 +148,8 @@ struct StoreKVCacheKernel {
const tvm::ffi::TensorView v_cache, const tvm::ffi::TensorView v_cache,
const tvm::ffi::TensorView indices, const tvm::ffi::TensorView indices,
const int num_split, const int num_split,
const int64_t size_limit) { const int64_t size_limit,
const int64_t reserved_skip_index) {
using namespace host; using namespace host;
auto B = SymbolicSize{"batch_size"}; auto B = SymbolicSize{"batch_size"};
auto D = SymbolicSize{"element_size"}; auto D = SymbolicSize{"element_size"};
@@ -196,6 +200,7 @@ struct StoreKVCacheKernel {
.stride_indices = I.unwrap(), .stride_indices = I.unwrap(),
.batch_size = static_cast<uint32_t>(B.unwrap()), .batch_size = static_cast<uint32_t>(B.unwrap()),
.size_limit = size_limit, .size_limit = size_limit,
.reserved_skip_index = reserved_skip_index,
}; };
// select kernel and update num_split if needed // select kernel and update num_split if needed
const auto use_int32 = indice_dtype.is_type<int32_t>(); const auto use_int32 = indice_dtype.is_type<int32_t>();
@@ -58,6 +58,7 @@ def store_cache(
row_bytes: int = 0, row_bytes: int = 0,
num_split: int = 0, # can be tuned for performance num_split: int = 0, # can be tuned for performance
size_limit: int = 0, size_limit: int = 0,
reserved_skip_index: int = 0,
) -> None: ) -> None:
"""Store key and value tensors into KV cache at specified indices. """Store key and value tensors into KV cache at specified indices.
@@ -71,6 +72,9 @@ def store_cache(
reserved padding slot); an index outside [0, size_limit) fails fast reserved padding slot); an index outside [0, size_limit) fails fast
(device assert) instead of an illegal memory access. Defaults to the (device assert) instead of an illegal memory access. Defaults to the
cache row count when 0. cache row count when 0.
reserved_skip_index (int): If nonnegative, writes targeting this index
are skipped. Defaults to the reserved CUDA-graph padding slot 0;
pass -1 to disable skipping.
""" """
row_bytes = row_bytes or k.shape[-1] * k.element_size() row_bytes = row_bytes or k.shape[-1] * k.element_size()
module = _jit_kvcache_module(row_bytes) module = _jit_kvcache_module(row_bytes)
@@ -91,4 +95,5 @@ def store_cache(
indices, indices,
num_split, num_split,
size_limit, size_limit,
reserved_skip_index,
) )
@@ -33,7 +33,7 @@ def test_store_cache(batch_size: int, element_dim: int) -> None:
v = torch.randn((batch_size, element_dim), dtype=DTYPE, device=DEVICE) v = torch.randn((batch_size, element_dim), dtype=DTYPE, device=DEVICE)
k_cache = torch.randn((CACHE_SIZE, element_dim), dtype=DTYPE, device=DEVICE) k_cache = torch.randn((CACHE_SIZE, element_dim), dtype=DTYPE, device=DEVICE)
v_cache = torch.randn((CACHE_SIZE, element_dim), dtype=DTYPE, device=DEVICE) v_cache = torch.randn((CACHE_SIZE, element_dim), dtype=DTYPE, device=DEVICE)
indices = torch.randperm(CACHE_SIZE, device=DEVICE)[:batch_size] indices = torch.randperm(CACHE_SIZE - 1, device=DEVICE)[:batch_size] + 1
# AOT store cache # AOT store cache
store_cache(k, v, k_cache, v_cache, indices) store_cache(k, v, k_cache, v_cache, indices)
@@ -60,7 +60,7 @@ def test_store_cache_dtypes(
v = torch.randn((batch_size, element_dim), dtype=dtype, device=DEVICE) v = torch.randn((batch_size, element_dim), dtype=dtype, device=DEVICE)
k_cache = torch.randn((SMALL_CACHE, element_dim), dtype=dtype, device=DEVICE) k_cache = torch.randn((SMALL_CACHE, element_dim), dtype=dtype, device=DEVICE)
v_cache = torch.randn((SMALL_CACHE, element_dim), dtype=dtype, device=DEVICE) v_cache = torch.randn((SMALL_CACHE, element_dim), dtype=dtype, device=DEVICE)
indices = torch.randperm(SMALL_CACHE, device=DEVICE)[:batch_size] indices = torch.randperm(SMALL_CACHE - 1, device=DEVICE)[:batch_size] + 1
store_cache(k, v, k_cache, v_cache, indices) store_cache(k, v, k_cache, v_cache, indices)
@@ -78,7 +78,9 @@ def test_store_cache_int32_indices(batch_size: int, element_dim: int) -> None:
k_cache = torch.randn((SMALL_CACHE, element_dim), dtype=DTYPE, device=DEVICE) k_cache = torch.randn((SMALL_CACHE, element_dim), dtype=DTYPE, device=DEVICE)
v_cache = torch.randn((SMALL_CACHE, element_dim), dtype=DTYPE, device=DEVICE) v_cache = torch.randn((SMALL_CACHE, element_dim), dtype=DTYPE, device=DEVICE)
# int32 indices exercise a different CUDA template instantiation than default int64 # int32 indices exercise a different CUDA template instantiation than default int64
indices = torch.randperm(SMALL_CACHE, device=DEVICE)[:batch_size].to(torch.int32) indices = (torch.randperm(SMALL_CACHE - 1, device=DEVICE)[:batch_size] + 1).to(
torch.int32
)
store_cache(k, v, k_cache, v_cache, indices) store_cache(k, v, k_cache, v_cache, indices)
@@ -86,6 +88,55 @@ def test_store_cache_int32_indices(batch_size: int, element_dim: int) -> None:
assert torch.all(v_cache[indices.long()] == v) assert torch.all(v_cache[indices.long()] == v)
@pytest.mark.parametrize("index_dtype", [torch.int32, torch.int64])
@pytest.mark.parametrize("num_split", [1, 2, 4])
def test_store_cache_reserved_skip_index(
index_dtype: torch.dtype, num_split: int
) -> None:
element_dim = 1024
k = torch.randn((4, element_dim), dtype=DTYPE, device=DEVICE)
v = torch.randn((4, element_dim), dtype=DTYPE, device=DEVICE)
# Model kernels may leave CUDA-graph padding rows undefined. Reproduce the
# dangerous case directly instead of requiring a full model checkpoint.
k[[0, 2]] = torch.nan
v[[0, 2]] = torch.nan
k_cache = torch.randn((SMALL_CACHE, element_dim), dtype=DTYPE, device=DEVICE)
v_cache = torch.randn((SMALL_CACHE, element_dim), dtype=DTYPE, device=DEVICE)
reserved_k_before = k_cache[0].clone()
reserved_v_before = v_cache[0].clone()
indices = torch.tensor([0, 7, 0, 9], dtype=index_dtype, device=DEVICE)
store_cache(
k,
v,
k_cache,
v_cache,
indices,
num_split=num_split,
)
torch.testing.assert_close(k_cache[0], reserved_k_before, rtol=0.0, atol=0.0)
torch.testing.assert_close(v_cache[0], reserved_v_before, rtol=0.0, atol=0.0)
torch.testing.assert_close(k_cache[indices[1].long()], k[1], rtol=0.0, atol=0.0)
torch.testing.assert_close(v_cache[indices[1].long()], v[1], rtol=0.0, atol=0.0)
torch.testing.assert_close(k_cache[indices[3].long()], k[3], rtol=0.0, atol=0.0)
torch.testing.assert_close(v_cache[indices[3].long()], v[3], rtol=0.0, atol=0.0)
def test_store_cache_zero_index_can_be_written_when_skip_disabled() -> None:
element_dim = 64
k = torch.randn((1, element_dim), dtype=DTYPE, device=DEVICE)
v = torch.randn((1, element_dim), dtype=DTYPE, device=DEVICE)
k_cache = torch.randn((SMALL_CACHE, element_dim), dtype=DTYPE, device=DEVICE)
v_cache = torch.randn((SMALL_CACHE, element_dim), dtype=DTYPE, device=DEVICE)
indices = torch.zeros(1, dtype=torch.int64, device=DEVICE)
store_cache(k, v, k_cache, v_cache, indices, reserved_skip_index=-1)
torch.testing.assert_close(k_cache[0], k[0], rtol=0.0, atol=0.0)
torch.testing.assert_close(v_cache[0], v[0], rtol=0.0, atol=0.0)
def _valid_num_splits(element_dim: int, dtype: torch.dtype) -> list: def _valid_num_splits(element_dim: int, dtype: torch.dtype) -> list:
"""Return the list of valid num_split values for a given element_dim/dtype.""" """Return the list of valid num_split values for a given element_dim/dtype."""
row_bytes = element_dim * dtype.itemsize row_bytes = element_dim * dtype.itemsize
@@ -114,7 +165,7 @@ def test_store_cache_num_split(
v = torch.randn((batch_size, element_dim), dtype=dtype, device=DEVICE) v = torch.randn((batch_size, element_dim), dtype=dtype, device=DEVICE)
k_cache = torch.randn((SMALL_CACHE, element_dim), dtype=dtype, device=DEVICE) k_cache = torch.randn((SMALL_CACHE, element_dim), dtype=dtype, device=DEVICE)
v_cache = torch.randn((SMALL_CACHE, element_dim), dtype=dtype, device=DEVICE) v_cache = torch.randn((SMALL_CACHE, element_dim), dtype=dtype, device=DEVICE)
indices = torch.randperm(SMALL_CACHE, device=DEVICE)[:batch_size] indices = torch.randperm(SMALL_CACHE - 1, device=DEVICE)[:batch_size] + 1
# Verify each num_split kernel path (1, 2, 4) produces correct results # Verify each num_split kernel path (1, 2, 4) produces correct results
store_cache(k, v, k_cache, v_cache, indices, num_split=num_split) store_cache(k, v, k_cache, v_cache, indices, num_split=num_split)