Conversation
Shunkangz
requested review from
Ying1123,
alphabetc1,
hanming-lu,
hnyls2002,
huangtingwei9988,
hzh0425,
ispobock,
merrymercy,
xiezhq-hermann and
yizhang2077
as code owners
August 5, 2026 05:34
45 tasks
Shunkangz
force-pushed
the
unified_kv_l3
branch
from
August 27, 2026 02:54
577b567 to
6ab5177
Compare
Introduce --hicache-storage-key-scheme {rank-suffix, canonical-grid}. Under
canonical-grid, L3 storage keys carry a topology-free canonical cell
coordinate ({ns_digest}_L{i}[_H{j}]) instead of the per-backend
rank/pp/cp suffixes, so keys name what an object holds (a model-global
layer-range x kv-head-range rectangle of one page) rather than who wrote
it. The namespace identity is an out-of-band msgspec descriptor (JSON file
via --hicache-storage-namespace-descriptor, or derived from the deployment);
its digest prefixes every key, so mismatched descriptors partition into
disjoint keyspaces instead of colliding.
v1 scope: one cell per rank per page (grid == deployment shard shape), so
objects stay byte-identical to the rank-suffix scheme; file and mooncake
backends; plain KV pools only. Rank-replicated (MLA-family) pools share
keys across TP sizes immediately; PP partitions can no longer collide
(absolute layer-group index replaces pp_rank). Finer grids (head/layer
fan-out, subsuming tp_lcm_size split-heads), CP, draft pools, and hybrid
side pools are rejected fail-fast at attach with the remedy in the error.
Design: DESIGN_l3_canonical_shard_grid.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per review direction, the staging machinery moves out of MooncakeStore
into a CellAdapter class in hicache_key_scheme.py, so other L3 backends
(nixl/eic/...) can serve partitioned cell-v1 namespaces without
reimplementing it. The adapter owns everything backend-independent:
- per-cell key fan-out (cell_keys; single _k object for rank-replicated
cells, _k + _v for sharded ones) in the pools' exact slab order,
- arena-sized sub-batching (sub_batches; arenas reused from offset 0),
- the two pinned staging arenas (one per IO direction; skipped entirely
when every slab is pool-contiguous), with an overridable _alloc_arena
seam and an optional register_buffer hook for RDMA transports,
- gather / read_metas / scatter plumbing to the host pool primitives.
A backend integrates by supplying only its pointer-based batch put/get;
mooncake keeps just its own concerns (exists-filter, group ids, metrics,
dispatch). Behavior is unchanged.
Also: drop DESIGN_l3_canonical_shard_grid.md from the branch (PR ships
code + description only) and the two dangling references to it; guard
sub_batches against empty batches (was a latent ValueError on all-direct
pools). Both found by adversarial review.
Tests: TestCellAdapterStaging drives the adapter exactly as a backend
would — raw pointers, a dict as the store — covering the no-arena
all-direct case and a staged cross-layout round trip with one-page
sub-batches (cell_arena_mb=0). 53 tests pass in the dev container.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rename the scheme's vocabulary per review feedback — "canonical" and
"cell" were jargon; the names now say what things are:
--hicache-storage-key-scheme unified (was canonical-grid)
KVCacheLayoutAdapter (was CellAdapter)
UnifiedKVPlan / plan_unified_kv (was CanonicalCellPlan /
plan_canonical_cells)
build_unified_suffixes (was build_canonical_cell_
suffixes)
unified_suffix / unified_layer_ranges / (was canonical_*)
unified_head_ranges
object_layout "unified-v1" (was "cell-v1")
gather/scatter_unified_chunks, (was *_cells_canonical,
get_unified_chunk_meta cell_read_metas)
unified_zero_copy / unified_bytes_per_page (was cells_all_direct /
cell_bytes)
staging_buffer_mb knob, staging_set/get (was cell_arena_mb, arena_*)
Prose, log lines, and error messages follow ("the unified key scheme
..."), with the per-page pieces now called chunks and the arenas called
staging buffers. Pure rename — no behavior change; keys and digests are
unaffected except object_layout's constant, which is an identity string
of the unreleased scheme. Pre-existing unrelated uses of "canonical"/
"arena" (CP resolution, kv_vmm_backing) are untouched. 53 tests pass in
the dev container.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… order
The adapter stored every chunk as (head, layer, token, dim) per K/V half --
the byte order of a host layout that does not exist. Measured consequence: an
MHA chunk decomposed into head_dim-sized fragments (256 B at D=128 bf16) on
every shipped host layout, so all of them staged 100% of their bytes through
the pinned buffer and none could be picked to avoid it.
Three changes:
1. Serialize a chunk as (layer, token, head, dim) instead -- page_first_direct's
own page block. A chunk covering the rank's whole head shard is then a single
contiguous range and transfers straight from pool memory, for any layer
partition including a model's short trailing chunk at a prime layer count
(DeepSeek-V3's L=61). Cross-PP reuse needs no padding and no special case.
MLA is unaffected: no head axis, and its order was already
(layer, token, dim) -- the flip generalizes the case the scheme already got
right instead of trading it away.
2. Support only page_first and page_first_direct. _page_kv_view_unified drops
to two branches per pool; layer_first and page_head fail fast at plan time.
3. Keep the host layout in the namespace identity: object_layout becomes
"unified-v2:{layout}", so page_first and page_first_direct never share
objects (their page blocks are byte-permuted), and adapter chunks never
collide with the raw-layout objects an unpartitioned deployment writes for
the same layout -- which the previous layout-neutral constant could do when
a knob happened to be a no-op on the grid.
Load path: for the zero-copy configurations get_unified_chunk_meta resolves
every read target to an address inside the pool, so the transport writes L3
bytes exactly where the H2D kernel will read them, the scatter is a no-op, and
the device transfer runs unchanged -- no second copy between CPU and GPU.
Pinned by test_load_lands_in_the_pool_with_no_second_copy.
What still stages: cutting the kv-head axis. A head subgroup breaks contiguity
at the token axis on both layouts, so cross-TP GQA reuse keeps today's
conversion. A single linear order can make only one axis outermost; this one
buys the layer axis -- the axis page_first_direct stores, and the only axis MLA
has. Removing that copy too needs an offset-computing H2D kernel, not a layout
change.
Adds test/registered/unit/mem_cache/test_hicache_unified_layout_perf.py
(CPU, base-a-test-cpu, ~12 s): prices gather/scatter and the
batch_set_v1 / batch_get_v1 round trip per layout x fan-out, sweeps the
head-split penalty, and proves the span-based copy-free alternative reproduces
the staged bytes exactly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Shunkangz
force-pushed
the
unified_kv_l3
branch
from
September 1, 2026 03:29
6ab5177 to
425e49b
Compare
Contributor
Author
|
/tag-and-rerun-ci |
Shunkangz
force-pushed
the
unified_kv_l3
branch
from
September 3, 2026 07:31
dd35e9f to
a831897
Compare
Shunkangz
force-pushed
the
unified_kv_l3
branch
3 times, most recently
from
September 4, 2026 03:16
28379ee to
ca2801d
Compare
Shunkangz
force-pushed
the
unified_kv_l3
branch
from
September 4, 2026 07:23
a15f7aa to
3c4071a
Compare
Shunkangz
force-pushed
the
unified_kv_l3
branch
from
September 4, 2026 07:50
3c4071a to
09e9463
Compare
Resolve hicache_hook.handle_hicache: main added the external-linker mode gate where we added L3 key-scheme validation. Ours runs first -- it is a no-op for the default rank-suffix scheme, so external-linker configs are unaffected, while a --hicache-storage-key-scheme unified flag still errors instead of being silently ignored under a mode that has no L3.
Contributor
Author
|
/tag-and-rerun-ci |
1 similar comment
Contributor
Author
|
/tag-and-rerun-ci |
Contributor
Author
|
/rerun-failed-ci |
Shunkangz
force-pushed
the
unified_kv_l3
branch
from
September 5, 2026 13:51
e0e33d8 to
9d1b7fa
Compare
Contributor
Author
|
/rerun-failed-ci |
1 similar comment
Contributor
Author
|
/rerun-failed-ci |
Conflict: python/sglang/srt/server_args.py main's "Config Round 6.x" refactor (sgl-project#38046-sgl-project#38113) moved every field declaration out of ServerArgs into per-namespace classes under arg_groups/fields/, shrinking server_args.py from 4455 to 1087 lines. The branch's only change to that file was the hicache_storage_key_scheme declaration, so the file takes main's version and the field moves to arg_groups/fields/memory.py, keeping its position after hicache_storage_prefetch_retry_max_attempts. NS("memory") is dropped: the declaring module is now the namespace. The field is deliberately NOT added to field_order.py. That record freezes ServerArgs' positional constructor signature; an unlisted name sorts after every listed one, which is the only backward-compatible slot for a new field. Consumers needed no change: hicache_hook.py already reads through resolving_view() and cache_controller.py through get_memory(), both of which are unaffected by where the declaration lives. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Brings in sgl-project#38280, which removes the stale is_cp_v2_active mock patches from test_deepseek_nextn_mm_embed.py. The previous sync landed at e24e31e, inside the 86-minute window where sgl-project#36228 had removed is_cp_v2_active from deepseek_nextn.py but the test had not yet been updated.
Brings in sgl-project#38314, which repairs the DSpark dp-tier test fixture after sgl-project#34919 added self._num_token_non_padded to DraftBlockProposer. PR CI tests refs/pull/N/merge against live main, so it picked up sgl-project#34919 during the 100-minute window before sgl-project#38314 landed.
Contributor
Author
|
/rerun-failed-ci |
1 similar comment
Contributor
Author
|
/rerun-failed-ci |
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
HiCache's L3 tier names every stored KV object by who wrote it: each backend appends its own rank/topology suffix
(mooncake {hash}_{tp_rank}_{pp_rank}_k, file {hash}_{model}_{tp_rank}_{tp_size}_{pp_size}_{pp_rank}_cp{r}_{s}.bin, nixl/eic/hf3fs/simm/umbp each different again). This has two costs:No cross-topology reuse, by construction. A page written by one parallel configuration is invisible to any other, even when the reader needs exactly the bytes that are already stored (MLA KV is bit-identical on every TP rank; a TP4 reader's head shard is exactly half of a TP8 writer's two objects).
Modifications
New Server Args
--hicache-storage-key-scheme unifiedswitches L3 keys from rank-suffixed names to a topology-free coordinate:page_hashnamespace_digestKVCacheNamespacedescriptor: model id, logical KV dtype, page size, head grid, layer grid, object byte order, optional numerics id. Any mismatch lands in a disjoint keyspace, so deployments miss instead of colliding.L{start}-{end}H{j}Use
--hicache-storage-backend-extra-configto partition a page into finer-grained chunks. The example below makes 1 kv head x 6 layers the smallest independently stored unit:Both entries are fleet-wide agreements, not local tuning options: they fix chunk boundaries, and boundaries are part of the namespace identity. Every member of a fleet that should share cache must pass the same values. Pick
head_group = total_kv_heads / lcm(the fleet's attn-TP sizes).Layout Support
The L2 host memory pool supports four layouts:
With the unified scheme enabled we serve
page_first_directonly, overriding--hicache-mem-layout. It is the layout friendliest to both head and layer partitioning: a chunk is one contiguous run, so the transport moves it with a single descriptor instead of one per (layer, token). With a head partition the page block becomes head-group-major:A layer partition alone leaves the data naturally contiguous, so the copy engine still works. A head partition fragments it, which the copy engine handles poorly, so the kernel path is selected automatically. To avoid a host-side repack we added a transfer kernel that converts KV cache between GPU and CPU directly.
Follow Up
Accuracy Tests
We do a small E2E test based on Qwen3-8B-FP8 model with 36 layers and 8 KV heads and compare the accuracy on GSM8K dataset.
The launch command
With unified kv cache layout, we can achieve the same 94.5% accuracy as origin.
Speed Tests and Profiling
Measured on one B200 over PCIe Gen5 x16 — 61 layers, head_dim 128, page_size 64, 2048 tokens (~488 MiB per direction at H=8), bf16.
His kv heads,HGhead groups, andrun Bthe resulting contiguous run per pass:All figures GB/s. Cutting the head axis costs little — at 32 CTAs the run size falls 8x with no measurable change, and even at 2 CTAs the loss is under 15%. What sets the bandwidth is the CTA count, which
block_quotacaps. We keep the default at 2, matching the existing kvcacheio kernels, so KV copies contend minimally with model forward for SMs; the cost is that they run well below link speed. Raiseblock_quotaper call site to trade SM occupancy for bandwidth.Checklist
Review and Merge Process
/tag-and-rerun-ci,/tag-run-ci-label,/rerun-failed-ciCI States
Latest PR Test (Base): ✅ Run #34124794944
Latest PR Test (Extra): ❌ Run #34124794759
Latest PR Test (AMD ROCm 7.2): ❌ Run #34124794947