HiSparse: shared-index (IndexShare) plan-then-IO swap-in prefetch (#34329)
Co-authored-by: Tingwei Huang <huangtingwei9988@gmail.com>
This commit is contained in:
co-authored by
Tingwei Huang
parent
396722e490
commit
5469faec45
@@ -174,6 +174,55 @@ __device__ __forceinline__ int popc_mask(BallotMask mask) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// Copy one missed item host->device with one warp. Shared by the fused swap-in
|
||||
// kernel and copy_cache_planned_kernel so the layout dispatch cannot drift.
|
||||
template <bool IsMLA, bool IsDsv4Layout>
|
||||
__device__ __forceinline__ void copy_miss_item(
|
||||
int32_t lane_id,
|
||||
const void* __restrict__ host_cache_k,
|
||||
const void* __restrict__ host_cache_v,
|
||||
void* __restrict__ device_buffer_k,
|
||||
void* __restrict__ device_buffer_v,
|
||||
int64_t src_loc,
|
||||
int64_t dst_loc,
|
||||
int64_t item_size_bytes) {
|
||||
static_assert(!IsDsv4Layout || IsMLA, "DSv4 page-padded layout is K-only (MLA).");
|
||||
if constexpr (IsDsv4Layout) {
|
||||
#ifdef USE_ROCM
|
||||
// ROCm path: host cache and device buffer both use the page-padded C4
|
||||
// layout (same as the write path and the CUDA branch). We can't reuse
|
||||
// device::hisparse::transfer_item here because its warp logic is hardcoded
|
||||
// to a 32-lane warp; on wavefront64 we use transfer_dsv4_item_warp, which
|
||||
// moves the value and the scale in one warp-width-agnostic copy.
|
||||
using namespace device::hisparse;
|
||||
const auto [dst_value_ptr, dst_scale_ptr] = get_pointer_paged(device_buffer_k, static_cast<int32_t>(dst_loc));
|
||||
const auto [src_value_ptr, src_scale_ptr] =
|
||||
get_pointer_paged(const_cast<void*>(host_cache_k), static_cast<int32_t>(src_loc));
|
||||
transfer_dsv4_item_warp(lane_id, src_value_ptr, src_scale_ptr, dst_value_ptr, dst_scale_ptr);
|
||||
#else
|
||||
// CUDA path: page-padded device layout + page-padded host layout, K-only.
|
||||
// The host cache is pinned DRAM but uses the same row layout as the GPU C4
|
||||
// cache, so use the page-padded address calculation for both ends.
|
||||
device::hisparse::transfer_item(
|
||||
/*dst_cache=*/device_buffer_k,
|
||||
/*src_cache=*/const_cast<void*>(host_cache_k),
|
||||
/*dst_index=*/static_cast<int32_t>(dst_loc),
|
||||
/*src_index=*/static_cast<int32_t>(src_loc));
|
||||
#endif
|
||||
} else {
|
||||
// Generic path: device + host both linear, stride = item_size_bytes.
|
||||
const auto src_k = static_cast<const char*>(host_cache_k) + src_loc * item_size_bytes;
|
||||
auto dst_k = static_cast<char*>(device_buffer_k) + dst_loc * item_size_bytes;
|
||||
transfer_item_warp(lane_id, src_k, dst_k, item_size_bytes);
|
||||
|
||||
if constexpr (!IsMLA) {
|
||||
const auto src_v = static_cast<const char*>(host_cache_v) + src_loc * item_size_bytes;
|
||||
auto dst_v = static_cast<char*>(device_buffer_v) + dst_loc * item_size_bytes;
|
||||
transfer_item_warp(lane_id, src_v, dst_v, item_size_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <int BLOCK_SIZE>
|
||||
__global__ __launch_bounds__(BLOCK_SIZE, 1) void transfer_cache_dsv4_mla_kernel(
|
||||
void** src_caches,
|
||||
@@ -268,12 +317,20 @@ struct SmemLayout {
|
||||
// IsDsv4Layout selects the miss-copy addressing:
|
||||
// false -> generic byte-stride: device + host both linear, stride = item_size_bytes
|
||||
// true -> DSv4 page-padded device + page-padded host (kvcacheio.cuh constants)
|
||||
//
|
||||
// RecordMissPlan records this step's miss plan (miss_src/dst = host/device loc
|
||||
// per miss, miss_count per request) for shared-index skip layers to replay via
|
||||
// copy_cache_planned_kernel. SkipIO elides only the KV byte movement (timing
|
||||
// probe; output is garbage). Both are compile-time flags so the production
|
||||
// (false, false) instantiation stays byte-identical.
|
||||
template <
|
||||
int BLOCK_SIZE,
|
||||
int NUM_TOP_K,
|
||||
int HOT_BUFFER_SIZE,
|
||||
bool IsMLA,
|
||||
bool IsDsv4Layout,
|
||||
bool RecordMissPlan,
|
||||
bool SkipIO,
|
||||
typename SeqLensT,
|
||||
typename ReqPoolIndicesT>
|
||||
__global__ void load_cache_to_device_buffer_kernel(
|
||||
@@ -296,7 +353,11 @@ __global__ void load_cache_to_device_buffer_kernel(
|
||||
int64_t top_k_tokens_stride,
|
||||
int64_t top_k_device_locs_stride,
|
||||
int64_t page_size,
|
||||
int64_t item_size_bytes) {
|
||||
int64_t item_size_bytes,
|
||||
int64_t* __restrict__ miss_src_out,
|
||||
int32_t* __restrict__ miss_dst_out,
|
||||
int32_t* __restrict__ miss_count_out,
|
||||
int64_t plan_stride) {
|
||||
static_assert(!IsDsv4Layout || IsMLA, "DSv4 page-padded layout is K-only (MLA).");
|
||||
// todo hisparse: support page wise sparsity
|
||||
constexpr int NUM_WARPS = BLOCK_SIZE / WARP_SIZE;
|
||||
@@ -345,6 +406,12 @@ __global__ void load_cache_to_device_buffer_kernel(
|
||||
}
|
||||
req_top_k_device_locs[i] = device_loc;
|
||||
}
|
||||
// Short sequences load nothing from host: an empty miss plan for this request.
|
||||
if constexpr (RecordMissPlan) {
|
||||
if (tid == 0) {
|
||||
miss_count_out[bid] = 0;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -554,11 +621,22 @@ __global__ void load_cache_to_device_buffer_kernel(
|
||||
s_top_k_tokens[miss_offset] = my_token;
|
||||
req_top_k_device_locs[my_token_idx] = req_device_buffer_locs[evict_slot];
|
||||
req_device_buffer_tokens[evict_slot] = my_token;
|
||||
// Record the plan where the eviction is decided so it cannot disagree
|
||||
// with the copy phase; locs are layer-independent (lockstep buffers).
|
||||
if constexpr (RecordMissPlan) {
|
||||
miss_src_out[bid * plan_stride + miss_offset] = req_host_cache_locs[my_token];
|
||||
miss_dst_out[bid * plan_stride + miss_offset] = req_device_buffer_locs[evict_slot];
|
||||
}
|
||||
}
|
||||
}
|
||||
__syncthreads();
|
||||
|
||||
total_misses = NUM_TOP_K - s_total_hits - s_newest_hit;
|
||||
if constexpr (RecordMissPlan) {
|
||||
if (tid == 0) {
|
||||
miss_count_out[bid] = total_misses;
|
||||
}
|
||||
}
|
||||
// Write back LRU order: evictables at front (LRU), hits at back (MRU).
|
||||
{
|
||||
const int total_evictable = HOT_BUFFER_SIZE - s_total_hits;
|
||||
@@ -596,51 +674,28 @@ __global__ void load_cache_to_device_buffer_kernel(
|
||||
}
|
||||
|
||||
// each warp copies one miss directly, can be separated into a new kernel if parallelism is a concern
|
||||
for (int miss_idx = warp_id; miss_idx < total_misses; miss_idx += NUM_WARPS) {
|
||||
const int32_t miss_token = s_top_k_tokens[miss_idx];
|
||||
const int16_t evict_slot = s_lru_slots_out[HOT_BUFFER_SIZE - 1 - miss_idx];
|
||||
if constexpr (!SkipIO) {
|
||||
for (int miss_idx = warp_id; miss_idx < total_misses; miss_idx += NUM_WARPS) {
|
||||
const int32_t miss_token = s_top_k_tokens[miss_idx];
|
||||
const int16_t evict_slot = s_lru_slots_out[HOT_BUFFER_SIZE - 1 - miss_idx];
|
||||
|
||||
const int64_t src_loc = req_host_cache_locs[miss_token];
|
||||
const int64_t dst_loc = static_cast<int64_t>(req_device_buffer_locs[evict_slot]);
|
||||
const int64_t src_loc = req_host_cache_locs[miss_token];
|
||||
const int64_t dst_loc = static_cast<int64_t>(req_device_buffer_locs[evict_slot]);
|
||||
|
||||
if constexpr (IsDsv4Layout) {
|
||||
#ifdef USE_ROCM
|
||||
// ROCm path: host cache and device buffer both use the page-padded C4
|
||||
// layout (same as the write path and the CUDA branch). We can't reuse
|
||||
// device::hisparse::transfer_item here because its warp logic is hardcoded
|
||||
// to a 32-lane warp; on wavefront64 we use transfer_dsv4_item_warp, which
|
||||
// moves the value and the scale in one warp-width-agnostic copy.
|
||||
using namespace device::hisparse;
|
||||
const auto [dst_value_ptr, dst_scale_ptr] = get_pointer_paged(device_buffer_k, static_cast<int32_t>(dst_loc));
|
||||
const auto [src_value_ptr, src_scale_ptr] =
|
||||
get_pointer_paged(const_cast<void*>(host_cache_k), static_cast<int32_t>(src_loc));
|
||||
transfer_dsv4_item_warp(lane_id, src_value_ptr, src_scale_ptr, dst_value_ptr, dst_scale_ptr);
|
||||
#else
|
||||
// CUDA path: page-padded device layout + page-padded host layout, K-only.
|
||||
// The host cache is pinned DRAM but uses the same row layout as the GPU C4
|
||||
// cache, so use the page-padded address calculation for both ends.
|
||||
device::hisparse::transfer_item(
|
||||
/*dst_cache=*/device_buffer_k,
|
||||
/*src_cache=*/const_cast<void*>(host_cache_k),
|
||||
/*dst_index=*/static_cast<int32_t>(dst_loc),
|
||||
/*src_index=*/static_cast<int32_t>(src_loc));
|
||||
#endif
|
||||
} else {
|
||||
// Generic path: device + host both linear, stride = item_size_bytes.
|
||||
const auto src_k = static_cast<const char*>(host_cache_k) + src_loc * item_size_bytes;
|
||||
auto dst_k = static_cast<char*>(device_buffer_k) + dst_loc * item_size_bytes;
|
||||
transfer_item_warp(lane_id, src_k, dst_k, item_size_bytes);
|
||||
|
||||
if constexpr (!IsMLA) {
|
||||
const auto src_v = static_cast<const char*>(host_cache_v) + src_loc * item_size_bytes;
|
||||
auto dst_v = static_cast<char*>(device_buffer_v) + dst_loc * item_size_bytes;
|
||||
transfer_item_warp(lane_id, src_v, dst_v, item_size_bytes);
|
||||
}
|
||||
copy_miss_item<IsMLA, IsDsv4Layout>(
|
||||
lane_id, host_cache_k, host_cache_v, device_buffer_k, device_buffer_v, src_loc, dst_loc, item_size_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <int BLOCK_SIZE, int NUM_TOP_K, int HOT_BUFFER_SIZE, bool IsMLA, bool IsDsv4Layout>
|
||||
template <
|
||||
int BLOCK_SIZE,
|
||||
int NUM_TOP_K,
|
||||
int HOT_BUFFER_SIZE,
|
||||
bool IsMLA,
|
||||
bool IsDsv4Layout,
|
||||
bool RecordMissPlan,
|
||||
bool SkipIO>
|
||||
void load_cache_to_device_buffer(
|
||||
tvm::ffi::TensorView top_k_tokens,
|
||||
tvm::ffi::TensorView device_buffer_tokens,
|
||||
@@ -656,11 +711,22 @@ void load_cache_to_device_buffer(
|
||||
tvm::ffi::TensorView lru_slots,
|
||||
tvm::ffi::TensorView num_real_reqs,
|
||||
int64_t page_size,
|
||||
int64_t item_size_bytes) {
|
||||
int64_t item_size_bytes,
|
||||
tvm::ffi::TensorView miss_src_out,
|
||||
tvm::ffi::TensorView miss_dst_out,
|
||||
tvm::ffi::TensorView miss_count_out) {
|
||||
using namespace host;
|
||||
|
||||
const int64_t bs = top_k_tokens.shape()[0];
|
||||
const int64_t host_stride = host_cache_locs.shape()[1];
|
||||
// Miss-plan side outputs; 0-dim sentinels when RecordMissPlan is false.
|
||||
int64_t* const miss_src_ptr = RecordMissPlan ? static_cast<int64_t*>(miss_src_out.data_ptr()) : nullptr;
|
||||
int32_t* const miss_dst_ptr = RecordMissPlan ? static_cast<int32_t*>(miss_dst_out.data_ptr()) : nullptr;
|
||||
int32_t* const miss_count_ptr = RecordMissPlan ? static_cast<int32_t*>(miss_count_out.data_ptr()) : nullptr;
|
||||
const int64_t plan_stride = RecordMissPlan ? miss_src_out.strides()[0] : 0;
|
||||
if (RecordMissPlan && miss_dst_out.strides()[0] != plan_stride) {
|
||||
throw std::runtime_error("load_cache_to_device_buffer: miss_src/miss_dst row strides differ");
|
||||
}
|
||||
const int64_t buffer_stride_0 = device_buffer_tokens.strides()[0];
|
||||
const int64_t lru_slot_stride_0 = lru_slots.strides()[0];
|
||||
const int64_t top_k_tokens_stride = top_k_tokens.strides()[0];
|
||||
@@ -697,7 +763,11 @@ void load_cache_to_device_buffer(
|
||||
top_k_tokens_stride,
|
||||
top_k_device_locs_stride,
|
||||
page_size,
|
||||
item_size_bytes);
|
||||
item_size_bytes,
|
||||
miss_src_ptr,
|
||||
miss_dst_ptr,
|
||||
miss_count_ptr,
|
||||
plan_stride);
|
||||
};
|
||||
|
||||
const auto seq_dtype = seq_lens.dtype();
|
||||
@@ -713,6 +783,8 @@ void load_cache_to_device_buffer(
|
||||
HOT_BUFFER_SIZE,
|
||||
IsMLA,
|
||||
IsDsv4Layout,
|
||||
RecordMissPlan,
|
||||
SkipIO,
|
||||
int64_t,
|
||||
int64_t>,
|
||||
static_cast<const int64_t*>(seq_lens.data_ptr()),
|
||||
@@ -725,6 +797,8 @@ void load_cache_to_device_buffer(
|
||||
HOT_BUFFER_SIZE,
|
||||
IsMLA,
|
||||
IsDsv4Layout,
|
||||
RecordMissPlan,
|
||||
SkipIO,
|
||||
int64_t,
|
||||
int32_t>,
|
||||
static_cast<const int64_t*>(seq_lens.data_ptr()),
|
||||
@@ -737,6 +811,8 @@ void load_cache_to_device_buffer(
|
||||
HOT_BUFFER_SIZE,
|
||||
IsMLA,
|
||||
IsDsv4Layout,
|
||||
RecordMissPlan,
|
||||
SkipIO,
|
||||
int32_t,
|
||||
int64_t>,
|
||||
static_cast<const int32_t*>(seq_lens.data_ptr()),
|
||||
@@ -749,6 +825,8 @@ void load_cache_to_device_buffer(
|
||||
HOT_BUFFER_SIZE,
|
||||
IsMLA,
|
||||
IsDsv4Layout,
|
||||
RecordMissPlan,
|
||||
SkipIO,
|
||||
int32_t,
|
||||
int32_t>,
|
||||
static_cast<const int32_t*>(seq_lens.data_ptr()),
|
||||
@@ -756,4 +834,93 @@ void load_cache_to_device_buffer(
|
||||
}
|
||||
}
|
||||
|
||||
// Copy-only swap-in for shared-index skip layers: replays the anchor's recorded
|
||||
// miss plan (no hit detection / LRU; the anchor's slot table stays valid). The
|
||||
// small fixed grid (num_blocks) keeps the SM footprint low while overlapping
|
||||
// compute on a side stream. SkipIO is the same probe as in the fused kernel.
|
||||
template <int BLOCK_SIZE, bool IsMLA, bool IsDsv4Layout, bool SkipIO>
|
||||
__global__ __launch_bounds__(BLOCK_SIZE, 1) void copy_cache_planned_kernel(
|
||||
const int64_t* __restrict__ miss_src_locs,
|
||||
const int32_t* __restrict__ miss_dst_locs,
|
||||
const int32_t* __restrict__ miss_counts,
|
||||
const int32_t* __restrict__ num_real_reqs,
|
||||
const void* __restrict__ host_cache_k,
|
||||
const void* __restrict__ host_cache_v,
|
||||
void* __restrict__ device_buffer_k,
|
||||
void* __restrict__ device_buffer_v,
|
||||
int64_t plan_stride,
|
||||
int64_t item_size_bytes) {
|
||||
constexpr int NUM_WARPS = BLOCK_SIZE / WARP_SIZE;
|
||||
const int lane_id = threadIdx.x % WARP_SIZE;
|
||||
const int warp_global = blockIdx.x * NUM_WARPS + threadIdx.x / WARP_SIZE;
|
||||
const int total_warps = gridDim.x * NUM_WARPS;
|
||||
const int real = num_real_reqs[0];
|
||||
|
||||
// Warp-sized windows amortize the miss_counts loads; warps then round-robin
|
||||
// the flattened (request, miss) space so a large sparse batch spreads over
|
||||
// all warps (183us -> 29us at bs=100 with 2 misses/req on H200) while one
|
||||
// request's miss burst still uses every warp.
|
||||
int start = 0; // flat index of the current request's first miss
|
||||
for (int base = 0; base < real; base += WARP_SIZE) {
|
||||
const int r_lane = base + lane_id;
|
||||
const int cnt_lane = (r_lane < real) ? miss_counts[r_lane] : 0;
|
||||
const int window = (real - base < WARP_SIZE) ? (real - base) : WARP_SIZE;
|
||||
for (int j = 0; j < window; ++j) {
|
||||
const int cnt = __shfl_sync(FULL_WARP_MASK, cnt_lane, j);
|
||||
if (cnt == 0) continue;
|
||||
int m0 = (warp_global - start) % total_warps;
|
||||
if (m0 < 0) m0 += total_warps;
|
||||
const int64_t r = base + j;
|
||||
const int64_t* src_row = miss_src_locs + r * plan_stride;
|
||||
const int32_t* dst_row = miss_dst_locs + r * plan_stride;
|
||||
for (int m = m0; m < cnt; m += total_warps) {
|
||||
// Timing probe: the plan is still walked; only the bytes stay put.
|
||||
if constexpr (SkipIO) continue;
|
||||
copy_miss_item<IsMLA, IsDsv4Layout>(
|
||||
lane_id,
|
||||
host_cache_k,
|
||||
host_cache_v,
|
||||
device_buffer_k,
|
||||
device_buffer_v,
|
||||
src_row[m],
|
||||
static_cast<int64_t>(dst_row[m]),
|
||||
item_size_bytes);
|
||||
}
|
||||
start += cnt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <int BLOCK_SIZE, bool IsMLA, bool IsDsv4Layout, bool SkipIO>
|
||||
void copy_cache_planned(
|
||||
tvm::ffi::TensorView miss_src_locs,
|
||||
tvm::ffi::TensorView miss_dst_locs,
|
||||
tvm::ffi::TensorView miss_counts,
|
||||
tvm::ffi::TensorView num_real_reqs,
|
||||
tvm::ffi::TensorView host_cache_k,
|
||||
tvm::ffi::TensorView host_cache_v,
|
||||
tvm::ffi::TensorView device_buffer_k,
|
||||
tvm::ffi::TensorView device_buffer_v,
|
||||
int64_t num_blocks,
|
||||
int64_t item_size_bytes) {
|
||||
using namespace host;
|
||||
const int64_t plan_stride = miss_src_locs.strides()[0];
|
||||
if (miss_dst_locs.strides()[0] != plan_stride) {
|
||||
throw std::runtime_error("copy_cache_planned: miss_src/miss_dst row strides differ");
|
||||
}
|
||||
const auto device = LaunchKernel::resolve_device(miss_src_locs.device());
|
||||
LaunchKernel(num_blocks, BLOCK_SIZE, device)(
|
||||
copy_cache_planned_kernel<BLOCK_SIZE, IsMLA, IsDsv4Layout, SkipIO>,
|
||||
static_cast<const int64_t*>(miss_src_locs.data_ptr()),
|
||||
static_cast<const int32_t*>(miss_dst_locs.data_ptr()),
|
||||
static_cast<const int32_t*>(miss_counts.data_ptr()),
|
||||
static_cast<const int32_t*>(num_real_reqs.data_ptr()),
|
||||
host_cache_k.data_ptr(),
|
||||
(IsMLA || host_cache_v.ndim() == 0) ? (const void*)nullptr : host_cache_v.data_ptr(),
|
||||
device_buffer_k.data_ptr(),
|
||||
(IsMLA || device_buffer_v.ndim() == 0) ? (void*)nullptr : device_buffer_v.data_ptr(),
|
||||
plan_stride,
|
||||
item_size_bytes);
|
||||
}
|
||||
|
||||
} // namespace sglang
|
||||
|
||||
@@ -19,12 +19,29 @@ def _jit_sparse_module(
|
||||
hot_buffer_size: int,
|
||||
is_mla: bool = False,
|
||||
is_dsv4_layout: bool = False,
|
||||
record_miss_plan: bool = False,
|
||||
skip_io: bool = False,
|
||||
) -> Module:
|
||||
# record_miss_plan / skip_io are compile-time kernel flags; the
|
||||
# (False, False) production instantiation stays byte-identical.
|
||||
template_args = make_cpp_args(
|
||||
block_size, num_top_k, hot_buffer_size, is_mla, is_dsv4_layout
|
||||
block_size,
|
||||
num_top_k,
|
||||
hot_buffer_size,
|
||||
is_mla,
|
||||
is_dsv4_layout,
|
||||
record_miss_plan,
|
||||
skip_io,
|
||||
)
|
||||
cache_args = make_cpp_args(
|
||||
item_size_bytes, block_size, num_top_k, hot_buffer_size, is_mla, is_dsv4_layout
|
||||
item_size_bytes,
|
||||
block_size,
|
||||
num_top_k,
|
||||
hot_buffer_size,
|
||||
is_mla,
|
||||
is_dsv4_layout,
|
||||
record_miss_plan,
|
||||
skip_io,
|
||||
)
|
||||
return load_jit(
|
||||
"sparse_cache",
|
||||
@@ -39,6 +56,30 @@ def _jit_sparse_module(
|
||||
)
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _jit_copy_planned_module(
|
||||
block_size: int,
|
||||
is_mla: bool,
|
||||
is_dsv4_layout: bool,
|
||||
skip_io: bool,
|
||||
) -> Module:
|
||||
template_args = make_cpp_args(block_size, is_mla, is_dsv4_layout, skip_io)
|
||||
return load_jit(
|
||||
"sparse_copy_planned",
|
||||
block_size,
|
||||
is_mla,
|
||||
is_dsv4_layout,
|
||||
skip_io,
|
||||
cuda_files=["hisparse.cuh"],
|
||||
cuda_wrappers=[
|
||||
(
|
||||
"copy_cache_planned",
|
||||
f"copy_cache_planned<{template_args}>",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@functools.cache
|
||||
def _jit_dsv4_transfer_module(block_size: int) -> Module:
|
||||
template_args = make_cpp_args(block_size)
|
||||
@@ -91,11 +132,16 @@ def _load_cache_to_device_buffer_mla(
|
||||
page_size: int,
|
||||
block_size: int,
|
||||
num_real_reqs: torch.Tensor | None,
|
||||
miss_src: torch.Tensor | None,
|
||||
miss_dst: torch.Tensor | None,
|
||||
miss_count: torch.Tensor | None,
|
||||
skip_io: bool,
|
||||
) -> None:
|
||||
assert (
|
||||
hot_buffer_size >= num_top_k
|
||||
), f"hot_buffer_size ({hot_buffer_size}) must be >= num_top_k ({num_top_k})"
|
||||
|
||||
record_miss_plan = miss_src is not None
|
||||
module = _jit_sparse_module(
|
||||
item_size_bytes,
|
||||
block_size,
|
||||
@@ -103,6 +149,8 @@ def _load_cache_to_device_buffer_mla(
|
||||
hot_buffer_size,
|
||||
is_mla=True,
|
||||
is_dsv4_layout=is_dsv4_layout,
|
||||
record_miss_plan=record_miss_plan,
|
||||
skip_io=skip_io,
|
||||
)
|
||||
|
||||
empty = torch.empty(0)
|
||||
@@ -112,6 +160,16 @@ def _load_cache_to_device_buffer_mla(
|
||||
[top_k_tokens.size(0)], dtype=torch.int32, device=top_k_tokens.device
|
||||
)
|
||||
|
||||
if record_miss_plan:
|
||||
assert miss_dst is not None and miss_count is not None
|
||||
assert miss_src.dtype == torch.int64 and miss_dst.dtype == torch.int32
|
||||
assert miss_count.dtype == torch.int32
|
||||
# The kernel indexes both plan rows with one stride.
|
||||
assert miss_src.stride(0) == miss_dst.stride(0)
|
||||
else:
|
||||
# Unused sentinels; the RecordMissPlan=false instantiation never reads them.
|
||||
miss_src = miss_dst = miss_count = empty
|
||||
|
||||
module.load_cache_to_device_buffer(
|
||||
top_k_tokens,
|
||||
device_buffer_tokens,
|
||||
@@ -128,6 +186,9 @@ def _load_cache_to_device_buffer_mla(
|
||||
num_real_reqs,
|
||||
page_size,
|
||||
item_size_bytes,
|
||||
miss_src,
|
||||
miss_dst,
|
||||
miss_count,
|
||||
)
|
||||
|
||||
|
||||
@@ -148,8 +209,16 @@ def load_cache_to_device_buffer_mla(
|
||||
page_size: int = 1,
|
||||
block_size: int = 256,
|
||||
num_real_reqs: torch.Tensor | None = None,
|
||||
miss_src: torch.Tensor | None = None,
|
||||
miss_dst: torch.Tensor | None = None,
|
||||
miss_count: torch.Tensor | None = None,
|
||||
skip_io: bool = False,
|
||||
) -> None:
|
||||
"""Generic MLA hisparse swap-in: device + host both linear (stride=item_size_bytes)."""
|
||||
"""Generic MLA hisparse swap-in: device + host both linear (stride=item_size_bytes).
|
||||
|
||||
Optional miss_src/miss_dst/miss_count record the miss plan for replay by
|
||||
copy_cache_planned_mla; skip_io elides only the KV bytes (timing probe).
|
||||
"""
|
||||
_load_cache_to_device_buffer_mla(
|
||||
is_dsv4_layout=False,
|
||||
top_k_tokens=top_k_tokens,
|
||||
@@ -168,6 +237,47 @@ def load_cache_to_device_buffer_mla(
|
||||
page_size=page_size,
|
||||
block_size=block_size,
|
||||
num_real_reqs=num_real_reqs,
|
||||
miss_src=miss_src,
|
||||
miss_dst=miss_dst,
|
||||
miss_count=miss_count,
|
||||
skip_io=skip_io,
|
||||
)
|
||||
|
||||
|
||||
def copy_cache_planned_mla(
|
||||
*,
|
||||
miss_src: torch.Tensor,
|
||||
miss_dst: torch.Tensor,
|
||||
miss_count: torch.Tensor,
|
||||
num_real_reqs: torch.Tensor,
|
||||
host_cache: torch.Tensor,
|
||||
device_buffer: torch.Tensor,
|
||||
item_size_bytes: int,
|
||||
num_blocks: int = 4,
|
||||
block_size: int = 1024,
|
||||
is_dsv4_layout: bool = False,
|
||||
skip_io: bool = False,
|
||||
) -> None:
|
||||
"""Replay a recorded miss plan (host_cache -> device_buffer) for a skip layer.
|
||||
|
||||
IO-only, no planning; the small fixed grid keeps the SM footprint low while
|
||||
overlapped on a side stream. The anchor's slot table stays valid (lockstep).
|
||||
"""
|
||||
assert miss_src.dtype == torch.int64 and miss_dst.dtype == torch.int32
|
||||
assert miss_count.dtype == torch.int32
|
||||
module = _jit_copy_planned_module(block_size, True, is_dsv4_layout, skip_io)
|
||||
empty = torch.empty(0)
|
||||
module.copy_cache_planned(
|
||||
miss_src,
|
||||
miss_dst,
|
||||
miss_count,
|
||||
num_real_reqs,
|
||||
host_cache,
|
||||
empty,
|
||||
device_buffer,
|
||||
empty,
|
||||
num_blocks,
|
||||
item_size_bytes,
|
||||
)
|
||||
|
||||
|
||||
@@ -188,6 +298,10 @@ def load_cache_to_device_buffer_dsv4_mla(
|
||||
page_size: int = 1,
|
||||
block_size: int = 256,
|
||||
num_real_reqs: torch.Tensor | None = None,
|
||||
miss_src: torch.Tensor | None = None,
|
||||
miss_dst: torch.Tensor | None = None,
|
||||
miss_count: torch.Tensor | None = None,
|
||||
skip_io: bool = False,
|
||||
) -> None:
|
||||
"""DSv4 hisparse swap-in: page-padded device + page-padded host C4 layout."""
|
||||
_load_cache_to_device_buffer_mla(
|
||||
@@ -208,4 +322,8 @@ def load_cache_to_device_buffer_dsv4_mla(
|
||||
page_size=page_size,
|
||||
block_size=block_size,
|
||||
num_real_reqs=num_real_reqs,
|
||||
miss_src=miss_src,
|
||||
miss_dst=miss_dst,
|
||||
miss_count=miss_count,
|
||||
skip_io=skip_io,
|
||||
)
|
||||
|
||||
@@ -846,6 +846,14 @@ class Envs:
|
||||
# Triton two_dot variant, 1.16-1.38x faster across GLM/DS shapes).
|
||||
SGLANG_OPT_Q8KV8_QPREP_VARIANT = EnvStr("auto")
|
||||
|
||||
# HiSparse
|
||||
# Kill-switch for the shared-index (IndexShare) swap-in prefetch
|
||||
# (auto-enabled for GLM-5.2-style DSA); set True to A/B synchronous swap-in.
|
||||
SGLANG_DISABLE_HISPARSE_PREFETCH = EnvBool(False)
|
||||
# Timing probe: run the swap-in fully but skip the host->device KV bytes,
|
||||
# measuring the "IO is free" floor. GARBAGE OUTPUT -- benchmarking only.
|
||||
SGLANG_DEBUG_HISPARSE_SKIP_IO = EnvBool(False)
|
||||
|
||||
# TRT-LLM-gen fused MoE (SiTU) via sglang JIT: path to an unpacked SiTU
|
||||
# cubin pool (cubins + flat ABI headers + overlay/; distributed as a
|
||||
# single downloadable archive). Needs the public flashinfer package
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
# to be combined with the sparse coordinator class and sparse algorithm family
|
||||
|
||||
import logging
|
||||
from typing import List, NamedTuple, Union
|
||||
from typing import Dict, List, NamedTuple, Optional, Tuple, Union
|
||||
|
||||
import torch
|
||||
|
||||
from sglang.kernels.ops.kvcache.hisparse import (
|
||||
copy_cache_planned_mla,
|
||||
load_cache_to_device_buffer_dsv4_mla,
|
||||
load_cache_to_device_buffer_mla,
|
||||
)
|
||||
from sglang.srt.configs.model_config import dsa_layer_skips_topk, is_deepseek_dsa
|
||||
from sglang.srt.environ import envs
|
||||
from sglang.srt.managers.schedule_batch import Req
|
||||
from sglang.srt.mem_cache.allocator.hisparse import (
|
||||
DeepSeekV4HiSparseTokenToKVPoolAllocator,
|
||||
@@ -42,6 +45,69 @@ class HiSparseTokenStats(NamedTuple):
|
||||
host_token_usage: float
|
||||
|
||||
|
||||
def resolve_shared_index_layers(
|
||||
*,
|
||||
hf_text_config,
|
||||
pp_size: int,
|
||||
is_speculative: bool,
|
||||
) -> Optional[List[bool]]:
|
||||
"""Per-layer "reuses the previous layer's top-k index" pattern, or None.
|
||||
|
||||
Mirrors DeepseekV2AttentionMLA's skip_topk derivation (index_topk_pattern /
|
||||
index_topk_freq / cli_factor); None when the model has no sharing or the
|
||||
prefetch cannot run (PP, speculative decoding, kill-switch).
|
||||
"""
|
||||
if not is_deepseek_dsa(hf_text_config):
|
||||
return None
|
||||
num_layers = hf_text_config.num_hidden_layers
|
||||
cli_factor = getattr(hf_text_config, "cli_factor", 1) or 1
|
||||
if cli_factor > 1:
|
||||
pattern = [i % cli_factor != 0 for i in range(num_layers)]
|
||||
else:
|
||||
pattern = [dsa_layer_skips_topk(hf_text_config, i) for i in range(num_layers)]
|
||||
if not any(pattern):
|
||||
return None
|
||||
if pp_size != 1 or is_speculative:
|
||||
logger.warning(
|
||||
"HiSparse shared-index prefetch is unsupported under pipeline "
|
||||
"parallelism / speculative decoding; falling back to synchronous "
|
||||
"swap-in."
|
||||
)
|
||||
return None
|
||||
if envs.SGLANG_DISABLE_HISPARSE_PREFETCH.get():
|
||||
logger.info(
|
||||
"HiSparse shared-index prefetch disabled via "
|
||||
"SGLANG_DISABLE_HISPARSE_PREFETCH; using synchronous swap-in."
|
||||
)
|
||||
return None
|
||||
return pattern
|
||||
|
||||
|
||||
def _build_prefetch_groups(
|
||||
is_shared_index_layer: List[bool],
|
||||
) -> Tuple[Dict[int, List[int]], List[int]]:
|
||||
"""Group consecutive shared-index (skip) layers under their anchor layer.
|
||||
|
||||
Returns (groups, slot): anchor layer_id -> ordered skip layers, and each
|
||||
skip layer's position in its group (indexes the per-slot prefetch events).
|
||||
"""
|
||||
groups: Dict[int, List[int]] = {}
|
||||
slot = [0] * len(is_shared_index_layer)
|
||||
anchor = None
|
||||
for i, is_shared in enumerate(is_shared_index_layer):
|
||||
if not is_shared:
|
||||
anchor = i # compute layer; anchors the skip layers after it
|
||||
continue
|
||||
assert anchor is not None, (
|
||||
f"shared-index (skip) layer {i} has no preceding compute layer; "
|
||||
"the model's index-topk pattern is invalid"
|
||||
)
|
||||
group = groups.setdefault(anchor, [])
|
||||
slot[i] = len(group)
|
||||
group.append(i)
|
||||
return groups, slot
|
||||
|
||||
|
||||
class HiSparseCoordinator:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -56,6 +122,7 @@ class HiSparseCoordinator:
|
||||
tp_group,
|
||||
host_to_device_ratio: int = 2,
|
||||
swap_in_block_size: int = 960,
|
||||
shared_index_layers: Optional[List[bool]] = None,
|
||||
):
|
||||
self.req_to_token_pool = req_to_token_pool
|
||||
self.token_to_kv_pool_allocator = token_to_kv_pool_allocator
|
||||
@@ -63,6 +130,9 @@ class HiSparseCoordinator:
|
||||
self.device_buffer_size = device_buffer_size
|
||||
self.device = device
|
||||
self.swap_in_block_size = swap_in_block_size
|
||||
# Timing probe: skip the host->device KV bytes to measure the "IO is
|
||||
# free" floor. Produces garbage output; benchmarking only.
|
||||
self.skip_io = envs.SGLANG_DEBUG_HISPARSE_SKIP_IO.get()
|
||||
self.compress_ratio = self.token_to_kv_pool_allocator.compress_ratio
|
||||
|
||||
self.is_dsv4_hisparse = isinstance(
|
||||
@@ -186,6 +256,65 @@ class HiSparseCoordinator:
|
||||
# staging already backed up all prefill tokens. Cleared after one step.
|
||||
self._skip_first_backup = [False] * max_num_req_slots
|
||||
|
||||
self._init_shared_index_prefetch(
|
||||
shared_index_layers=shared_index_layers,
|
||||
layer_num=layer_num,
|
||||
max_num_req_slots=max_num_req_slots,
|
||||
)
|
||||
|
||||
def _init_shared_index_prefetch(
|
||||
self,
|
||||
shared_index_layers: Optional[List[bool]],
|
||||
layer_num: int,
|
||||
max_num_req_slots: int,
|
||||
) -> None:
|
||||
"""Set up the plan-then-IO prefetch for shared-index (IndexShare) models:
|
||||
the anchor's kernel records its miss plan and skip layers replay it on
|
||||
`prefetch_stream`, overlapping their IO with the intervening compute."""
|
||||
if shared_index_layers is not None and len(shared_index_layers) != layer_num:
|
||||
# Attention-layer count differs from num_hidden_layers (e.g. Longcat
|
||||
# doubles it): pattern would be misindexed, fall back to synchronous.
|
||||
logger.warning(
|
||||
"HiSparse shared-index prefetch disabled: pattern length %d != "
|
||||
"KV pool layer_num %d; using synchronous swap-in.",
|
||||
len(shared_index_layers),
|
||||
layer_num,
|
||||
)
|
||||
shared_index_layers = None
|
||||
self._is_shared_index_layer = list(shared_index_layers or [False] * layer_num)
|
||||
self.enable_prefetch = any(self._is_shared_index_layer)
|
||||
self._prefetch_groups, self._prefetch_slot = _build_prefetch_groups(
|
||||
self._is_shared_index_layer
|
||||
)
|
||||
if not self.enable_prefetch:
|
||||
return
|
||||
|
||||
# Small fixed grid for the copy-only kernel: low SM footprint so the
|
||||
# copies overlap compute with little contention.
|
||||
self._prefetch_copy_blocks = 4
|
||||
max_group_size = max(len(g) for g in self._prefetch_groups.values())
|
||||
self.prefetch_stream = device_module.Stream()
|
||||
self._prefetch_events = [device_module.Event() for _ in range(max_group_size)]
|
||||
# Plan recorded by the current anchor, replayed by its skip layers. One
|
||||
# buffer set suffices: the last skip layer's event wait orders the next
|
||||
# anchor's writes after this group's copies.
|
||||
self._miss_src = torch.zeros(
|
||||
(max_num_req_slots, self.top_k), dtype=torch.int64, device=self.device
|
||||
)
|
||||
self._miss_dst = torch.zeros(
|
||||
(max_num_req_slots, self.top_k), dtype=torch.int32, device=self.device
|
||||
)
|
||||
self._miss_count = torch.zeros(
|
||||
(max_num_req_slots,), dtype=torch.int32, device=self.device
|
||||
)
|
||||
logger.info(
|
||||
"HiSparse: shared-index prefetch (plan-then-IO) enabled; %d anchor "
|
||||
"group(s), %d skip layer(s) of %d total.",
|
||||
len(self._prefetch_groups),
|
||||
sum(self._is_shared_index_layer),
|
||||
layer_num,
|
||||
)
|
||||
|
||||
def set_decode_producer_stream(self, stream) -> None:
|
||||
self.decode_producer_stream = stream
|
||||
|
||||
@@ -194,6 +323,9 @@ class HiSparseCoordinator:
|
||||
# See HostKVCache.destroy for why the explicit unregister matters.
|
||||
self.write_staging_stream.synchronize()
|
||||
self.decode_backup_stream.synchronize()
|
||||
if self.enable_prefetch:
|
||||
# Skip-layer copies read the pinned host pool on the prefetch stream.
|
||||
self.prefetch_stream.synchronize()
|
||||
self.mem_pool_host.destroy()
|
||||
|
||||
def get_token_stats(self) -> HiSparseTokenStats:
|
||||
@@ -802,16 +934,20 @@ class HiSparseCoordinator:
|
||||
self.lru_slots[:, req.req_pool_idx, :].copy_(self._lru_init)
|
||||
self._skip_first_backup[req.req_pool_idx] = False
|
||||
|
||||
def swap_in_selected_pages(
|
||||
def _run_swap_in_kernel(
|
||||
self,
|
||||
req_pool_indices: torch.Tensor,
|
||||
compressed_seq_lens: torch.Tensor,
|
||||
top_k_result: torch.Tensor,
|
||||
layer_id: int,
|
||||
record_plan: bool = False,
|
||||
) -> torch.Tensor:
|
||||
"""Swap selected top-k tokens into device memory and return their indices."""
|
||||
num_reqs = req_pool_indices.size(0)
|
||||
"""Run the full plan+IO swap-in kernel for one layer; return its slot table.
|
||||
|
||||
record_plan (set on the anchor of a shared-index group) also records the
|
||||
miss plan into self._miss_{src,dst,count} for the skip layers to replay.
|
||||
"""
|
||||
num_reqs = req_pool_indices.size(0)
|
||||
top_k_indices = self.top_k_device_locs_buffer[:num_reqs]
|
||||
|
||||
swap_in_fn = (
|
||||
@@ -819,6 +955,15 @@ class HiSparseCoordinator:
|
||||
if self.is_dsv4_hisparse
|
||||
else load_cache_to_device_buffer_mla
|
||||
)
|
||||
plan = (
|
||||
dict(
|
||||
miss_src=self._miss_src[:num_reqs],
|
||||
miss_dst=self._miss_dst[:num_reqs],
|
||||
miss_count=self._miss_count[:num_reqs],
|
||||
)
|
||||
if record_plan
|
||||
else {}
|
||||
)
|
||||
swap_in_fn(
|
||||
top_k_tokens=top_k_result,
|
||||
device_buffer_tokens=self.req_device_buffer_tokens[layer_id],
|
||||
@@ -836,5 +981,70 @@ class HiSparseCoordinator:
|
||||
page_size=1,
|
||||
block_size=self.swap_in_block_size,
|
||||
num_real_reqs=self.num_real_reqs,
|
||||
skip_io=self.skip_io,
|
||||
**plan,
|
||||
)
|
||||
return top_k_indices
|
||||
|
||||
def _run_copy_only_kernel(self, num_reqs: int, skip_layer: int) -> None:
|
||||
"""Replay the anchor's recorded miss plan into a skip layer's buffers
|
||||
(IO-only; the anchor's slot table stays valid -- lockstep layout)."""
|
||||
copy_cache_planned_mla(
|
||||
miss_src=self._miss_src[:num_reqs],
|
||||
miss_dst=self._miss_dst[:num_reqs],
|
||||
miss_count=self._miss_count[:num_reqs],
|
||||
num_real_reqs=self.num_real_reqs,
|
||||
host_cache=self.mem_pool_host.kv_buffer[skip_layer],
|
||||
device_buffer=self.mem_pool_device.kv_buffer[skip_layer],
|
||||
item_size_bytes=self.item_size_bytes,
|
||||
num_blocks=self._prefetch_copy_blocks,
|
||||
is_dsv4_layout=self.is_dsv4_hisparse,
|
||||
skip_io=self.skip_io,
|
||||
)
|
||||
|
||||
def swap_in_selected_pages(
|
||||
self,
|
||||
req_pool_indices: torch.Tensor,
|
||||
compressed_seq_lens: torch.Tensor,
|
||||
top_k_result: torch.Tensor,
|
||||
layer_id: int,
|
||||
) -> torch.Tensor:
|
||||
"""Swap selected top-k tokens into device memory and return their indices.
|
||||
|
||||
With prefetch enabled, anchors swap in synchronously (recording the miss
|
||||
plan) and prefetch their skip layers' copies; skip layers just wait.
|
||||
"""
|
||||
if not self.enable_prefetch:
|
||||
return self._run_swap_in_kernel(
|
||||
req_pool_indices, compressed_seq_lens, top_k_result, layer_id
|
||||
)
|
||||
|
||||
num_reqs = req_pool_indices.size(0)
|
||||
if self._is_shared_index_layer[layer_id]:
|
||||
# Skip layer: wait for its prefetched copy; the anchor's slot table
|
||||
# applies (shared index + lockstep buffers).
|
||||
slot = self._prefetch_slot[layer_id]
|
||||
self._prefetch_events[slot].wait(device_module.current_stream())
|
||||
return self.top_k_device_locs_buffer[:num_reqs]
|
||||
|
||||
# Anchor: swap in synchronously (recording the plan), then prefetch the
|
||||
# skip layers' copies on the side stream.
|
||||
group = self._prefetch_groups.get(layer_id)
|
||||
anchor_locs = self._run_swap_in_kernel(
|
||||
req_pool_indices,
|
||||
compressed_seq_lens,
|
||||
top_k_result,
|
||||
layer_id,
|
||||
record_plan=group is not None,
|
||||
)
|
||||
if group:
|
||||
# Fork: the prefetch stream must observe the anchor's plan (produced
|
||||
# on the current stream) before replaying it.
|
||||
self.prefetch_stream.wait_stream(device_module.current_stream())
|
||||
with device_module.stream(self.prefetch_stream):
|
||||
for skip_layer in group:
|
||||
self._run_copy_only_kernel(num_reqs, skip_layer)
|
||||
self._prefetch_events[self._prefetch_slot[skip_layer]].record(
|
||||
self.prefetch_stream
|
||||
)
|
||||
return anchor_locs
|
||||
|
||||
@@ -847,7 +847,10 @@ class ModelRunner:
|
||||
def maybe_init_hisparse_coordinator(self):
|
||||
if not self.enable_hisparse:
|
||||
return
|
||||
from sglang.srt.managers.hisparse_coordinator import HiSparseCoordinator
|
||||
from sglang.srt.managers.hisparse_coordinator import (
|
||||
HiSparseCoordinator,
|
||||
resolve_shared_index_layers,
|
||||
)
|
||||
from sglang.srt.mem_cache.sparsity import parse_hisparse_config
|
||||
|
||||
hisparse_cfg = parse_hisparse_config(self.server_args)
|
||||
@@ -867,6 +870,11 @@ class ModelRunner:
|
||||
),
|
||||
host_to_device_ratio=hisparse_cfg.host_to_device_ratio,
|
||||
swap_in_block_size=hisparse_cfg.swap_in_block_size,
|
||||
shared_index_layers=resolve_shared_index_layers(
|
||||
hf_text_config=self.model_config.hf_text_config,
|
||||
pp_size=self.ps.pp_size,
|
||||
is_speculative=self.spec_algorithm.is_speculative(),
|
||||
),
|
||||
)
|
||||
|
||||
def post_capture_resize_kv_pool(self):
|
||||
|
||||
Reference in New Issue
Block a user