Repository navigation
[None][feat] Keep hybrid speculative replay state in private KV cache buffers - #21324
Draft
VALLIS-NERIA wants to merge 24 commits into
Draft
VALLIS-NERIA wants to merge 24 commits into
VALLIS-NERIA wants to merge 24 commits into
Conversation
…on and SSM Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
Add an optional per-buffer reuse policy. A BufferConfig without one uses its layer's policy. An SSM buffer may set a different policy and then belongs to the SSM lifecycle of that policy while keeping its layer ID, so one SSM layer can span several layer groups. Attention buffers must use their layer's policy. Lifecycle registration and the storage layout are built per buffer, and per-layer storage attributes are keyed by (layer, lifecycle), so page index converters use the buffer's own lifecycle. Add getLayerGroupId(layerId, role) and its get_layer_group_id(layer_id, role) binding. The one-argument overload throws when a layer's buffers span several groups; layerGrouping lists such a layer under each group. Signed-off-by: Xiwen Yu <13230610+VALLIS-NERIA@users.noreply.github.com>
…By Agent) _remove_zero_size_buffers rebuilt SSM layer configs without their reuse policy, which turned PRIVATE and OPTIONAL SSM layers into REQUIRED ones whenever any layer had a zero-size buffer. Forward the layer policy; the retained buffers keep their own policies. Describe how the layer-group helpers handle an SSM layer listed under several groups and cover them with a split layer. Signed-off-by: Xiwen Yu <13230610+VALLIS-NERIA@users.noreply.github.com>
Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
Signed-off-by: Jiagan Cheng <jiaganc@nvidia.com>
…ent) Pool sizing treats every lifecycle as elastic, so a PRIVATE SSM pool got the same ratio-derived slot count as the REQUIRED SSM pool, although its pages never enter the radix tree and only living caches use them, one slot each. Add KVCacheManagerConfig::maxLiveKvCaches (max_live_kv_caches), an upper bound on living (active or suspended) caches. When it is set, createKvCache() rejects a cache beyond the bound, and the PRIVATE SSM lifecycle gets a pool group of its own with a fixed GPU capacity of ceil(maxLiveKvCaches / maxUtilForResume) slots. ratioToSlotCountList() takes per-pool-group fixed slot counts out of the quota before splitting the rest by ratio. Construction, resize/adjust and tuner targets pass them, so adjust() never resizes the fixed group and the tuner does not chase it. A GPU quota that cannot hold the fixed group plus the other groups' minimum slots fails at construction, and beam widths above one are rejected. initialPoolRatio keeps one entry per layer group; the hot tier ignores the fixed group's entry. Other PRIVATE lifecycles stay ratio-sized, cold tiers are unchanged, and without the field the layout and slot counts are unchanged. The Python binding, stub and dataclass field list gain the field; no production caller sets it yet. Signed-off-by: Xiwen Yu <13230610+VALLIS-NERIA@users.noreply.github.com>
…By Agent) A recurrent buffer with its own reuse policy places one SSM layer in two layer groups. get_layer_group_id() and layer_grouping raise for such a layer, so a hybrid V2 manager could not be constructed once a Mamba layer carried a PRIVATE buffer. Re-apply the buffer-API consumer forms from 25c879c, b91d86e, c624c86 and 2ffc2e5 (reverted by 256ac3b) for the sites the hybrid managers reach: - KVCacheManagerV2 counts pools and builds its layer, pool-mapping, page-table, event and stats metadata from buffer_grouping, resolving a split layer's execution group through one of its buffers. It again rejects attention layers whose K/V and scales need separate page tables and SWA scratch layouts that need split slot indices. - MambaHybridCacheManagerV2 resolves the recurrent-state group and a Mamba layer's execution group through SSM_STATE and describes a group without SSM_STATE by its own role. - Disaggregated transfer gives view-less groups no layers or slots and selects the Mamba state group by its pool views. Connector, sharing and non-hybrid sites keep the legacy API. Without a split layer every lookup returns the same group as before. Restore the three split-layer tests the revert deleted and add a test that a hybrid V2 manager whose Mamba layers carry a PRIVATE buffer constructs, serves state indices and builds its disaggregated page table. Signed-off-by: Xiwen Yu <13230610+VALLIS-NERIA@users.noreply.github.com>
…By Agent) Mamba2 speculative-decoding replay history (old_x, old_B, old_dt, old_dA_cumsum, PNAT and the active buffer index) lived in standalone tensors indexed by the SSM state slot. A V2 suspend can move a request's SSM page to another slot on resume, and the history stayed behind. MambaHybridCacheManagerV2 gains a generic replay declaration: - _get_replay_layout() lets a model manager declare per-layer replay roles; _replay_reuse_policy() makes them PRIVATE (the default) or lets them inherit the layer's REQUIRED policy and share the state slot. - PRIVATE roles become PRIVATE buffers of the existing SSM layers. The manager resolves replay_layer_group_id, fills replay slots from get_ssm_block_base_index() next to the state slots, and serves them through get_replay_state_indices() and cuda_replay_state_indices. Without a PRIVATE group replay slots equal state slots, so GDN, KDA and the V1 managers keep their behaviour. - A request's replay slot is reset the first time it gets one, including generation-only dummies; free and preemption clear that mark, suspend does not, so resumed history is never reset. - max_live_kv_caches is set to the IndexMapper capacity, whose computation is hoisted into _compute_index_mapper_capacity(). _create_kv_cache() returns None at the bound instead of letting the native manager raise. Disaggregated context servers declare no replay buffers. - The fixed PRIVATE pool is a separate quota intercept, in the manager's quota and in the KV-cache estimate, not part of the per-state-slot bytes. A small GPU quota is raised so the fixed pool fits next to the other pools' minimum slots. - Replay roles are disaggregation-ignored and the PRIVATE group is left out of recurrent-cache movement logging. Mamba2 kernels and metadata take a separate replay slot: - replay_selective_state_update() accepts replay_batch_indices (default: state_batch_indices) and strided PNAT / active-buffer rows; the precompute kernels address only replay buffers through it, and the main kernels address the state by state slot and the history by replay slot. - Replay work items gain a replay-slot field; the builders read PNAT and the active buffer through the replay slots. The GDN all-layer commit accepts the wider rows. - Mamba2Metadata keeps a static replay_state_indices buffer, filled in prepare() (aliasing state_indices without a PRIVATE group), and update_mamba_states() passes it to the acceptance update through MambaAcceptanceBatch.replay_indices. NemotronHybridCacheManager declares ReplayHistory's roles as PRIVATE, with a 16-byte metadata row on the first local layer. GDN and KDA are unchanged. Tests cover the kernel with replay slots that differ from state slots, the work-item builders, the static metadata buffer, the PRIVATE layout and group, coupled declaration, fresh-page reset of generation dummies, the living-cache bound and its Python guard, exclusion of replay history from prefix reuse, and a suspended request whose SSM slot is taken while it is suspended and that then decodes bit-identically to one never suspended. Signed-off-by: Xiwen Yu <13230610+VALLIS-NERIA@users.noreply.github.com>
Qwen3.5 and Qwen4Exp kept Gated-DeltaNet speculative-decoding replay state in standalone tensors indexed by the SSM state slot. A V2 suspend can move a request's SSM page to another slot on resume, and the history stayed behind. Both GDN managers declare their replay buffers through the manager's replay hook (shared helpers in modules/fla/cache_manager.py): - Triton double-buffer mode (default): old_x, old_B and old_dt on every local GDN layer plus a 16-byte metadata row on the first local layer ([PNAT, active buffer, ring start, padding]) are PRIVATE buffers with their own replay slots. The unused old_dA_cumsum is no longer allocated. - FlashInfer ring mode (TRTLLM_USE_GDN_FLASHINFER_REPLAY): FlashInfer addresses the rings by the recurrent-state index, so k_ring, u_ring, g_ring and the metadata row inherit the layer's REQUIRED policy, share the recurrent-state slot and index, and move with the SSM page. FlashInfer's strided mode reads the block-strided ring views directly. - A disaggregated context server declares no replay buffers; there, and wherever no pool is bound, the state allocates its history per state slot as before. V1 managers keep their own buffers, and their replay slots are the state slots. - The cache estimate counts the Triton history as the fixed PRIVATE intercept and the rings as bytes of every recurrent-state slot. The kernels address the history by a replay slot of its own: - fused_recurrent_gated_delta_rule_cached_replay_update() takes replay_indices (default: state_indices), explicit history slot strides and element strides for PNAT and the active buffer, instead of requiring contiguous tensors. - commit_gdn_cached_replay_history_layers() reads the replay slot from full-width work items, takes explicit layer and slot strides of stacked history, or a per-layer descriptor table from build_gdn_history_descriptors() for views that are not evenly spaced. - _advance_replay_state() and _advance_ring_start() take replay indices; GDNReplayState.update() reads MambaAcceptanceBatch.replay_indices, and reset_replay_slots() clears fresh replay slots. - The GDN mixer and the experimental Qwen4Exp engine pass mamba_metadata.replay_state_indices; the engine's replay checks size the bookkeeping and history by the replay slots, and its cached-replay op wrapper accepts replay_indices. Tests cover both kernels with replay slots disjoint from the state slots in strided and descriptor-addressed buffers with strided bookkeeping (bit-exact against the coupled-index run), FlashInfer on block-strided rings and on the manager's ring views (bit-exact against contiguous rings), the PRIVATE layout, group and pool size, max_live_kv_caches, the cache estimate, fresh-page reset of generation dummies and exclusion from prefix reuse for Qwen3.5 and Qwen4Exp, the coupled ring layout, and a request suspended while its SSM slot is taken that then decodes bit for bit like one never suspended, in Triton and ring mode. Signed-off-by: Xiwen Yu <13230610+VALLIS-NERIA@users.noreply.github.com>
…ent) KimiK3 and Glm5Next kept every KDA fused-verify replay buffer (the conv windows, the k/g/v/beta caches or the draft records, and the accepted-draft count) in the REQUIRED SSM slot. Every snapshot slot therefore carried replay bytes, and because the replay state did not follow a request, a V2 suspend folded the pending drafts into the recurrent state and the resume re-seeded the replay caches; later verifies matched an unsuspended run only to the kernel's accuracy. The KDA replay buffers now go through MambaHybridCacheManagerV2's replay hook as PRIVATE buffers of the SSM layers: - KDAReplayState.replay_layout() declares the roles, with the count row on the first local layer, and bind_replay_pool() takes the manager's views of the replay group, which has a slot count of its own. This replaces bind_kda_replay_pool and the managers' buffer-config, disagg-role and model-state overrides. - Acceptance recording (also by the host drafter), context and fresh-page resets, disaggregated seeding and the pending-draft commit address the replay buffers by replay slot and the recurrent and conv states by state slot. - A suspend no longer commits the pending drafts and a resume no longer re-seeds: the PRIVATE pages follow the request. A disaggregated generation request suspended before its transferred state seeded the replay caches is seeded when it resumes. Disaggregated context servers still declare no replay buffers. - The replay bytes are a fixed intercept of one replay slot per living cache instead of per-state-slot bytes; only the stochastic-rounding seed stays per state slot. - The enable_kv_pool_rebalance restriction is unchanged. The kernels take a replay slot of their own, defaulting to the state slot: - trtllm::kda_mtp_decode, kda_mtp_decode_fp8 and kda_mtp_commit_pending_drafts take replay_state_indices (schema, compile key, validation and kernel). The conv and replay caches and, on the packed path, the accepted counts are addressed by replay slot and no longer need to cover the recurrent-state rows. - The KDA Triton helpers (conv-window copy and store, draft-record replay, pending-draft commit) take replay indices. - KimiK3MambaMetadata keeps a static 16-byte-aligned copy of the generation requests' replay slots, and the KDA mixer passes the metadata's replay slots to prefill, decode and verify. Tests cover the op and the Triton helpers with replay slots that differ from the state slots, bit for bit against shared slots; the aligned metadata copy; for both managers the PRIVATE layout, group and pool size, the fresh-page reset of generation dummies, exclusion of replay state from prefix reuse and a suspended request whose replay state follows it; and the living-cache bound. A request suspended with pending drafts, whose SSM slot another cache takes meanwhile, now verifies bit for bit like one never suspended. Signed-off-by: Xiwen Yu <13230610+VALLIS-NERIA@users.noreply.github.com>
…y (By Agent) KVCM raised a GPU quota that could not hold the elastic pool groups' minimum slots, but rejected one that could not hold the fixed-capacity PRIVATE SSM group. The Mamba V2 manager worked around that by guessing KVCM's per-pool rounding in Python (_fit_fixed_replay_pool) and raising the quota itself. The fixed slot count is already the group's entry in mMinSlots, so drop the construction-time rejection and let the existing minimum-quota raise cover the fixed group too. Remove the Python workaround and its imports. The KDA byte-limit test goes back to its 16 MiB limits, and the tests that expected a rejection now check that the quota is raised and the fixed group keeps its full size. Signed-off-by: Xiwen Yu <13230610+VALLIS-NERIA@users.noreply.github.com>
… (By Agent) Clean up what the hybrid replay series left behind: - KVCacheManagerV2 no longer rejects SWA scratch-reuse layouts whose coalesced share is not a whole layer block per slot. The check came back with the buffer-API adoption from a reverted series, but it only concerns non-hybrid SWA configurations; its test goes with it. The rejection of split coupled attention buffers and its test stay. - Drop buffer_to_pool_mapping_dict, which nothing reads. - Build _layer_ids_by_group once (functools.cached_property) instead of on every access, including the per-request get_page_indices_by_layer_group path: the buffer grouping is fixed once the native manager exists. - The KV-cache estimate and the runtime decide whether a guard page is reserved with the same parse of TRTLLM_KV_GUARD_PAGE (_read_guard_page_setting). The estimate used to count any non-empty value, including ones the runtime ignores. - MambaHybridCacheManagerV2._build_cache_config no longer passes the unchanged cache tiers back into replace(). - MambaState.reset_slots documents what the manager calls on every state, and a MambaReplayState protocol describes what it calls on a state that holds declared replay buffers: bind_replay_pool(), reset_replay_slots() and reset_slots(..., replay_slots=...). - A suspend no longer commits KDA pending drafts, so the commit chain has no production caller. Remove KDAReplayState.commit_pending_drafts and the SSM views only it read, commit_kda_pending_drafts, replay_kda_token_records, store_kda_replay_conv_window, the kda_mtp_commit_pending_drafts launcher with its stand-in inputs, and the REPLAY_ONLY variant of the KDA MTP verify kernel with its replay_only launcher argument. The remaining kernel variants trace the same code as before. Remove the tests that exercised only these, and the suspend tests' checks that nothing was committed; the conv-window copy that disaggregated seeding uses keeps its separate-replay-slot test. Signed-off-by: Xiwen Yu <13230610+VALLIS-NERIA@users.noreply.github.com>
This branch has not been deployed
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.
Description
Stacked on #21088 (layer and buffer reuse policies). Until that merges, this PR's diff also shows #21088's commits; the commits listed below are this PR's own.
Motivation
Hybrid models with speculative decoding keep per-request replay state next to their recurrent (SSM) state: the inputs of the draft tokens accepted since the last checkpoint, which the verify kernels replay instead of writing a new checkpoint every step. Under KVCacheManagerV2 that state had three problems:
ReplayHistory(Nemotron) andGDNReplayState(Qwen3.5, Qwen4Exp) keptold_x/old_B/old_dt/..., the accepted-token count (PNAT) and the active double-buffer index in standalone tensors indexed by the SSM state slot. A V2 suspend can offload a request's SSM page and resume it into a different slot; the history stayed at the old slot, so the request continued from another request's (or a reset) history.Design
KVCM: a fixed-capacity PRIVATE SSM pool.
KVCacheManagerConfig.max_live_kv_caches(maxLiveKvCaches) bounds the living (active + suspended)KvCaches;createKvCache()rejects one beyond the bound. When it is set, the PRIVATE SSM lifecycle gets a pool group of its own with a fixed GPU capacity ofceil(max_live_kv_caches / max_util_for_resume)slots: PRIVATE pages never enter the radix tree and each living cache holds exactly one, so this keeps the group at or below the resume gate. Construction,adjust()/resize()and the tuner take the fixed group out of the quota first and split the rest by ratio;adjust()never resizes it, and the fixed count is the group's entry in the minimum slots, so a GPU quota too small for all floors is raised, as for the elastic floors. Beam widths above 1 are rejected while a fixed group exists. Without the field, layouts and slot counts are unchanged.Hybrid managers: replay state as KVCM buffers with its own index.
MambaHybridCacheManagerV2gains a replay declaration hook (_get_replay_layout(),_replay_reuse_policy()). Replay roles become buffers of the existing SSM layers, either PRIVATE (own lifecycle, own slot, own index) or coupled (inherit REQUIRED, share the state slot and index).get_ssm_block_base_index()next to the state slots, and carries them to the kernels through a staticMamba2Metadata.replay_state_indicesbuffer (CUDA-graph safe; it aliasesstate_indiceswhen there is no PRIVATE group) andMambaAcceptanceBatch.replay_indices.max_live_kv_cachesis set to the IndexMapper capacity (max_batch_size * pp_size * (2 if disagg or overlap) + reserved dummy slots + guard page), which already bounds every KvCache creation;_create_kv_cache()returnsNoneat the bound like a full IndexMapper. Disaggregated context servers declare no replay buffers.ceil(N / util)replay slots) in the manager quota and the KV-cache estimate instead of per-state-slot bytes.get_buffer_group_id/buffer_grouping, re-applying the adoption from [None][feat] Support layer and buffer reuse policies for attention and SSM #21088's history for these sites; connector, sharing and non-hybrid sites keep the legacy API.Per model family:
old_x,old_B,old_dt,old_dA_cumsumper layer + 16-byte PNAT/active-buffer rowreplay_selective_state_update(all precompute/main kernels), work-item buildersold_x,old_B,old_dtper layer + 16-byte metadata row (unusedold_dA_cumsumdropped)k_ring,u_ring,g_ring+ metadata rowinitial_state_indices; its strided mode reads the KVCM viewstrtllm::kda_mtp_decode*,kda_mtp_commit_pending_drafts, KDA Triton helpersThe KDA suspend-time fold of pending drafts and resume-time re-seed are removed, together with the pending-draft commit path that only they used (
kda_mtp_commit_pending_drafts, its replay-only kernel variant and Triton helpers); the disaggregated-transfer seeding is kept. V1 managers are unchanged; every kernel's replay index defaults to the state index.Commits
maxLiveKvCaches, fixed PRIVATE SSM pool group, beam rejection, bindingsmax_live_kv_caches, estimator; Mamba2MambaReplayStateprotocol, cached group layer ids, shared guard-page parsing; drops an attention-only SWA scratch check that the buffer-API adoption had brought backKnown limitations
kv_cache_config.pool_ratiomust now include an entry for the PRIVATE replay group (the hot tier ignores it, but the length must match the layer groups).max_batch_sizewith the overlap scheduler or disaggregation. With prefix reuse enabled this is smaller than before; without it, replay memory can grow up to about 2x.TRTLLM_DISAGG_ROLE=context(the existing KDA convention). A context server launched without it declares PRIVATE replay it never uses and bounds living caches by the IndexMapper capacity, so its early index release no longer admits extra requests while transfers are in flight; correctness is unaffected.Test Coverage
End-to-end (8x B300, sm_103 wheel): in progress, results will be added here. Planned runs: MTP GSM8K accuracy for Qwen3.5-35B-A3B and Qwen3-Next (Triton and FlashInfer ring replay) and Nemotron-3 Super NVFP4 TP8 with prefix reuse, plus Qwen3.5 and Nemotron runs with a small GPU KV budget and a host tier that force V2 suspend/resume (asserting suspended and resumed requests > 0).
Unit tests (B200,
umbriel-b200-043, on the integrated branch):kvCacheManagerV2*Test: 208 passed, 0 failed (fixed-capacity sizing, tuner, living-cache bound across suspend, beam rejection, quota raise).kv_cache_manager_v2_tests, kvcm2 integration, csa2, estimation, mixed precision, deepseek_v41 snapshots, dual pool, extra buffers): 892 passed, 14 skipped.test_mamba_cache_manager.py,test_mamba_model_managers.py,_torch/modules/mamba/): 2,026 passed, 2 skipped._torch/modules/kimi_kda,glm_kpool, Kimi/GLM modeling,test_kda_decode.py): 743 passed, 6 skipped.test_certified_cells[all_layer n=16 T=3 L=8 S=461 ...](cached-replay kernel); it did not recur in 42 targeted reruns or the final full run.reuse_status[private].completeis False and the reader starts from reset bookkeeping;max_live_kv_cachesequals the IndexMapper capacity and the Python guard returnsNone.PR Checklist
🤖 Generated with Claude Code