21 KiB
CP Shared KV + HiCache Host Integration Design
Goal: Deliver a first reliable integration between NSA CP shared KV and HiCache host caching for GLM 5 inference.
Architecture: Keep scheduler, radix cache, and req-to-token in the CP-group logical address space. Keep device and host KV pools physical and per-rank sharded. Add a CP-aware HiCache controller adapter plus explicit per-node host metadata so only owned physical shards are transferred while radix state remains logically complete.
Tech Stack: Python, SGLang server runtime, NSA NSATokenToKVPool, CPSharedPagedTokenToKVPoolAllocator, CpSharedKVLayout, HiRadixCache, HiCacheController.
Scope
This design covers the first implementation stage only:
- Support
--enable-nsa-prefill-cp-shared-kvwith HiCache host write/load/evict. - Keep HiCache storage backends unsupported in this mode.
- Keep radix tree, scheduler admission, and req-to-token state in full logical KV loc space.
- Keep each CP rank's device KV pool and host KV pool sharded by physical owner pages.
This design does not cover:
- HiCache storage backend backup or prefetch.
- Storage key changes for File, NIXL, Mooncake, EIC, HF3FS, or other backends.
- Decode-side CP/shared KV.
- HiSparse NSA KV pools.
- A full replacement of HiRadixCache with upstream UnifiedRadixTree or HybridCacheController.
If CP shared KV and hicache_storage_backend are enabled together, startup must fail fast with a clear error. Silent fallback is not acceptable because the existing storage path assumes page-hash sequences and contiguous host indices that do not describe CP-owned sparse shards.
Existing Code Facts
The current CP shared KV branch already separates logical and physical KV locs:
python/sglang/srt/mem_cache/cp_shared_kv_layout.pyprovides owner masks and logical-to-physical conversions for torch and NumPy paths.python/sglang/srt/mem_cache/allocator.py::CPSharedPagedTokenToKVPoolAllocatortracks logical allocator size while storing physical pool size separately.python/sglang/srt/model_executor/model_runner_kv_cache_mixin.pyprofiles physical token capacity and expands logical capacity by CP size.- Runtime persistent writes already use
CpSharedKVLayoutto filter owned logical locs and convert to physical locs before touching local KV buffers.
The current HiCache path is not CP-aware:
HiRadixCache.write_backup()passesnode.valuedirectly toHiCacheController.write().HiCacheController.write()allocates host indices for the full tensor length and transfers those indices as if they were physical device indices.HiCacheController.load()allocates device indices for the full host tensor length and transfers those indices as if they were physical device indices.HiRadixCache.load_back()useslen(node.host_value)to compute host hit length and to slice restored device indices.HiRadixCache._split_node()slicesnode.host_valuewhen radix nodes split.
Under CP shared KV, node.value is a full logical loc tensor. It cannot be passed directly into physical host/device transfer functions.
Core Invariants
The implementation must preserve these invariants:
TreeNode.valueremains a full logical loc tensor.req_to_tokenremains full logical loc state.- Scheduler admission and radix matching remain based on full logical token lengths.
HostKVCacheremains physical-only and does not import or depend onCpSharedKVLayout.- CP filtering and logical-to-physical conversion happen in
HiCacheControlleror a small adapter owned by it. TreeNode.host_valuekeeps the legacy non-CP meaning: physical host indices tensor.- CP mode does not store CP-owned host subsets in
TreeNode.host_value. - CP mode uses explicit metadata to represent host backup state.
- Host eviction and load-back accounting report logical token counts, even though local physical transfers are sparse.
Metadata Model
Add explicit CP host metadata for HiRadixCache nodes.
Recommended structure:
@dataclass
class CpHiCacheNodeMetadata:
logical_len: int
owned_positions: torch.Tensor
host_indices: torch.Tensor
def split(self, split_len: int) -> tuple["CpHiCacheNodeMetadata", "CpHiCacheNodeMetadata"]:
if split_len < 0 or split_len > self.logical_len:
raise ValueError(
f"split_len must be in [0, {self.logical_len}], got {split_len}"
)
parent_mask = self.owned_positions < split_len
child_mask = ~parent_mask
parent = CpHiCacheNodeMetadata(
logical_len=split_len,
owned_positions=self.owned_positions[parent_mask],
host_indices=self.host_indices[parent_mask],
)
child = CpHiCacheNodeMetadata(
logical_len=self.logical_len - split_len,
owned_positions=self.owned_positions[child_mask] - split_len,
host_indices=self.host_indices[child_mask],
)
return parent, child
Field semantics:
logical_lenis the full logical token count represented by this node.owned_positionsare node-local logical token positions owned by this CP rank.host_indicesare local physical host slots storing the owned token KV data.len(owned_positions) == len(host_indices).owned_positionscan be empty on a rank that owns no page in this node.owned_positionsare node-local positions, not old logical loc values. This avoids depending on evicted logical locs remaining reusable after load-back.owned_positionsandhost_indicesare stored as CPUtorch.int64tensors. Radix tree operations are CPU-side bookkeeping, and keeping metadata on CPU avoids retaining GPU tensors in long-lived tree nodes.
Add fields to TreeNode or attach equivalent metadata from HiRadixCache:
host_len: int = 0cp_hicache: Optional[CpHiCacheNodeMetadata] = None
For non-CP HiCache, existing host_value behavior remains unchanged.
For CP HiCache, backup state is represented by host_len > 0 and cp_hicache is not None.
Radix backup checks must become cache-type aware. A helper is preferable to changing the global TreeNode.backuped property for all cache classes:
def _node_backuped(self, node: TreeNode) -> bool:
if self._uses_cp_hicache:
return node.host_len > 0 and node.cp_hicache is not None
return node.host_value is not None
Use this helper inside HiRadixCache paths that currently read node.backuped. Do not change SWA, Mamba, or plain Radix semantics as part of this integration unless a touched path requires it.
Controller Adapter
HiCacheController is the transfer chokepoint and should own CP-aware translation.
CP HiCache mode must be detected from the allocator or explicit layout, not from the KV pool alone. Use isinstance(token_to_kv_pool_allocator, CPSharedPagedTokenToKVPoolAllocator) or an explicitly passed CpSharedKVLayout to enable the CP adapter. After CP mode is detected, validate that the underlying KV pool is NSATokenToKVPool for this stage.
Add initialization state:
uses_cp_hicache: boolcp_shared_kv_layout: Optional[CpSharedKVLayout]page_size: int
The controller can derive CP mode from the allocator by checking for CP-specific fields such as cp_size and cp_rank, or it can receive an explicit layout from HiRadixCache. Prefer explicit construction in HiRadixCache and passing the layout into HiCacheController so the adapter has one source of truth.
CP Write
External call shape stays close to the current API:
metadata = cache_controller.write(device_indices=node.value, node_id=node.id)
CP behavior:
- Treat
device_indicesas full logical locs. - Build
owned_mask = layout.owned_by_this_rank(device_indices). - Build
owned_positions = owned_mask.nonzero(as_tuple=True)[0]. - Build
physical_device_indices = layout.logical_locs_to_physical(device_indices[owned_mask]). - Allocate
host_indices = mem_pool_host.alloc(len(physical_device_indices)). - If allocation fails, return a failure that exposes
required_host_slots=len(physical_device_indices)soHiRadixCachecan evict physical host slots and retry. - If the owned count is zero, skip host allocation and skip physical transfer, but still return metadata with
logical_len=len(device_indices). - Enqueue or run the existing host transfer using
host_indicesandphysical_device_indices. - Store returned metadata tensors on CPU:
owned_positions.cpu()andhost_indices.cpu(). - Return
CpHiCacheNodeMetadata(logical_len, owned_positions, host_indices).
Every logical write-back must produce the same acknowledgement cardinality on every TP rank. If a rank owns zero physical slots for the node, the controller must still create a completed no-op write acknowledgement when HiRadixCache adds the node to ongoing_write_through. This preserves the existing writing_check() invariant that all ranks pop the same logical ack ids after all_reduce(MIN).
The non-CP path continues returning the existing host indices tensor.
CP Load
The load path must allocate full logical device locs but transfer only owned physical shards.
Recommended API:
device_indices = cache_controller.load_cp(nodes_to_load)
CP behavior:
- Compute
logical_len = sum(node.host_len for node in nodes_to_load). - Allocate
device_indices = mem_pool_device_allocator.alloc(logical_len). - For each node, take its slice of
device_indices. - Use
node.cp_hicache.owned_positionsto select the newly allocated logical locs owned by this rank. - Convert selected logical locs to physical locs with
layout.logical_locs_to_physical(selected_logical_locs). - Concatenate all owned host indices and owned physical device indices.
- If the owned count is zero, skip physical transfer and still return the full logical
device_indices. - Enqueue or run the existing host-to-device transfer for the owned physical pairs.
- Return the full logical
device_indicesto HiRadixCache.
CP load-back has the same acknowledgement cardinality requirement as CP write-back. A zero-owned rank must still produce a completed no-op load acknowledgement or equivalent completed producer event so lock release and producer ids remain consistent across ranks.
The non-CP path continues using the existing load(host_indices=host_indices) API shape.
HiRadixCache Changes
Initialization
HiRadixCache.__init__() must detect CP shared KV mode and construct the controller adapter.
It should also enforce:
- CP shared KV + HiCache storage backend is unsupported and must fail during server argument validation or cache initialization.
- CP shared KV + HiCache is valid only for
NSATokenToKVPoolin this stage. - CP HiCache mode is enabled from
CPSharedPagedTokenToKVPoolAllocatoror an explicitCpSharedKVLayout, not merely because the device pool isNSATokenToKVPool.
Backup State
Introduce helpers inside HiRadixCache:
def _node_backuped(self, node: TreeNode) -> bool:
if self._uses_cp_hicache:
return node.host_len > 0 and node.cp_hicache is not None
return node.host_value is not None
def _node_host_len(self, node: TreeNode) -> int:
if self._uses_cp_hicache:
return node.host_len
return len(node.host_value)
def _node_host_evict_indices(self, node: TreeNode) -> torch.Tensor:
if self._uses_cp_hicache:
return node.cp_hicache.host_indices
return node.host_value
Use these helpers in paths currently reading node.backuped, len(node.host_value), or node.host_value for host eviction/load-back.
Write Backup
Non-CP path remains unchanged.
CP path:
- Call controller CP write with full logical
node.value. - If write reports insufficient host slots, evict host entries until physical freed slots are at least the requested
required_host_slots, then retry. - On success, set
node.host_len = len(node.value)andnode.cp_hicache = metadata. - Keep
node.host_value is Nonein CP mode. - Add the node to
ongoing_write_throughif write-through acknowledgement is required. - Locking and metrics count logical tokens, not owned physical tokens.
_inc_hit_count() must use _node_backuped(node) instead of node.backuped, otherwise CP-backed nodes would appear unbacked because node.host_value remains None in CP mode and the write-through path would repeatedly call write_backup().
Split Node
_split_node() must split CP metadata when a backed-up node is split:
- Parent metadata keeps positions
< split_len. - Child metadata keeps positions
>= split_len, rebased by subtractingsplit_len. - Parent
host_len = split_len. - Child
host_len = old_host_len - split_len. - Host indices are split according to the owned positions mask, not by slicing first
split_lenentries.
Non-CP host_value slicing remains unchanged.
Load Back
Non-CP path remains unchanged.
CP path:
- Collect
nodes_to_loadwhile walking evicted ancestors. - Compute
host_hit_len = sum(node.host_len for node in nodes_to_load). - Apply
load_back_thresholdandmem_quotachecks tohost_hit_len. - Call controller CP load with
nodes_to_load. - If allocation fails, evict device logical tokens and retry.
- Assign full logical slices from returned
device_indicesto eachnode.valueusingnode.host_len. - Update
evictable_size_, locks, and metrics by full logical lengths.
The load_back_threshold check intentionally uses logical length in this first stage. Using local physical owned-token count would let different CP ranks make different load-back decisions. A physical-transfer threshold would require a CP-group reduction and is deferred.
Host Eviction
Non-CP path remains unchanged.
CP path:
- Host eviction candidate selection still uses radix logical nodes.
- Free only
node.cp_hicache.host_indicesfrom the local host pool. - Track two counts separately: physical freed slots for allocator retry progress, and logical evicted tokens for metrics/debug output.
- Clear
node.host_lenandnode.cp_hicache. - Remove the node from the radix tree as the existing host eviction path does.
Host eviction for CP write retry must progress by physical freed slots, not by logical host_len. A node can have a large logical host_len but zero local host_indices on a rank. Counting that logical length as eviction progress could stop the eviction loop without freeing the physical host slots needed by the retry.
Capacity Policy
Host memory remains per-rank physical capacity based.
The existing HostKVCache default sizing uses device_pool.size * hicache_ratio when hicache_size <= 0. Under CP shared KV, device_pool.size is already physical per-rank size, so this is the desired behavior.
Do not change host pool sizing to logical capacity. That would multiply host memory by CP size per rank and defeat the CP memory-saving goal.
Do not add a new CP-specific host ratio parameter in the first stage. Existing hicache_ratio and hicache_size remain the user-facing controls.
dev-hicache Fix Integration
Do not blindly cherry-pick the dev-hicache commits onto dev-cp-refactor. The CP branch already modified allocator/common/model runner code, and direct cherry-picks risk overwriting CP shared KV logic.
Manually port fixes in this order:
- Correctness fixes before CP HiCache integration:
- Load-back
mem_quotascheduling budget froma666ab552. - Paged allocator extend OOM accounting from
d6ca2fd0c, merged carefully with compute-owner lane eviction incommon.py. defaultdict(TreeNode)ghost-node fix frome008d2cf3for radix-like nodes.
- Load-back
- CP host HiCache integration from this design.
- HiCache performance fixes from
25b6a957c:- Incremental eviction heap.
- Cached timestamp in hot loops.
- Persistent all-reduce tensors.
- Async write acknowledgement thread.
The short bigram page lookup fix from upstream commit 7be96c8ce is a separate small correctness task and should be manually ported with a unit test.
The upstream SWA HiCache storage type registration is out of scope for GLM 5 NSA host-only integration.
Error Handling
Startup or initialization must fail with clear messages for unsupported combinations:
- CP shared KV + HiCache storage backend.
- CP shared KV + HiSparse NSA pool.
- CP shared KV + non-NSA KV pool in HiCache host integration.
Runtime allocation failure handling follows existing HiCache behavior:
- Host write allocation failure returns
None; HiRadixCache evicts host entries and retries. - Device load allocation failure returns
None; HiRadixCache evicts device entries and retries. - If retry still fails,
load_back()returnsNoneand the request recomputes the prefix as today.
For CP metadata consistency failures, fail loudly with assertions in unit-testable helper methods. Examples:
metadata.logical_lenmust match the node logical length.len(metadata.owned_positions) == len(metadata.host_indices).- split lengths must be within
[0, logical_len]. owned_positionsmust be sorted and in range.owned_positionsandhost_indicesmust be CPU tensors before being stored onTreeNode.
Testing Strategy
Use SGLang unit-test conventions for new tests:
- Place tests under
test/registered/unit/. - Inherit from
CustomTestCase. - Register CPU tests with
register_cpu_ciwhere possible. - Avoid launching a server for metadata and controller mapping tests.
Required unit coverage:
CpHiCacheNodeMetadata.split():- page-aligned split.
split_len == 0.- split with no owned positions on this rank.
- split where owned positions are on both sides.
- invalid split values raise errors.
- Controller CP write mapping:
- full logical locs produce only owned host allocation.
- physical device indices passed to host transfer match
CpSharedKVLayout. - zero-owned node returns metadata without allocating host slots.
- zero-owned node still produces one completed logical write acknowledgement if the node enters
ongoing_write_through.
- Controller CP load mapping:
- full logical device locs are allocated.
- owned positions map to newly allocated logical locs, then to physical locs.
- zero-owned metadata skips transfer but returns full logical locs.
- zero-owned metadata still produces a completed logical load acknowledgement or producer event.
- HiRadixCache metadata behavior:
_node_backuped()returns true for CP nodes withhost_lenand metadata._inc_hit_count()uses_node_backuped()and does not repeatedly write back already CP-backed nodes._split_node()splitshost_lenand CP metadata correctly.load_back()useshost_lenrather thanlen(host_value)in CP mode.- host eviction frees owned host indices and uses physical freed slots as retry progress.
- Server args/config validation:
- CP shared KV +
hicache_storage_backendfails fast.
- CP shared KV +
Required regression checks:
- CP shared KV layout tests.
- Radix cache unit tests.
- Scheduler prefill-adder tests covering
mem_quota. - New CP HiCache metadata/controller tests.
Optional manual GPU smoke:
- Launch GLM5/NSA CP shared KV with HiCache host-only.
- Use repeated-prefix requests to trigger backup, device eviction, and host load-back.
- Confirm no logical/physical index assertion failures and no host/storage unsupported path is entered.
Implementation Order
- Add tests for existing correctness fixes that are currently missing from the CP branch.
- Manually port load-back
mem_quota, allocator OOM accounting, anddefaultdict(TreeNode)fixes. - Add CP+storage fail-fast validation.
- Add
CpHiCacheNodeMetadataand unit tests. - Add CP adapter methods in
HiCacheControllerwith mock unit tests. - Wire
HiRadixCachebackup, split, load-back, and host eviction to CP metadata helpers. - Run CPU/unit regression tests.
- Manually smoke test GPU CP+HiCache host-only if hardware/model access is available.
- Port performance fixes from
25b6a957cas separate patches with regression tests.
Open Follow-Up Work
These items are deliberately deferred:
- CP-aware HiCache storage key and prefetch design.
- Support for File/NIXL/Mooncake/EIC/HF3FS storage backends under CP HiCache.
- Independent CP-specific host ratio configuration.
- Decode-side CP/shared KV HiCache support.
- Upstream UnifiedRadixTree or HybridCacheController migration.
Deferred Performance Follow-Up
After host-only CP HiCache correctness lands, port HiCache performance work from
25b6a957c as separate changes with their own tests. Split cached timestamp use,
incremental eviction heap maintenance, persistent all-reduce tensors, and async
write acknowledgement into independent patches. Each patch must preserve CP
zero-owned write/load no-op acknowledgement cardinality and must rerun the CP
HiCache unit regression commands from the implementation plan.