Support CPU offload for mxfp8 KV cache (#35888)

This commit is contained in:
Ke Bao
2026-08-22 10:19:06 +08:00
committed by GitHub
parent d90318b3e2
commit 5662c03363
3 changed files with 134 additions and 12 deletions
+64 -4
View File
@@ -3547,13 +3547,73 @@ class MHATokenToKVPoolMXFP8(MHATokenToKVPool):
self.k_scale_buffer[idx][tgt_loc] = self.k_scale_buffer[idx][src_loc]
self.v_scale_buffer[idx][tgt_loc] = self.v_scale_buffer[idx][src_loc]
# These paths copy k/v buffers without the scale buffers; fail loudly
# instead of silently corrupting dequantization.
def _read_scales(self, idx, loc):
"""Per-token UE8M0 K/V scales at ``loc``, inverse of ``_write_scales``."""
if self.mxfp8_sf_interleaved:
return (
self._read_sf_interleaved(self.k_scale_buffer[idx], loc),
self._read_sf_interleaved(self.v_scale_buffer[idx], loc),
)
return self.k_scale_buffer[idx][loc], self.v_scale_buffer[idx][loc]
def get_cpu_copy(self, indices, mamba_indices=None):
raise NotImplementedError("CPU offloading is unsupported for MXFP8 KV cache.")
# The scales travel with their fp8 payload; a restored slot dequantizes
# against mismatched exponents without them.
assert not self.use_hnd, (
"CPU KV offload indexes by slot (NHD); HND KV cache "
"(SGLANG_USE_HND_KVCACHE) is not supported with CPU offload yet."
)
current_platform.synchronize()
kv_cache_cpu = []
chunk_size = self.cpu_offloading_chunk_size
for layer_id in range(self.layer_num):
kv_cache_cpu.append([])
for i in range(0, len(indices), chunk_size):
chunk_indices = indices[i : i + chunk_size]
k_scale, v_scale = self._read_scales(layer_id, chunk_indices)
kv_cache_cpu[-1].append(
[
self.k_buffer[layer_id][chunk_indices].to(
"cpu", non_blocking=True
),
self.v_buffer[layer_id][chunk_indices].to(
"cpu", non_blocking=True
),
k_scale.to("cpu", non_blocking=True),
v_scale.to("cpu", non_blocking=True),
]
)
current_platform.synchronize()
return kv_cache_cpu
def load_cpu_copy(self, kv_cache_cpu, indices, mamba_indices=None):
raise NotImplementedError("CPU offloading is unsupported for MXFP8 KV cache.")
assert not self.use_hnd, (
"CPU KV offload indexes by slot (NHD); HND KV cache "
"(SGLANG_USE_HND_KVCACHE) is not supported with CPU offload yet."
)
current_platform.synchronize()
device = self.k_buffer[0].device
chunk_size = self.cpu_offloading_chunk_size
for layer_id in range(self.layer_num):
for i in range(0, len(indices), chunk_size):
chunk_indices = indices[i : i + chunk_size]
k_cpu, v_cpu, k_scale_cpu, v_scale_cpu = kv_cache_cpu[layer_id][
i // chunk_size
]
assert k_cpu.shape[0] == v_cpu.shape[0] == len(chunk_indices)
self.k_buffer[layer_id][chunk_indices] = k_cpu.to(
device, non_blocking=True
)
self.v_buffer[layer_id][chunk_indices] = v_cpu.to(
device, non_blocking=True
)
self._write_scales(
layer_id,
chunk_indices,
k_scale_cpu.to(device, non_blocking=True),
v_scale_cpu.to(device, non_blocking=True),
)
current_platform.synchronize()
def get_kv_scale_buf_infos(self):
"""(ptrs, lens, item_lens) for the UE8M0 scale buffers, k then v.
@@ -245,16 +245,17 @@ class SWAKVPool(BaseSWAKVPool):
filtered.append([])
continue
k_cpu = torch.cat([chunk[0] for chunk in layer_chunks], dim=0)
v_cpu = torch.cat([chunk[1] for chunk in layer_chunks], dim=0)
k_cpu = k_cpu[row_mask]
v_cpu = v_cpu[row_mask]
# A chunk is whatever the sub-pool produced: k/v, plus the block
# scales for a quantized pool. Filter every tensor it carries.
num_tensors = len(layer_chunks[0])
tensors = [
torch.cat([chunk[t] for chunk in layer_chunks], dim=0)[row_mask]
for t in range(num_tensors)
]
filtered_layer = []
for i in range(0, len(k_cpu), chunk_size):
filtered_layer.append(
[k_cpu[i : i + chunk_size], v_cpu[i : i + chunk_size]]
)
for i in range(0, len(tensors[0]), chunk_size):
filtered_layer.append([t[i : i + chunk_size] for t in tensors])
filtered.append(filtered_layer)
return filtered