feat(connector): support vLLM hybrid memory allocator (SupportsHMA)#393
feat(connector): support vLLM hybrid memory allocator (SupportsHMA)#393moreWax wants to merge 2 commits into
Conversation
Hybrid-attention models (sliding-window + full attention, e.g. Gemma-style 5:1 layouts) run under vLLM's hybrid KV cache manager, which requires the KV connector to subclass SupportsHMA — without it vLLM disables the hybrid manager and SWA models pay full-window KV allocation. Builds on and supersedes novitalabs#202 (thanks @xiaguan), reworked for vLLM 0.22+ semantics observed live: - per-group block lists are POSITIONAL and null-padded (the null block fills out-of-window SWA positions); alignment is by position, not by suffix, and group counts are model-derived (a 48-layer 5:1 hybrid yields 6 groups with the full-attention group LAST) - loads: one RPC per kv_cache_group (each group's layers get that group's destination block ids). The server consumes a query lease on first use, so the scheduler mints one lease per group; a failed mint degrades to reporting that group's blocks as load errors (vLLM recomputes them) - destination lists are padded with the null block id up to the lease length (vLLM trims full-prompt hits by one block for logits; the null block is never read, so the extra DMA is harmless and the server's len(lease) == len(destinations) contract holds) - saves: a position is stored only when EVERY group still holds a live, hash-matching block (checked via bind_gpu_block_pool) — slid-out SWA blocks whose slots were reused can never poison the store, and every stored hash is complete across all layers - namespace gains a kv-group-signature factor; cross-layer registration is forced off for multi-group; heterogeneous per-group block sizes are rejected loudly (needs per-group hash granularity — future work) - request_finished_all_groups implemented; world_size stays the true worker count (post-novitalabs#346 the server seals topology from registered layers, so novitalabs#202's world_size * num_groups is no longer needed) Validated live on a 48-layer 5:1 SWA hybrid and a dense model under vLLM 0.22.1: cross-restart prefix reuse with byte-identical outputs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds vLLM Hybrid Memory Allocator (HMA) compatibility to the PegaFlow vLLM connector by making load/save intent metadata group-aware (per kv_cache_group) and updating both scheduler- and worker-side logic to correctly handle multi-group hybrid layouts (e.g., sliding-window + full attention).
Changes:
- Implement HMA support in
PegaKVConnectorviaSupportsHMA, including per-group load RPCs and multi-group save semantics. - Extend connector metadata (
LoadIntent,SaveIntent,PegaConnectorMetadata) to carry per-group block ids and leases plus layer→group mapping. - Add/extend tests covering multi-group behaviors like partial group failures, null-slice lease release, and positional save alignment across groups.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| python/pegaflow/connector/init.py | Declares SupportsHMA, wires kv-cache-group config into scheduler connector, and adds group signature + block size validation for safe HMA operation. |
| python/pegaflow/connector/common.py | Updates intent metadata structures to be group-aware; adds namespace isolation factor for group layout signature. |
| python/pegaflow/connector/scheduler.py | Implements scheduler-side HMA behavior (group parsing, per-group leases, positional multi-group save cutting with pool validation and pinning). |
| python/pegaflow/connector/worker.py | Implements worker-side per-group load tracking and HMA-aware save batching per layer/group. |
| python/tests/test_combine_hashes.py | Updates existing tests to the new group_block_ids shape and adds extensive hybrid/HMA behavioral coverage. |
| python/tests/test_connector_fault_tolerance.py | Adds tests for HMA-specific fault tolerance (partial group failures and null-slice lease release). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| try: | ||
| blk = pool.blocks[block_id] | ||
| except (IndexError, TypeError, AttributeError): | ||
| return "dead" |
There was a problem hiding this comment.
Fixed in 8ff0b0e — KeyError added to the except tuple so dict-like pools classify missing ids as dead instead of aborting the intent cut.
| "[PegaKVConnector] req=%s multi-group save skipped: GPU block pool not bound", | ||
| req_id, | ||
| ) | ||
| self._next_stored_block_idx[req_id] = saveable_block_idx |
There was a problem hiding this comment.
Fixed in 8ff0b0e — the pool-unbound branch no longer consumes the cursor; the range is retried once the pool binds.
| g_idx = layer_to_group.get(layer_name, 0) | ||
| if g_idx >= len(save_intent.group_block_ids): | ||
| g_idx = 0 | ||
| layer_ids = save_intent.group_block_ids[g_idx] | ||
| layer_hashes = save_intent.block_hashes |
There was a problem hiding this comment.
Fixed in 8ff0b0e — an out-of-range group now warns and skips the layer; no more silent remap to group 0.
| failed_ids_by_req: dict[str, list[int]] = {} | ||
| states_by_req: dict[str, list[PyLoadState]] = {} | ||
| launched: list[tuple[str, PyLoadState, int, list[int], set[str]]] = [] |
- classify dict-like pool misses (KeyError) as dead instead of raising mid save-intent cut - keep the save cursor when the GPU block pool is not bound yet: the branch lacks safety information, so retry once it binds instead of permanently skipping the range - never remap an out-of-range layer group to group 0 on save (wrong physical blocks under valid hashes); warn and skip the layer - drop dead states_by_req tracking from start_load_kv Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Addressed all review feedback in 8ff0b0e: dict-like pool misses (KeyError) classify as dead; the save cursor is kept (not consumed) when the block pool is not yet bound; an out-of-range layer group now warns and skips instead of silently remapping to group 0; dead |
|
Thanks for the detailed writeup — the positional multi-group semantics analysis (null-block padding, FA-group prefix accounting, packed Before we go deeper on review, could you share the concrete motivation behind this?
Context for the questions: we deliberately kept the connector single-group so far because HMA support carries real ongoing cost — per-group lease mints as sequential blocking RPCs in the scheduler step, dependence on vLLM block-pool internals (packed hash layout, ref-pin APIs) that drift across versions, and ref-pinning GPU blocks across async saves, which works against the memory savings SWA is supposed to provide. Whether that cost is worth carrying long-term depends on a concrete production need rather than API completeness. Also note: per repo policy, connector-visible changes need the |
What
Adds vLLM hybrid memory allocator (HMA) support to
PegaKVConnector: models that mix sliding-window and full attention (Gemma-style 5:1 layouts and similar) currently cannot use PegaFlow at all — vLLM refuses to combine a non-SupportsHMAconnector with the hybrid KV cache manager, and disabling the manager makes SWA models allocate full-window KV.Closes #228. Builds on and supersedes #202 — thanks @xiaguan for the groundwork; this rework exists because the on-GPU group semantics turned out to be different from what #202 assumed (details below).
Design
Observed semantics on vLLM 0.22.1 (
KVCacheBlocks,SlidingWindowManager, and the in-tree HMA referenceSimpleCPUOffloadConnector):query_prefetchover the already-resident range); a failed mint degrades to reporting that group's real destinations as load errors, which vLLM recomputes.len(lease hashes) == len(destinations)holds.bind_gpu_block_pool, with three-state classification: valid positions are ref-pinned untilfinished_sending(the save copy is asynchronous — without the pin, a sliding-window block can slide out and be reused mid-DMA, storing foreign bytes under a valid hash); transient positions (allocated but not yet sealed/hashed) stop the cursor and retry next round instead of punching permanent holes in the hash chain; dead positions (slid out / reused — pool hash carries vLLM's 4-byte group-id suffix, stripped and verified) are skipped for all groups together so every stored hash is complete across all layers.finished_recvingonly after its in-flight group states drain (a premature duplicate report trips vLLM's scheduler asserts). Fully-out-of-window SWA slices (all-null destinations) skip their RPC and release the minted lease.FullAttentionSpec+SlidingWindowSpecare accepted — Mamba/SSM or cross-attention state must not be saved/loaded as positional KV. Single-group models are untouched (including MLA page-first striping, which stays on the legacy path).request_finished_all_groupsis implemented;world_sizestays the true worker count (after fix(core)!: seal instance layer topology from registered layers #346 the server seals topology from registered layers, so fix(connector): align hybrid save window across kv groups #202'sworld_size * num_groupsis no longer needed).Validation
finished_sending, exactly-once reporting under partial group failure, all-null-slice lease release, and partial-accept destination padding.test_vllm_e2e_correctness.pygate (no/data/models/Qwen3-4Bhere); happy to iterate if CI or a reviewer run surfaces anything.Limitations / future work
🤖 Generated with Claude Code