Skip to content

support hybrid attention for vllm connector && add integration tests - #257

Open
lpdink wants to merge 56 commits into
mainfrom
feature/vllm-hybrid-attention-rewrite
Open

support hybrid attention for vllm connector && add integration tests#257
lpdink wants to merge 56 commits into
mainfrom
feature/vllm-hybrid-attention-rewrite

Conversation

@lpdink

@lpdink lpdink commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

Rewrite the vLLM connector around per-group KV cache transfer, adding support for hybrid attention models (mamba/linear state layers + full attention, e.g. Qwen3.5) on vLLM 0.23.0+, together with a unit-test suite and a two-phase bit-exact end-to-end verification harness.

Supported scope:

  • Full attention models (e.g. Qwen2.5): single FullAttentionSpec group — the one-group case of the same code path.
  • Hybrid models (e.g. Qwen3.5): multiple MambaSpec groups + FullAttentionSpec group, covering the mamba_cache_mode none / align / all behaviors.
  • MLA models are covered by the same inheritance chain but have no dedicated e2e test yet; SWA is not supported yet (both planned as follow-ups).

vLLM version support matrix (e2e-verified)

vLLM's flash_attn backend shipped three KV cache layouts over time; the connector detects them from the tensor shape itself (never from version strings) and normalizes each into token-major transfer pointers, so all three eras work through the same Triton kernel with zero kernel changes:

vLLM KV layout Full attention Hybrid (mamba)
<= 0.22.1 (2, num_blocks, block, H, D) split K/V ✅ verified on 0.22.1 ❌ upstream scheduler asserts num_external_computed_tokens == 0 for mamba align mode; the connector fails fast at startup with a clear NotImplementedError pointing to vLLM >= 0.23.0
0.23.0 – 0.25.x (num_blocks, 2, block, H, D) split K/V ✅ verified on 0.23.0 ✅ verified on 0.23.0
0.26.0+ (num_blocks, H, block, 2D) packed ✅ verified on 0.26.0 ✅ verified on 0.26.0

0.24.x / 0.25.x share the 0.23.0 layout and connector API surface (verified against the vLLM source per tag), so they are expected-compatible although not e2e-run.

Note — no KV portability across vLLM upgrades: the saved byte layout differs between the split-K/V and packed eras, so KV cache written by one era must not be loaded by another. In practice instance_id isolation already guarantees this (a redeployed engine is a different instance); operators should simply not reuse an old instance_id across a vLLM major upgrade.

Motivation

The previous connector was written against vLLM <= 0.22.1 and assumed a single full-attention KV cache group. That data model is fundamentally unfriendly to hybrid models:

  • Hybrid models expose several kv_cache_groups, each with its own block table, block size and storage layout (token-granular KV for attention vs per-block opaque state for mamba/linear).
  • vLLM <= 0.22.1 has internal asserts that block external KV loads for mamba align mode entirely (see the support matrix above), and vLLM 0.23.0 changed the KV cache layout in a way that unblocks hybrid support.

Adapting the old implementation would have meant grafting a multi-group model onto single-group assumptions everywhere; a rewrite around "one group = one independent transfer unit" is simpler and strictly more general (a full-attention model is just the one-group case, there is no separate hybrid path).

Details

  • Every group gets its own KVCM location spec (tp{rank}_g{group}), its own block-table translation and its own transfer strategy:
    • attention groups: token-granular strided gather/scatter through the Triton kernel (handles both the contiguous full-attention layout and the block-strided hybrid layout, including padded pages);
    • mamba/linear/gdn groups: per-block verbatim byte copy; a manager block's last token selects the state block. align mode materializes a recurrent state only at segment boundaries, so the interior manager blocks of a request have attention KV but no state — that sparsity is published to the manager through per-block location_spec_group_names (groups full / attn), never encoded as a successful write, and the load side truncates an external match to the last state-complete block.
  • One KVCM cache key (hashed from token ids) owns the location specs of all groups of all ranks, so a manager block covers the same token range in every group.
  • Multi-version KV layout support: attn_kv_views normalizes each attention layer's paged cache (packed 4-D, or split K/V 5-D in either KV-first or N-first order) into token-major per-pointer views; split layouts contribute two transfer pointers per layer ([K0, V0, K1, V1, ...]), the interleaved N-first layout rides the kernel's existing strided path, and unrecognized layouts fail fast at startup. per_block_bytes is identical across layouts, so the manager, storage backends and transfer protocol are layout-agnostic.
  • Scheduler-side fixes discovered by the tests: full-prompt external hits are capped to leave >= 1 token to recompute (vLLM asserts num_new_tokens > 0), canceled saves no longer raise KeyError, and failed loads no longer enter a retry loop.

Changes

File(s) Purpose
kv_cache_manager/py_connector/vllm/v1_connector.py Connector rewrite: per-group parsing, block translation, scheduler state machine
kv_cache_manager/py_connector/vllm/data_transfer.py Per-group save/load tasks, group-major result flattening, per-block state-sparsity dispositions (abstain / transfer / fail)
kv_cache_manager/py_connector/vllm/metadata.py Per-group metadata passed scheduler -> worker
kv_cache_manager/py_connector/common/types.py TransferGroup / KVCacheInfo shared data model
kv_cache_manager/py_connector/kernel/batch_gather_scatter_helper.py Strided (block-stride aware) gather/scatter kernel path
kv_cache_manager/py_connector/test/{test_block_translation,test_data_transfer_results,test_scheduler_state,test_kv_layouts}.py Pure-logic unit tests (95 cases) runnable without torch/vLLM/GPU, incl. layout detection for all three vLLM KV layout eras and the per-block spec-coverage / match-truncation rules
kv_cache_manager/py_connector/test/vllm_stubs.py Test stubs: vLLM, compiled pybind client and missing third-party deps (torch etc.); real modules always win
kv_cache_manager/py_connector/test/kernel/test_strided_gather_scatter.py GPU test for the strided kernel path (tagged manual)
integration_test/vllm_e2e/* Two-phase e2e harness: real manager + real vLLM server, independent KV capture, bit-exact comparison, 9 scenarios incl. cross-request prefix reuse, fault injection and a mutation meta-test
kv_cache_manager/manager/cache_manager.cc StartWriteCache: validate location_spec_group_names against the block count, so token-only writes (no explicit block_keys) can carry per-block group names

Known Limitations / Risks

  • e2e CI is not included in this PR. The e2e targets are tagged manual (they need a GPU runner, a prepared vLLM venv and local model checkouts); wiring them into CI needs runner/repo-variable setup and will land as a separate PR. The pure-logic unit tests do run in the existing open-source CI without torch.
  • vLLM 0.24.x / 0.25.x are expected-compatible but not e2e-run: they share the 0.23.0 KV layout and connector API surface (checked against the vLLM source per tag); 0.22.1 / 0.23.0 / 0.26.0 are e2e-verified (see the support matrix and the validation report comment).
  • MLA: supported through the same code path by inheritance, but no dedicated e2e scenario yet (e.g. GLM-4.7-Flash).
  • SWA: not supported yet (e.g. gemma-3-4b-it); next planned capability. The connector now rejects FullAttentionSpec groups carrying sliding_window / attention_chunk_size at startup instead of silently publishing windowed KV as full-prefix caches.
  • Hybrid load failures cannot be reported to vLLM. vLLM's invalid-block recovery (Scheduler._update_requests_with_invalid_blocks, vllm/v1/core/sched/scheduler.py) only understands single-group block tables (upstream TODO (davidb): add support for hybrid memory allocator), so the connector passes report_failures=False for multi-group (hybrid) models: a failed hybrid load is logged but not reported, and vLLM may decode from partially loaded KV, potentially producing corrupt output until upstream adds multi-group invalid-block recovery. Single-group (pure attention) models report failures normally and recover via recompute.
    The reachable surface of this limitation is now limited to genuine storage failures: the case that used to walk into it deterministically — an external match ending on a block whose recurrent state was never written — is prevented at scheduling time by the per-block spec coverage and match truncation described above (test_cross_request_prefix), so such a load is never started.
  • Hybrid models require prefix caching (mamba_cache_mode="align"). With prefix caching disabled (mamba_cache_mode="none") vLLM keeps a single resident mamba state block per request, so every manager block maps to that same block: saves would publish the current state under earlier prefix keys and loads would overwrite one block repeatedly (last write wins). Do not serve hybrid models through this connector with prefix caching off.
  • Cross-request hybrid prefix reuse is truncated to the last state-complete block, which costs cache hits: a hybrid request can only resume where the recurrent state ends, and align mode materializes states sparsely (observed: 3 of 7 manager blocks). Correctness is prioritized over hit rate here — the attention KV of a longer prefix is unusable without its boundary state. The manager already supports complementary writes (a block's missing state specs can be filled in by a later write), so back-filling the missing states is a possible follow-up optimization.
  • The e2e harness currently uses the local-file storage backend; parameterizing the storage cluster endpoint to broaden coverage is a follow-up.

Testing

  • Unit tests (no GPU / no torch needed, matches open-source CI):
    bazelisk test //kv_cache_manager/py_connector/...
    95 pure-logic cases covering block translation (against a brute-force reference), transfer-result flattening, the scheduler state machine, and KV layout detection/pointer construction for all three vLLM layout eras (incl. fail-fast on unrecognized layouts and the hybrid-on-old-vLLM gate). Verified green both in a full dev venv and inside the CI dev container (tair-kvcache-kvcm-dev, no torch installed).
  • GPU kernel test (tagged manual): //kv_cache_manager/py_connector/test/kernel:test_strided_gather_scatter — element-wise comparison of the strided gather/scatter against a naive torch reference.
  • End-to-end (tagged manual; needs 1-2 GPUs, a vLLM >= 0.26 venv with both KVCM wheels, and a local model — see integration_test/vllm_e2e/README.md for setup and the full environment-variable table, KVCM_E2E_MODEL / KVCM_E2E_PYTHON are required):
    bazelisk test //integration_test/vllm_e2e:e2e_tests \
      --test_env=KVCM_E2E_PYTHON=/path/to/vllm-venv/bin/python \
      --test_env=KVCM_E2E_MODEL=/path/to/model
    All 9 scenarios (basic, concurrent, TP=2, partial hit, full hit, multi-turn, cross-request prefix, load failure injection, mutation meta-test) pass bit-exact against both Qwen2.5-7B-Instruct (full attention) and Qwen3.5-4B (hybrid) on 2x A10, vLLM 0.26.0. The mutation meta-test proves the harness detects an injected off-by-one in the slot translation, i.e. the verification is not vacuous. Additionally, test_basic / test_concurrent / test_tp pass bit-exact on vLLM 0.22.1 (full attention) and 0.23.0 (full attention + hybrid) — full per-version results in the validation report comment below.

lpdink added 8 commits July 29, 2026 12:23
…ybrid attention

Every kv_cache_group (FullAttentionSpec / MambaSpec) becomes an independent
transfer unit with its own location spec (tp{rank}_g{group}), block table and
data access strategy: token-granular gather/scatter for attention groups,
per-block opaque byte copy for mamba/linear state groups. A full-attention
model is simply the one-group case.

- GroupMeta/TransferGroup abstractions; ReqState tracks per-group block tables
- SupportsHMA (request_finished_all_groups) for hybrid memory allocator models
- Adapt to vLLM 0.26.0 packed KV layout (num_blocks, heads, block, 2*head_size)
- Strided gather/scatter kernel path for padded/strided page layouts
- Null-block detection for unmaterialized mamba boundary state
…ention

Two-phase save/load verification driven through a real KVCM manager and a real
vLLM OpenAI server: phase 1 saves KV to KVCM and captures references straight
from vLLM's paged cache; phase 2 loads via the connector and captures again.
Captures use only vLLM's own block-table mapping, breaking the save/load
symmetry so per-group translation bugs cannot cancel out. Bit-exact compare
with cosine > 99.99% fallback.

Scenarios: test_basic (TP=1), test_concurrent (4 reqs), test_tp (TP=2 with
cross-block manager/vllm block size mapping for full-attention models). The
same targets run against full-attention (Qwen2.5) and hybrid (Qwen3.5) models
via KVCM_E2E_MODEL; hybrid runs enable prefix caching (mamba align mode) and
restart vLLM between phases so loads come from KVCM, not the local cache.

Adds a matrixed GitHub workflow (full-attention + hybrid) for self-hosted GPU
runners.
…Error

Two fixes in the vLLM connector scheduler path:

1. get_num_new_matched_tokens returned the raw external match count without
   capping it below the prompt length. This connector loads synchronously
   (load_kv_async=False), so vLLM schedules num_tokens - num_computed_tokens
   new tokens and asserts that count is > 0 (vllm 0.26.0
   v1/core/sched/scheduler.py waiting-queue loop). A prompt whose token count
   is an exact multiple of the manager block size with all blocks externally
   cached made the count 0 and crashed the engine. Drop trailing matched
   blocks until at least one token remains to recompute, mirroring the
   fallback in vLLM's own SharedStorage/NIXL connectors.

2. handle_canceled_save_req indexed _alive_requests[req_id] directly, but
   cancellations arrive from http_executor threads and can race request
   teardown; use .get() with a warning and skip.

Covered by kv_cache_manager/py_connector/test/test_scheduler_state.py and
the integration_test/vllm_e2e test_full_hit scenario.
…cheduler state

Previously zero unit coverage on the connector's pure logic. New Bazel
py_tests under py_connector/test (vllm_stubs.py registers lightweight vLLM /
pybind stand-ins in sys.modules so v1_connector imports without a GPU or the
compiled client):

* test_block_translation: _attn_token_indices / _state_block_ids against an
  independent brute-force reference, parameterized over ratio=1, ratio>1 and
  manager_bs != group_bs, plus hand-computed examples.
* test_data_transfer_results: MultiResult ordering (in-order, out-of-order,
  concurrent) and the save/load done callbacks' stride-AND merge, which pins
  the implicit group-major submission-order contract of _submit_group_tasks;
  includes the hybrid report_failures=False branch.
* test_scheduler_state: get_num_new_matched_tokens (incl. the full-hit cap),
  parse_block_mask_to_save_indices (offset and bool_masks), _parse_groups
  (full-attn, hybrid, eagle skip, unsupported spec), and the
  build_connector_meta state machine (new request, cached deltas with
  new_block_ids None/non-None, preemption resume via both resumed_req_ids and
  legacy resumed_from_preemption, save-threshold growth, both
  request_finished paths, canceled-save races).
* test/kernel/test_strided_gather_scatter (GPU): the strided kernel path
  (block_stride/local_block_size incl. padded pages) added in fc89691 had no
  coverage; checked element-wise against naive torch indexing, plus a
  roundtrip and a padding-untouched sentinel check.

vllm/BUILD: expose vllm_connector to py_connector subpackages for the tests.
Two production bugs surfaced by the new e2e scenarios:

1. Fail-reschedule loop after a KV load failure. With
   kv_load_failure_policy=recompute, vLLM reschedules the request and calls
   get_num_new_matched_tokens again; the manager still advertises the blocks
   whose storage is gone, so the connector re-matched them and the engine
   looped load-fail-reschedule forever (request hung). A request that already
   went through an external load attempt (blocks were allocated) now skips
   external matching on re-query and recomputes locally.

2. Multi-block hybrid saves always failed. vLLM's mamba 'align' mode only
   materializes the state block ending the matched region
   (single_type_kv_cache_manager.MambaManager assigns the null block to
   interior positions), so every interior manager block has a null (id 0)
   state target by design. save_task treated that as a failure, the stride-AND
   merge then dropped those manager blocks from the manager's prefix chain,
   and hybrid caching silently degraded to the final block only. Null state
   targets are now transferred vacuously (reported success, nothing copied) on
   both save and load; load_task previously failed the whole task on any null
   target for the same reason.

Covered by test_scheduler_state (retry guard), test_data_transfer_results
(vacuous null-state save/load), and e2e: hybrid basic now verifies 4/4 manager
blocks bit-exact; test_load_failure exercises the recompute path end to end.
…rage

Harness hardening (task B) plus the P2 coverage fix:

* Hybrid prompts were ~578 tokens = 1 x 528 manager block, so multi-block
  state mapping and incremental save never ran under hybrid. make_base_prompts
  now emits 140 sentences (~2100 tokens > 3 x 528) for hybrid models.
* wait_for_captures timeout raises AssertionError instead of warning.
* Expected ref/loaded capture counts are computed per prompt from the actual
  tokenization (len // mbs, shared-prefix for loads) and asserted as exact
  lower bounds via assert_report_ok(min_matched=...).
* bit-exact comparison is the default; cosine fallback only with
  KVCM_E2E_ALLOW_COSINE=1.
* compare_captures iterates the loaded capture's layers (a loaded layer
  without a reference is a hard error); mamba align-mode null-state layers may
  legitimately be absent on either side and test_connector skips capturing
  null (id 0) state blocks, mirroring the connector's vacuous transfer.
* VllmServer/ScenarioEnv: connector_name selection (mutation meta-test),
  log_level, kv_transfer extra-config overrides, kv_load_failure_policy,
  key_count_per_file, and a ScenarioEnv helper owning manager/vLLM lifecycle
  for the new custom scenarios; send_completions accepts token-id prompts and
  extra payload fields; file-backend root_path gets a trailing slash so block
  files land inside the dir.
* test_connector: drop a duplicated capture line; add MutatedConnector
  (slot -1 off-by-one, test-side injection only) for the mutation meta-test.
* test_mutation (B4): runs the basic scenario with MutatedConnector (slot -1
  in _attn_token_indices) and asserts KV verification FAILS -- proof the
  capture-based harness catches symmetric translation bugs and is not vacuous.
* test_full_hit (C2): prompt trimmed to an exact multiple of the manager
  block size, resent after being fully saved. Regression for the synchronous
  full-hit crash (vllm 0.26.0 scheduler.py 'assert num_new_tokens > 0');
  asserts the engine survives and 0 < matched < prompt tokens.
* test_partial_hit (C1): staged A / A+B / A+B+C prompts with prefix caching
  on; exercises the non-zero-offset incremental manager query and the
  incremental save extension, asserts a logged query with offset > 0 and
  verifies the blocks saved through the incremental path.
* test_load_failure (C3): deletes the tail half of the per-block storage
  files between save and load (key_count_per_file=1, block_per_load_task=1,
  kv_load_failure_policy=recompute). Full-attn: failures reported to vLLM,
  surviving head blocks verify bit-exact, mismatches confined to deleted
  blocks. Hybrid: failure swallowed by design (vLLM invalid-block recovery is
  single-group only), asserts no hang/crash and the failure log.
* test_multi_turn (C4): turn 1 decodes past a manager block boundary
  (ignore_eos + return_token_ids), asserts the manager committed more blocks
  than the prompt covers; turn 2 embeds turn 1 prompt+output as token ids and
  must externally match beyond prompt-only coverage with verified KV.

All scenarios pass for both Qwen2.5-7B-Instruct (full-attn) and Qwen3.5-4B
(hybrid) alongside the original basic/concurrent/tp regressions.
The open-source CI image has no torch/triton/orjson/zmq/requests, so the
three pure-logic py_tests failed at import time. vllm_stubs now registers
stand-ins for missing third-party deps (real modules always win), the
connector uses typing.TYPE_CHECKING instead of typing_extensions, and the
GPU-only Triton kernel test is tagged manual like the other GPU suites.
Comment thread integration_test/vllm_e2e/e2e_lib.py Fixed
…ness

- delete the immature test-vllm-e2e workflow (runner/model/venv/uv are all
  dev-machine specific); e2e CI will land as a separate PR
- KVCM_E2E_MODEL / KVCM_E2E_PYTHON become required env vars with clear
  errors instead of local-path defaults
- remove the unused cosine-similarity fallback: every scenario is verified
  bit-exact
- tag the e2e targets manual (GPU + vLLM venv + model required) and add an
  explicit :e2e_tests suite
- document all environment variables in the README
@lpdink
lpdink force-pushed the feature/vllm-hybrid-attention-rewrite branch from f673b14 to 729e95f Compare July 29, 2026 05:15

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f673b149a7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

# not a failure: report it saved vacuously. Failing it would
# stride-AND the whole manager block out of the manager's prefix
# chain and kill multi-block hybrid caching entirely.
valid = [i for i in range(n) if block_ids[i] != 0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not mark unsaved null state blocks as cached

For hybrid/Mamba none or align runs, a manager block can have block_id == 0 here when it was an interior/skipped state block, so this path reports the block saved without writing any bytes for that state group. If a later request only matches through that same manager block, vLLM may allocate a real nonzero final state target for it; load_task will then try to read the URI that was never written, and the hybrid load path reports no invalid block ids, so the request can continue with missing recurrent state instead of recomputing. These null-source blocks should not be advertised as cacheable unless the required state was actually materialized.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3979907 (connector), c0a1a48 (manager-side validation fix this uncovered), with the reproducing e2e scenario in 0d70939.

You were right, and the failure mode is exactly the one you described. The fix expresses the absence to the manager instead of encoding it as a success, using the location_spec_group_names mechanism the server already implements (FilterWriteCache even documents the intent: "allowing complementary locations (e.g. KV-only and Mamba-only) to coexist in the same block").

Save side. Registration now declares two LocationSpecGroupsfull (every group's spec) and attn (attention specs only). build_connector_meta derives each manager block's state completeness from vLLM's block table (in the scheduler loop, since later steps mutate that table) and start_write_cache announces it per key, so the manager allocates and advertises exactly the specs that will hold data. Transfers no longer conflate "nothing to do" with "done": a group abstains for blocks it provably has no data for, and a block's verdict is the AND over the groups that did carry data — a block no group wrote is not published at all. Full-attention models declare no groups and their requests are byte-identical to before.

Load side. getCacheLocation reports the real per-block spec coverage, so get_num_new_matched_tokens truncates an external match to the last state-complete block. A hybrid request can only resume where the recurrent state ends; the attention KV of a longer prefix is unusable without it. This also means the case you found is no longer merely detected — it is never started, which narrows the reachable surface of the "hybrid load failures cannot be reported" limitation to genuine storage failures.

Manager fix. StartWriteCache validated location_spec_group_names.size() against keys.size(), but a token-only request (empty block_keys) derives its keys further down in the same function, so keys.size() was 0 and every such request carrying group names was rejected with size not match, expect[0], real[N]. The proto contract says the list is per block, so the block count is now derived from the tokens when block_keys is empty. sglang never hit this because it always sends explicit block_keys.

Evidence — the new test_cross_request_prefix scenario. It saves request A, measures which of A's blocks really hold a state independently (from the harness's own reference captures, not from the code under test), then runs a strict token prefix B whose match would end on a state-less block: once on a fresh instance id (ground truth) and once against A's cache.

Before the fix, with A's states materialized only at blocks [2,5,6]:

A state materialized per block (from captures): [False, False, True, False, False, True, True]
A per-block coverage:                           ['full','full','full','full','full','full','full']
B reference output: [7189, 279, 271, 248068, 198, 90700, ...]
B cached output:    [7189, 279, 869, 220, 17, 17, ...]      <- silently different

After:

A per-block coverage: [missing, missing, full, missing, missing, full, full]   <- matches reality
req:... truncated external match from 5 to 3 blocks: later blocks carry no recurrent state
B cached output == B reference output   (32 tokens, token for token)

Regression: 9/9 e2e scenarios pass on vLLM 0.26.0 against both Qwen3.5-4B (hybrid) and Qwen2.5-7B-Instruct (full attention); test_basic + test_cross_request_prefix also pass on 0.23.0. Unit tests grew 76 -> 95 cases (the two test_*_vacuously_succeed cases that pinned the old behaviour are replaced by tests for the new dispositions, truncation and per-key group names).

Known cost, disclosed in the PR body: cross-request hybrid prefix reuse is now truncated to the last state-complete block, which costs hits (observed 3 of 7 blocks carry state). Correctness is prioritized over hit rate; back-filling the missing states via complementary writes — which the manager already supports — is a possible follow-up.

Comment thread integration_test/vllm_e2e/e2e_lib.py Outdated
import uuid
from typing import Optional

import requests

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Declare requests for the e2e harness

As added, every e2e test imports e2e_lib, and this top-level import runs before any KVCM_E2E_* validation. In the repo test Python, and in Bazel sandboxes unless @pip_cpu//requests is included in the target deps, requests is not present, so the manual e2e targets fail at collection with ModuleNotFoundError before they can launch the vLLM venv specified by KVCM_E2E_PYTHON. Add the pip dependency to the BUILD target or avoid importing it in the Bazel runner.

Useful? React with 👍 / 👎.

Comment on lines +567 to +570
table = load_req.all_block_ids[0]
gbs = self._group_metas[0].block_size
report_ids = [table[(mb * self._manager_block_size) // gbs]
for mb in load_req.manager_block_idxes]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report load failures using the active group's table

When an EAGLE/drafter group is present and skipped by _parse_groups, _num_groups can still be 1 while the only transferred group has group_idx == 1. In that case a single-group load failure is reported from all_block_ids[0], which is the skipped EAGLE table rather than the table used by _plan_group_transfers; vLLM then invalidates the wrong blocks and the failed external load can be retried or treated inconsistently. Use the surviving group's group_idx when choosing the table for report_ids.

Useful? React with 👍 / 👎.

Comment on lines +308 to +318
elif isinstance(spec, FullAttentionSpec):
# Attention KV is token-granular; scale from the spec's page size
# to the manager block size.
per_token_bytes = spec.page_size_bytes // spec.block_size
metas.append(GroupMeta(
group_idx=idx,
is_attention=True,
layer_names=list(group.layer_names),
block_size=spec.block_size,
per_block_bytes=per_token_bytes * self._manager_block_size * len(group.layer_names),
))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject windowed attention specs instead of caching them

When vLLM supplies a FullAttentionSpec that carries sliding_window or attention_chunk_size metadata, this branch accepts it as ordinary full attention and publishes its blocks to KVCM. For SWA/chunked-local models (called out as unsupported by this change), that lets later requests reuse external blocks under full-prefix semantics even though the model runner only treats a moving window/chunk as valid context, which can produce incorrect KV reuse instead of failing fast. Check those spec fields here and raise until the connector implements the matching/clipping rules.

Useful? React with 👍 / 👎.

lpdink added a commit that referenced this pull request Jul 29, 2026
…tion

PR #257's rewrite only accepted vLLM >= 0.26.0's packed 4-D KV layout
(num_blocks, H, block, 2D), regressing full-attention support on older
vLLM. Detect all three flash_attn layouts from the tensor shape itself
(never from version strings) and normalize each into token-major
per-pointer views:

* 4-D packed (vllm >= 0.26.0): one pointer per layer, as before.
* 5-D N-first (num_blocks, 2, block, H, D) (vllm 0.23.0 - 0.25.x): two
  pointers per layer, K/V interleaved per block -> kernel strided path.
* 5-D KV-first (2, num_blocks, block, H, D) (vllm <= 0.22.1): two flat
  pointers per layer.

The Triton gather/scatter kernel already addresses through a flat
pointer array ([K0, V0, K1, V1, ...] for non-MLA), so it needs zero
changes; TransferGroup grows num_kv_ptrs (pointer count, = layer_num
for packed, 2x for split layouts) and the staging buffer views in
data_transfer use it. per_block_bytes is identical across layouts, so
the manager, storage and transfer protocol are unaffected. Unrecognized
layouts still fail fast at startup.

Hybrid (mamba) models on vllm <= 0.22.x are rejected with a clear
NotImplementedError: those schedulers assert
num_external_computed_tokens == 0 in _mamba_block_aligned_split, so the
first external hit would crash mid-flight. The gate probes the
installed scheduler for that blocking assert (capability check, not a
version comparison).

Note the saved byte layout differs between the packed and split-K/V
eras, so KV cache is not portable across vLLM upgrades; instance_id
isolation already prevents such mixing in practice.

New unit tests cover view construction for all three layouts (shape /
stride / pointer math on stub tensors, no torch required), fail-fast on
unrecognized and ambiguous shapes, and the hybrid gate on old/new/
unprobeable schedulers. The e2e VerifyingConnector reuses attn_kv_views
so its captures follow the same normalization.
@lpdink

lpdink commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

Multi-version validation report (vLLM 0.22.1 / 0.23.0 / 0.26.0)

As promised, here are the cross-version e2e results after commit 9dc5961 (shape-based KV layout detection). Environment: 2x NVIDIA A10, one dedicated venv per vLLM version (import vllm; vllm.__version__ verified per venv), models Qwen2.5-7B-Instruct (full attention) and Qwen3.5-4B (hybrid: 3 MambaSpec + 1 FullAttentionSpec groups). Every cell below is a bazel e2e run of the two-phase harness with bit-exact KV comparison (bit_exact == matched, loaded_without_ref == []); numbers are matched/bit-exact manager blocks.

vLLM 0.22.1 — split K/V layout (2, num_blocks, block, H, D)

Scenario Qwen2.5-7B (full attn) Qwen3.5-4B (hybrid)
test_basic ✅ PASSED, 36/36 ❌ by design (see below)
test_concurrent ✅ PASSED, 147/147 ❌ by design
test_tp (TP=2) ✅ PASSED, 72/72 ❌ by design

Hybrid on 0.22.1 fails fast, not mid-flight: vLLM <= 0.22.x asserts num_external_computed_tokens == 0 in Scheduler._mamba_block_aligned_split ("External KV connector is not verified yet"), so the first external hit would crash the engine. The connector now probes the installed scheduler for that blocking assert (a capability check — no version-string comparison anywhere in the detection logic) and raises at startup:

NotImplementedError: TairKvCacheConnector: this vLLM version cannot combine
hybrid (mamba) models with an external KV connector -- its scheduler asserts
num_external_computed_tokens == 0 in _mamba_block_aligned_split ('External KV
connector is not verified yet'). Upgrade to vLLM >= 0.23.0 for hybrid model
support; full-attention models are unaffected.

The gate behavior is verified against all three real venvs (raises on 0.22.1, passes on 0.23.0/0.26.0) and unit-tested against stubbed old/new/unprobeable schedulers.

vLLM 0.23.0 — split K/V layout (num_blocks, 2, block, H, D)

Scenario Qwen2.5-7B (full attn) Qwen3.5-4B (hybrid)
test_basic ✅ PASSED, 36/36 ✅ PASSED, 4/4
test_concurrent ✅ PASSED, 147/147 ✅ PASSED, 16/16
test_tp (TP=2) ✅ PASSED, 72/72 ✅ PASSED, 16/16

The K/V halves of one kernel page are interleaved in this layout, exercised through the Triton kernel's existing strided path (block_stride spans both halves). 0.24.x/0.25.x share this layout and API surface (checked per source tag), so they are expected-compatible although not e2e-run.

vLLM 0.26.0 — packed layout (num_blocks, H, block, 2D) — full regression, no test cache

All 8 scenarios x both models, --cache_test_results=no:

Scenario Qwen2.5-7B (full attn) Qwen3.5-4B (hybrid)
test_basic ✅ PASSED, 36/36 ✅ PASSED, 4/4
test_concurrent ✅ PASSED, 147/147 ✅ PASSED, 16/16
test_tp (TP=2) ✅ PASSED, 72/72 ✅ PASSED, 16/16
test_partial_hit ✅ PASSED ✅ PASSED
test_full_hit ✅ PASSED ✅ PASSED
test_multi_turn ✅ PASSED, 57/57 ✅ PASSED, 6/6
test_load_failure ✅ PASSED ✅ PASSED
test_mutation (meta) ✅ PASSED ✅ PASSED

Implementation notes

  • Layout detection is purely shape-based (attn_kv_views in v1_connector.py): 4-D packed -> one pointer per layer; 5-D KV-first (t[0]/t[1]) and 5-D N-first (t[:,0]/t[:,1]) -> two pointers per layer, [K0, V0, K1, V1, ...]. Ambiguous or unrecognized shapes fail fast at startup.
  • Zero changes to the Triton kernel — its pointer-array addressing already supports split K/V; git diff on batch_gather_scatter_helper.py is empty for this commit.
  • per_block_bytes is identical across layouts, so the manager, storage backends and transfer protocol are untouched.
  • KV saved under different layout eras is byte-incompatible; instance_id isolation prevents cross-era mixing in practice (noted in the PR description).
  • Unit suite grows to 53 cases (new test_kv_layouts.py: view construction for the three layouts, fail-fast cases, hybrid gate), all runnable without torch as before.

…tion

PR #257's rewrite only accepted vLLM >= 0.26.0's packed 4-D KV layout
(num_blocks, H, block, 2D), regressing full-attention support on older
vLLM. Detect all three flash_attn layouts from the tensor shape itself
(never from version strings) and normalize each into token-major
per-pointer views:

* 4-D packed (vllm >= 0.26.0): one pointer per layer, as before.
* 5-D N-first (num_blocks, 2, block, H, D) (vllm 0.23.0 - 0.25.x): two
  pointers per layer, K/V interleaved per block -> kernel strided path.
* 5-D KV-first (2, num_blocks, block, H, D) (vllm <= 0.22.1): two flat
  pointers per layer.

The Triton gather/scatter kernel already addresses through a flat
pointer array ([K0, V0, K1, V1, ...] for non-MLA), so it needs zero
changes; TransferGroup grows num_kv_ptrs (pointer count, = layer_num
for packed, 2x for split layouts) and the staging buffer views in
data_transfer use it. per_block_bytes is identical across layouts, so
the manager, storage and transfer protocol are unaffected. Unrecognized
layouts still fail fast at startup.

Hybrid (mamba) models on vllm <= 0.22.x are rejected with a clear
NotImplementedError: those schedulers assert
num_external_computed_tokens == 0 in _mamba_block_aligned_split, so the
first external hit would crash mid-flight. The gate probes the
installed scheduler for that blocking assert (capability check, not a
version comparison).

Note the saved byte layout differs between the packed and split-K/V
eras, so KV cache is not portable across vLLM upgrades; instance_id
isolation already prevents such mixing in practice.

New unit tests cover view construction for all three layouts (shape /
stride / pointer math on stub tensors, no torch required), fail-fast on
unrecognized and ambiguous shapes, and the hybrid gate on old/new/
unprobeable schedulers. The e2e VerifyingConnector reuses attn_kv_views
so its captures follow the same normalization.
@lpdink
lpdink force-pushed the feature/vllm-hybrid-attention-rewrite branch from 1f92800 to 9dc5961 Compare July 29, 2026 08:06

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9dc5961e63

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

elif isinstance(spec, FullAttentionSpec):
# Attention KV is token-granular; scale from the spec's page size
# to the manager block size.
per_token_bytes = spec.page_size_bytes // spec.block_size

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Size attention transfers from unpadded KV bytes

When a FullAttentionSpec has page_size_padded/block-strided pages, this uses spec.page_size_bytes, which includes the per-page padding gap, to size the registered location and staging buffer. The transfer group later gathers only compact token data with shape num_kv_ptrs × manager_block_size × per_token_dim, so padded pages make gpu_buffer.view(...).view(...) in save/load require fewer elements than group.per_block_bytes and the transfer fails before any block is written; size this from the unpadded/real page bytes instead of the padded page size.

Useful? React with 👍 / 👎.

def num_allocated_blocks(self) -> int:
if not self.block_ids_per_group:
return 0
return min(len(b) for b in self.block_ids_per_group)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count only transferred groups when deciding saves

When vLLM includes a skipped EAGLE/MTP group, that group's block table can intentionally lag or drop the last block while _parse_groups excludes it from transfer. Taking the minimum across every table in block_ids_per_group caps target_save_num by a group KVCM never saves, so fully computed main-model blocks beyond the drafter table are never submitted to start_write_cache; base this count on the transferred group indices instead.

Useful? React with 👍 / 👎.

Comment thread integration_test/vllm_e2e/e2e_lib.py Outdated
@@ -0,0 +1,837 @@
"""Orchestration for the KVCM <-> vLLM end-to-end KV cache verification test.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个文件做的事情太多了,要拆一下。不要都放到一个文件里。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 9418a10: e2e_lib.py is now a re-exporting facade over two modules with one concern each -- lib_utils.py (pure helpers: detection, paths, tokenization, queries, traffic, capture comparison; no process state) and servers.py (process lifecycle: manager binary, vLLM server, ScenarioEnv). Scenario files keep their imports.

"exclusive", # GPU tests must run serially to avoid CUDA OOM contention
"gpu", # requires 1+ GPU
"manual", # needs a GPU machine + vLLM venv + model; see README.md
"no-remote-exec",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

在后续CI ready时移除

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept for now: the manual tags are what keeps these targets out of OSS CI (no GPU runner there). They come off together with the CI-wiring PR that actually provides the runner.

Comment thread integration_test/vllm_e2e/e2e_lib.py Outdated
return (
"full_attention_interval" in text_cfg
or "linear_conv_kernel_dim" in text_cfg
or cfg.get("model_type", "").startswith("qwen3_5")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个判断看起来很可疑

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it was a shape-sniffing pile. Rewritten to prefer what the model actually declares: architectures first (Qwen3Next/Zamba/FalconH1/Samba/Jamba markers), falling back to the layer-knob heuristics only when the config declares no architectures (9418a10).

Comment thread integration_test/vllm_e2e/e2e_lib.py Outdated
# --------------------------------------------------------------------------- #
# vLLM server
# --------------------------------------------------------------------------- #
class VllmServer:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

拆一下,结构清晰一点,后续对vllmServer/kvcmServer均可能有其他setting。utils放到utils下面。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Covered by the same split (9418a10): utils live in lib_utils.py, the server classes in servers.py with room for further per-server settings.

Comment thread integration_test/vllm_e2e/e2e_lib.py Outdated
"--served-model-name", SERVED_MODEL_NAME,
"--port", str(self.port),
"--tensor-parallel-size", str(self.tp_size),
"--max-model-len", "4096",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

比如这里就hardcode了,考虑更好的方法来传入vllm庞大数量的参数。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serving knobs are no longer hardcoded CLI fragments: they move into an override-able vllm_args dict on VllmServer (max-model-len, gpu-memory-utilization, ...), so a scenario can widen them without editing the harness (9418a10).

Comment thread integration_test/vllm_e2e/e2e_lib.py Outdated
binary = find_manager_binary(repo_root)
cmd = [
binary,
"--env", f"kvcm.service.rpc_port={self.rpc_port}",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

相关setting文件化一下

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manager side already writes its config to a JSON file (_write_config); the vLLM side is now centralized in the vllm_args dict mentioned above. A scenario-file format (YAML/TOML) is the next step if the scenarios keep growing -- noted as follow-up.

lpdink added 4 commits August 18, 2026 18:29
_alive_requests was an unconditional per-request grab bag -- statistics,
block-table mirrors, burn flags and the save ledger all lived on one
 ReqState created for every request the match hook ever saw. It is now
three structures with disjoint lifetimes:

- _tracked: RequestLedger per request, created at the FIRST allocation
  (update_state_after_alloc) and dropped at retirement. It holds only
  what hooks do not re-provide: the accumulated per-group block tables,
  the save water-mark, and the save-session ledger. The token stream is
  read from the live vLLM Request (all_token_ids); only its length is
  tracked. Requests never allocated are never tracked.
- _load_failed / _load_attempted: the external-match discipline, as two
  small sets cleaned at retirement instead of booleans on every request.
- The local/remote match statistics are gone (vLLM already knows its
  computed tokens; nobody consumed them).

get_num_new_matched_tokens becomes a pure query per its contract: it
never registers state, and the LoadRequest moves to
update_state_after_alloc -- the moment both halves of its address (the
clamped match from the query cache + the freshly allocated block
tables) exist. The query cache drops its 1s TTL for a request-scoped
produce/consume lifecycle (produced by the match hook, consumed at
allocation, invalidated at retirement), so a request that waits many
steps between query and allocation no longer loses its answer.

Unit tests rewritten around the ledger and the FakeLocationQueries
produce/consume double; 97 cases pass.
…h asserted role slots

The role split files are connector_scheduler.py / connector_worker.py
(ConnectorScheduler / ConnectorWorker), and the shell owns one slot per
role -- connector_scheduler on scheduler-role instances,
connector_worker on worker-role instances, the other None -- with every
hook asserting the slot it needs (the mooncake pattern), so calling a
scheduler hook on a worker instance fails loudly instead of surprising.

TairKvCacheConnectorExtraConfig becomes a Pydantic model: unknown
extra_config keys are rejected at startup (typos fail fast), defaults
document every knob in one place, and model_dump() feeds the manager
client factory. request_finished stays as a shim for the pre-SupportsHMA
dispatch (upstream plans to deprecate that path -- link in the comment)
and will be removed once the minimum supported vLLM dispatches via
SupportsHMA only.

97 unit tests pass.
…e split

e2e_lib.py (839 lines) carried everything; it is now three modules with
one concern each, behind a re-exporting facade so scenario files keep
their imports:

* lib_utils.py -- pure helpers (model detection, paths, tokenization,
  manager queries, traffic, capture comparison); no process state;
* servers.py -- process lifecycle: the real manager binary, the real
  vLLM OpenAI server and ScenarioEnv;
* e2e_lib.py -- compatibility facade only.

Review follow-ups baked in:

* the hybrid model detection prefers declared architectures
  (Qwen3Next/Zamba/FalconH1/Samba/Jamba markers) and falls back to the
  layer-knob heuristics only when the config declares none;
* the vLLM serving knobs (max-model-len, gpu-memory-utilization, ...)
  move into an override-able vllm_args dict instead of hardcoded CLI
  fragments -- a scenario can widen them without editing the harness;
* the VLLM_KV_CACHE_LAYOUT=NHD comment now answers the question asked
  in review: it is required, not cosmetic -- the connector asserts
  token-major pages at register_kv_caches and refuses to start on an
  HND-default vLLM without it.

test_connector.py adapts to the connector's new shape: the worker keeps
no request mirror any more, so the VerifyingConnector's scheduler side
snapshots the ledger's live token streams onto the envelope
(token_snapshots) for the worker-side captures; the mutated meta-test
swaps connector_worker instead of _core; captures dispatch on
AttentionGroupMeta and the (views, layout) attn_kv_views signature.
The strided path existed only as inline comments inside the kernel body;
the module docstring now states why it exists (the N-first split layout
interleaves K/V per block and padded allocations leave gaps), gives the
offset decomposition explicitly, and clarifies that every pointer is its
own base. Also records that this kernel serves the vLLM connector only:
the sglang connector moves data through sglang's own hicache path and
never touches this module, so extending it for vLLM's layouts burdens no
other connector.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93fcb63414

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


def spec_name(tp_rank: int, group_idx: int) -> str:
"""Location spec name for one (tp rank, kv cache group) shard."""
return f"tp{tp_rank}_g{group_idx}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Namespace stored KV by tensor layout

During a rolling upgrade where vLLM 0.25 and 0.26 workers share an instance_id, this name remains identical even though attn_kv_views serializes split-K/V and packed pages in different byte orders. The registration metadata contains model/dtype/size but no KVLayout, so the manager accepts both workers and a new worker can load an old worker's bytes into the packed layout, silently corrupting inference. Include the layout/format version in the registered namespace or reject mixed layouts for one instance.

AGENTS.md reference: AGENTS.md:L40-L42

Useful? React with 👍 / 👎.

Comment on lines +101 to +103
if self._async_get_cache_location:
self._query_async(request, computed_blocks)
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reuse the existing in-flight location query

With the default asynchronous query mode, vLLM calls the match hook again every scheduler step while the manager request is pending. Because this branch invokes _query_async even when entry already exists with in_flight=True, every retry submits another identical HTTP request; the four-worker executor accumulates stale queries, amplifies manager load, and delays save-location work sharing the same executor. Only submit when this call creates or replaces the entry, and otherwise return None for the existing request.

Useful? React with 👍 / 👎.

Comment on lines +143 to +146
first_attn = next(kv_caches[name]
for meta in self._group_metas
if isinstance(meta, AttentionGroupMeta)
for name in meta.layer_names)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle state-only cache configurations

When vLLM supplies a pure Mamba/state-space model with one or more MambaSpec groups but no FullAttentionSpec, parse_groups accepts the configuration and the connector advertises HMA support, but this next(...) has no attention tensor and raises StopIteration during worker initialization. Derive the device from a state cache as well, or explicitly reject state-only configurations during parsing with an actionable error.

Useful? React with 👍 / 👎.

lpdink added 3 commits August 18, 2026 19:43
…ferences

Four fixes surfaced by running the e2e harness against the local tree:

- _version_info is stamped into the wheel at build time; a source
  checkout has none, so the connector falls back to dev markers instead
  of failing to import (running from a checkout is a supported mode --
  it is how the e2e harness exercises the code under review).
- data_transfer still passed the deleted kv_stride field to the gather/
  scatter kernels (AttributeError at save time); both call sites drop it.
- the e2e harness resolved worker-role attributes (_data_transfer,
  _tp_rank, ...) on the shell, where the role split moved them to
  ConnectorWorker; properties forward them now. _capture_pending_loaded
  also referenced its caller's meta variable instead of its parameter.
- the harness passes the repository root ahead of any installed wheel
  in the vLLM process's PYTHONPATH, so connector modules resolve to the
  checkout under test; run_e2e moves to servers.py (it orchestrates
  ScenarioEnv) and servers.py imports the utils it consumes.

test_basic passes bit-exact (36/36 blocks) against Qwen2.5-7B-Instruct
on vLLM 0.26.0 with the real manager binary.
During decode vLLM's all_token_ids lags the scheduled-token count by
one: the token scheduled in a step is appended to the request only once
sampled. The ledger kept token_len (the scheduled count) and the save
water-mark derived its block count from it while slicing the hash
material from all_token_ids -- so every decode-time save announced one
block more than the token list could key, the manager returned one
location fewer than announced, and the worker's strict alignment check
dropped the whole session. Decode-produced blocks never registered;
prefill has no lag, which is why only the first block survived.

The pre-mirror code was safe by accident: its request mirror appended
the same lagging slice it counted from. The fix restores the invariant
explicitly: the block count derives from len(all_token_ids) -- the key
material itself -- keeping the allocated-KV cap for the chunked-prefill
case where all_token_ids runs ahead of the computed KV. A one-block
shortfall self-heals on the next step.

Regression test pins the lag scenario (token unknown -> no
announcement; token lands -> 48 tokens announced). Found by the
multi_turn e2e (bit-exact 4/4 after the fix); the unit fakes kept the
two counters in sync, which is exactly why they missed it.
…tion swap

Two fixes from the hybrid e2e run:

- is_hybrid_model short-circuited on a non-empty architectures list:
  Qwen3.5-4B declares its own architecture (Qwen3_5ForConditionalGeneration)
  which matches no known marker, so the model was classified full-attention.
  With that, prefix caching stayed off and the connector ran in the
  explicitly unsupported mamba_cache_mode=none regime -- every loaded
  state diverged bit-exactly while attention KV verified. The signals
  are ORed now; a non-matching architecture string never shadows the
  layer knobs.
- MutatedConnector swapped connector_worker on scheduler-role instances
  too, where the slot is None (AttributeError at startup). The swap is
  guarded to worker-role instances only.

All nine scenarios pass bit-exact against Qwen3.5-4B (hybrid) on
vLLM 0.26.0 with the real manager binary, including the mutation
meta-test (the injected off-by-one is caught as expected).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b8ddf632a0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


from typing import Any, Dict

from pydantic import BaseModel, Field

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add Pydantic to the unit-test runtime

All four new connector unit tests import v1_connector, which imports this module during collection, but kv_cache_manager/py_connector/test/BUILD depends only on :vllm_stubs and the @pip_cpu requirements contain no Pydantic package. Consequently, the advertised Bazel tests fail at collection with ModuleNotFoundError: No module named 'pydantic' in the repository's dependency-only test environment. Add Pydantic to the open-source lockfile and Bazel deps, or extend the dependency-free stubs to cover it.

Useful? React with 👍 / 👎.

Comment on lines +98 to +99
if getattr(model_config, "use_mla", False):
raise NotImplementedError("MLA models are not supported by TairKvCacheConnector")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore support for MLA models

When model_config.use_mla is true, this unconditional rejection prevents the connector from starting for every MLA deployment. The previous implementation explicitly supported that case by setting _use_mla, registering the one-component per-rank layout, and propagating use_mla in the deployment metadata, while this change's stated supported scope still says MLA follows the attention inheritance path. Preserve the existing MLA path (including the correct registration metadata), or stop advertising it as supported.

Useful? React with 👍 / 👎.

The scheduler re-asks get_num_new_matched_tokens every step while an
async location query is on the wire, and get_locations_for_query only
short-circuited *completed* cache entries: an in-flight entry fell
through to _query_async unconditionally, so every re-ask submitted
another full getCacheLocation. With ~12ms query latency that is one
duplicate HTTP query (and one duplicate manager-side per-block lookup
of the whole request prefix) per engine step until the answer lands --
measured on the vLLM 0.22.1 e2e perf harness: 24 requests produced 163
getCacheLocation calls and 23,472 manager block queries, vs 24 calls /
3,456 blocks for the pre-rewrite connector at the same commit base
(the old manager-side query cache's RUNNING entry absorbed re-asks).
Client-side effect: +7.7% TTFT p50 from http-pool contention.

Fix: while an entry is in flight, return None (wait) regardless of the
asked offset; a completed entry with a stale offset still re-issues
exactly once. Re-measured: 24 calls / 3,456 blocks -- exact parity
with the pre-rewrite connector -- and TTFT p50 back to baseline. The
full e2e suite stays bit-exact (9/9 scenarios, both model kinds).

New test_location_query.py pins the fan-out contract with a gated fake
manager client: in-flight re-asks issue exactly one fetch, a stale
offset re-issues once after the previous answer lands, sync mode
answers inline, and a failed query drops the entry so the next ask
retries instead of waiting forever.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05714af000

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

expect_verification_failure=True to prove the harness detects an injected
off-by-one in the token translation.
"""
import torch # noqa: F401 (ensure torch importable early for clear errors)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Declare Torch for the e2e driver

When the documented Bazel :e2e_tests command runs, this import executes in the Bazel test interpreter before the separately configured KVCM_E2E_PYTHON child process is launched. However, integration_test/vllm_e2e/BUILD declares only @pip_cpu//requests, and the pip_cpu lockfile contains no Torch package, so the manual suite fails immediately with ModuleNotFoundError: No module named 'torch' in a dependency-only environment. Add Torch to the test runtime or perform hashing/comparison without importing it in the driver.

Useful? React with 👍 / 👎.

Comment on lines +74 to +77
# Re-issued with a different offset meanwhile.
return
self._local_query_cache[query_key].locations = need_load_locations
self._local_query_cache[query_key].is_done = True
except Exception as e:
logger.warning("get_cache_location error, request_id: %s, error: %s", request.request_id, e)
with self._local_query_cache_lock:
if query_key in self._local_query_cache:
self._local_query_cache.pop(query_key)
entry.locations = locations
entry.in_flight = False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bind async query callbacks to their entry

When a canceled request's request_id is reused while its old manager query is still running, invalidate() removes the old entry and the new request installs another in-flight entry under the same ID; this callback then finds that new entry and stores the old prompt's locations into it because it verifies neither entry identity nor the queried offset. The new request can consequently load KV belonging to the canceled prompt, while its own later callback is discarded because the entry has already been marked complete. Capture the entry or a generation token when submitting the query and only update that exact entry.

Useful? React with 👍 / 👎.

lpdink added 3 commits August 20, 2026 10:10
The lifecycle rewrite collapsed the query cache key to req_id, which
changed reuse semantics vs origin/main in a non-obvious way: a re-ask at
a grown offset (chunked prefill, re-ask after partial compute) is a
*different* query, but the req_id key either served it the older
offset's answer or made it wait behind the older offset's in-flight
query. Restore origin/main's key identity (req_id, query_type,
token_length, computed_blocks) under the lifecycle cache: entries for
several offsets of one request coexist, an in-flight query never blocks
a different offset, and consume still pops exactly the answer the match
hook last returned (tracked in _last_answered).
Per-task torch.empty staging made the connector's VRAM footprint
unbounded and racing the engine for its own allocation: at high load on
low-headroom GPUs (doc085 @ gpu-mem-util 0.92) the 98 MiB per load task
allocations OOMed the engine where origin/main -- whose gather kernel
writes a pre-allocated pinned host pool directly -- ran fine.

Replace the dynamic allocs with one _StagingPool per transfer group
(save and load share it): a fixed, configurable HBM + pinned reservation
(staging_pool_blocks, default 128, validated >= the largest task batch)
handed out as contiguous runs, with blocking acquire as backpressure
when exhausted. Bulk D2H/H2D and kernel views are unchanged -- only the
buffer source differs. Exception paths drain the stream before the
slots go back so a failed task cannot leave enqueued work against a
reused view.

This also restores origin/main's no-dynamic-allocation invariant the
revert of _PinnedBudget (7bd0413) dropped, without reintroducing the
connector-level lifetime governance that #280's deadline chain owns.
…path

The pool fix (a0d6289) bounded the connector's staging but kept a
device-side mirror per transfer group: staging_pool_blocks x
per_block_bytes of permanently reserved HBM (112 MiB/group at the
default on Qwen2.5-7B TP1, once per hybrid group) that competes with
the engine's KV cache and batch headroom -- exactly the reviewer
concern on low-end cards. The mirror only existed to route transfers
through a bulk D2H/H2D copy, and that copy is not needed: the strided
gather/scatter kernel addresses host pinned memory directly over PCIe
(UVA zero-copy, as in origin/main), and state-group copies are plain
copy_ between the GPU tensors and the pinned slices.

Make _StagingPool pinned-host-only and delete the GPU buffer, the
gpu_view, and both bulk copies. Save gathers HBM -> pinned slot and
hands the slot to SaveKvCaches; load hands the slot to LoadKvCaches and
scatters pinned -> HBM. Backpressure, contiguous-run allocation,
exception-path stream drains and the capacity guard are unchanged; the
kernel and the scheduler/worker instruction flow are untouched.
staging_pool_blocks now sizes host RAM only.

Trade-off: a slot is reused once its task reports, so after an SDK
timeout/error a background DMA may still scribble on a reused slot --
the same exposure origin/main's CopyBufferAllocator always had. The
proper fix is #280's deadline contract; taking the exposure now is
what buys zero VRAM without delaying this PR.

@wangxiyu191 wangxiyu191 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

我基于当前 head 05714af0 完成了这一轮 review。整体上,这次重构把 Scheduler / Worker / Common 的职责拆开后结构清晰了很多;per-block spec coverage 正确修复了空 Mamba state 被分配、被当作写成功并发布的问题,默认异步 query 对同一 in-flight 请求的重复提交在当前 head 也已修复。

建议合入前重点处理下面的 inline comments,尤其是:load failure recovery 的能力判断应基于 block-table 形状,而不是 is_hybrid;以及恢复以 bytes 为维度的 staging capacity/backpressure,并评估额外 GPU bounce buffer 的必要性。

现有 thread 中仍需跟进但这里不重复评论的有:异步 query callback 需要绑定 entry/generation;纯 Mamba 配置应在初始化前给出明确拒绝;MLA 是恢复支持还是更新支持矩阵需要形成显式决策;Pydantic 与 e2e Torch 的 Bazel 依赖闭包需要补齐。

几个跨文件结论:

  • Hybrid 模型真实 I/O load failure 的安全 recovery 受限于上游 vLLM 的 multi-group 能力;本 PR 暂不实现可以接受,但必须作为 Known Limitation 明确提示,并保留足够醒目的 error/metric,因为当前后果可能是基于未正确加载的 state 继续推理。
  • 删除 worker-side request mirror 的性能验证目标是不回退:在显存足以让 L1 全命中、外部加载为 0 的条件下,对比 Connector off 与 KVCM + TairMempool,关注吞吐、尾延迟和 GPU bubble;不要求证明删除 mirror 一定带来收益。
  • KV layout / serialization version 暂可依赖发布流程避免混用,不作为 blocker;仍建议把稳定的 format generation(例如 kv_serialization=vllm-hma-v1)写入 model_deployment.extra,只在不兼容格式变化时递增,使 RegisterInstance 能直接拒绝新旧 instance 混用。
  • 当前 PR 与最新 main 冲突,请在合入前 rebase;同时更新 PR 的支持矩阵和实际测试状态,避免实现与描述不一致。

# output. Remove report_failures gating once upstream supports
# multi-group invalid-block recovery.
report_ids = []
if not self.is_hybrid:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里判断的应该是上游 recovery 能处理的 block-table 形状,而不是模型是否 hybrid。vLLM 0.26 的 _update_requests_with_invalid_blocks 会把 get_block_ids(req_id) 解包成恰好一个 table;纯 Attention 也可能有多个 table(多个 Attention group,或仍保留被 connector 跳过的 EAGLE/MTP drafter group)。这时 not self.is_hybrid 仍为 true,失败 block 被上报后,上游可能在 tuple unpack 处报错,而不是进入 recompute。

建议用 len(load_req.all_block_ids) == 1 决定 report_failures;进入该分支后再断言唯一传输组为 Attention,并按它的 group_idxreport_ids。请覆盖单 Attention、多 Attention group、skipped drafter、Attention + Mamba 四种 table 形状。

assert all(uri is not None for uri in uris), \
f"group {group.spec_name}: save batch contains a block without a " \
f"location; _save_dispositions must have failed it"
cpu_buffer = torch.empty(len(valid) * group.per_block_bytes, dtype=torch.uint8,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

旧实现的 CopyBufferAllocator 同时承担了容量 backpressure;重构后每个并发 task 都会动态申请 len(valid) * per_block_bytes 的 pinned CPU buffer,save 还会申请同尺寸 GPU scratch,load 的 .to(device) 也会产生 GPU buffer。结合 32 个 I/O worker 和默认 128 blocks/task,staging 峰值可能达到 GiB 级。SDK deadline 只约束 buffer 生命周期,不能限制同时 in-flight 的总字节数。

建议恢复以 bytes 为维度的有界、预分配且可复用的 pinned CPU pool,并对超大 task 做切分;同时评估完整 GPU bounce buffer 是否必要,若不能消除也应做成有界、可复用的 staging。


if num_external_tokens <= 0:
return
locations, computed_blocks = self._location_query_manager.consume_locations(req_id)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consume_locations() 的签名是 Optional[Tuple[...]],返回 None 时这里会先在 tuple unpack 处抛 TypeError,所以下面的 locations is None ... 检查实际上到不了。请先保存并检查 result,再解包。

另外,此时 vLLM 已经按正的 external hit 分配了 blocks;如果缓存结果真的缺失,静默 return 也不是安全降级。建议将它作为 invariant violation fail closed(带 request id 和匹配/分配上下文),或者走明确受支持的 invalid-block recovery。

for vllm_req in scheduler_output.scheduled_new_reqs:
ledger = self._tracked.get(vllm_req.req_id)
if ledger is None:
continue # never allocated (defensive): nothing to record

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

请确认“scheduled request 找不到 ledger”是否存在正常生命周期路径。按 vLLM 0.26 的常规 hook 顺序,update_state_after_alloc() 会在 scheduled_new_reqs 产生前创建 ledger,我暂时没有找到合法路径;下面 cached request 的同类 continue 也一样。

如果 cancellation / preemption / resume 或某个受支持版本确实可能触发,请把原因写清楚;否则可以保留防御性 continue,但至少记录 warning/error,并带上 request id、new/cached/resumed 状态和 scheduled token 数,避免生命周期契约被破坏后静默丢状态。


if ledger.scheduled_saving_count == ledger.sent_saving_count:
self._retire_request(req_id)
return True, None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

旧实现会在这里返回 local_matched_token_num / remote_matched_token_num,vLLM 会把第二个返回值透传到 EngineCoreOutput.kv_transfer_params。统一返回 None 后,按请求关联 L1/local 与 KVCM/remote 命中、TTFT 和请求属性的能力消失了,aggregate counter 无法还原这层信息。

建议把两个计数保留在 RequestLedger 并继续返回原有结构;如果确实要移除,请把它作为显式兼容性决策,并提供等价的 per-request 替代观测。

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复:5f033efe

调查结论:删除发生在 ledger 重写(cbe1bc4d)时——两个计数和 request mirror 一起被清,属于把被消费的观测契约当成死代码顺手删了,并非 hybrid/group 模型的技术障碍(两个计数都是 group 无关的 token 粒度总量,与 per-block spec coverage 正交)。

已确认 vLLM 侧透传链三个版本(0.22.1 / 0.23.0 / 0.26.0)一致:request_finished 的 extra_info → EngineCoreOutput.kv_transfer_paramsRequestOutput.kv_transfer_params → OpenAI CompletionResponse.kv_transfer_params(协议字段,还会经 extra_args 回传用于 PD 预热),确实是被消费的用户可见行为。

实现照抄旧语义:

  • match hook 每条应答路径记录(local = 询问时的 num_computed_tokens,remote = clamp 后的应答)到 RequestLedger
  • burned-match 重查报告 local 照旧、remote = 0(match 已花掉);
  • _finish_request 两个分支(立即 retire / 延迟 finish)都返回该 dict。

关于您提到的断言缺失:完全同意,此前删了 4 个 commit 没有任何测试报警。新增 4 个契约测试 pin 住它:两个 finish 分支必须返回 dict(含类型断言)、re-ask 以最后一次应答覆盖、burned 路径 remote 归零而 local 保留。

# Per kv_cache_group block table, in each group's own block_size units.
block_ids_per_group: List[List[int]]
# Tokens accounted for by the ledger (prompt + scheduled decode steps).
token_len: int

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

小建议:token_len 目前看起来已经成为 dead state——生产代码会初始化/递增它,但 save watermark 已改为读取 len(vllm_request.all_token_ids),没有实际消费者。若没有计划用它做 telemetry/invariant,建议删除字段、更新逻辑和只验证该字段的测试;若要保留,请改成能表达真实语义的名字(例如 num_scheduled_tokens),并说明它相对 all_token_ids 的一 token lag。


locations = response["locations"]
write_session_id = response["write_session_id"]
logger.info("req:%s save session %s: block_mask=%s locations=%d",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

小问题:block_mask 可能包含覆盖整个目标 prefix 的 bool_masks.values。长请求或非连续写入时,每次 incremental save 都在 INFO 打完整向量,日志量会很大,累计开销也可能随 prefix 增长。建议降到 DEBUG,INFO 只记录 mask 类型、offset、total/existing/missing counts。


# Spec group names advertised at registration and used per key in
# start_write_cache. See build_spec_groups for the semantics.
ATTN_SPEC_GROUP = "attn"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里的命名容易反转读者的直觉:attn 实际是基础 Attention KV,而 full 是 Attention KV + 所有 recurrent state 的并集;KVCM 现有术语更接近原子组 Full(普通 Attention KV)/ Linear(线性 Attention / recurrent state)。建议统一为内部 FullAttentionGroupMeta / MambaStateGroupMeta,KVCM 原子 spec group 沿用 Full / Linear,并集用 All(比 Complete 更直接)。至少应避免再形成一套 attn/full 含义相反的协议词汇。

lpdink added 7 commits August 25, 2026 17:40
…hybridness

vLLM's invalid-block recovery unpacks a single-group block table
('(req_block_ids,) = get_block_ids(req_id)' in
_update_requests_with_invalid_blocks), so report_failures must key on
the table count, not on whether a state group exists: a multi-group
attention-only model (unmerged sw+full, or a skipped EAGLE/MTP drafter
group) is not hybrid yet still breaks the unpack and crashes the
scheduler.

Worker: report_failures = len(load_req.all_block_ids) == 1, with the
single-table branch asserting the transferred group is attention group 0
(a lone state group would feed state block ids into upstream's
token-granular recovery math).

Scheduler: _external_match_burned applies the one-shot burn by the same
shape, read from the block-table snapshot recorded at allocation time
(mirrors kv_cache_manager.get_block_ids; the group count is static).

Tests: worker-side table shapes (single attention / multi attention /
skipped drafter / hybrid / lone state group) and scheduler-side burn
shapes (skipped drafter and multi-attention now burn like hybrid).
…lots earlier

The pool fix bounded staging at 128 blocks (one full task): with the
synchronous SDK transfer holding slots, that serializes the SDK feed to
a single task and queues burst loads behind saves -- measured -3.5% ab
throughput and +54% vs +65% TP2 hit throughput against 512 blocks. The
GPU mirror is gone (zero-VRAM data path), so the pool costs pinned host
RAM only: ~896 MiB per attention group at 1024, which is origin/main's
validated concurrency (8 full tasks in flight). Host RAM is cheap;
restore 1024 as the default and document the sizing rationale in the
config comment.

Also wait for the engine's forward event *before* acquiring a slot: the
slot is only needed for gather + SDK transfer, so holding it during the
forward pass only extends pool occupancy.
…ne new_block_ids

Two hot-path fixes from the static-check pass (ty/ruff):

1. update_state_after_alloc unpacked consume_locations() into a tuple
   whose Optional return was never checked -- a None would raise
   TypeError from the unpack itself, and the dead 'is None' branch
   below would silently let vLLM run the request on KV that was never
   loaded. vLLM only allocates for an external hit after the match hook
   answered positively, so None here is a contract violation: fail
   closed with request context instead of corrupting the request's
   output without a traceback.

2. _ingest_scheduled_reqs replaced a resumed request's block table
   unconditionally ([list(b) for b in new_block_ids]), but upstream
   CachedRequestData.new_block_ids may be None (get_block_ids with
   allow_none=True when no group got fresh blocks, PR #23262): the
   replace would crash on None. Skip None entries -- nothing was
   allocated to record -- and let the incremental branch narrow the
   type for the zip.

Also document the MultiResult flatten invariant with an explicit assert
(ty cannot see the count check guarantees every slot is filled).
…k, identity-guarded writes

The four-tuple cache key (67e1a98) restored origin/main's key identity,
but origin/main's key serves a TTL cache that is *read* by its single
monolithic consumer; the split connector *pops* answers across two hooks
(match has the offset, alloc does not), and multi-entry coexistence then
needs a routing patch (_last_answered) whose correctness silently
depends on match/alloc adjacency. vLLM's contract makes coexistence
pointless: the alloc hook always consumes the answer of the *last* match
hook -- it passes the matched count back verbatim -- so only the newest
ask's answer can ever be consumed.

Replace the per-key entry dict and the _last_answered router with one
query slot per request:

* re-asking the slot's own offset deduplicates (in flight -> wait,
  answered -> serve);
* asking a different offset supersedes the slot: the newest ask wins
  and the old ask's answer -- arrived or in flight -- can no longer be
  consumed. This also removes the 05714af-era serialization where a
  grown offset waited behind the older offset's in-flight query;
* late answers never beat newer asks: each ask captures the slot object
  it created, and the async callback writes only if the slot is still
  that object (identity is the version; a monotonic counter is kept for
  logs only). Superseded/consumed/invalidated slots discard the answer.

store_result gets simpler too: the slot is necessarily the query the
hook just answered (the scheduler thread is the only superseder), so
the clamp writes the slot directly -- the mis-addressed-write failure
mode disappears with the router.

Tests: rewrite test_location_query.py around the supersession contract
(same-offset dedupe, immediate RPC for a grown offset, superseded slots
not servable, late answers dropped in both orders, consumed/invalidated
slots discarding in-flight answers).
…n_len, tame mask logs

Three review follow-ups in the scheduler-role connector:

* scheduled_new_reqs / cached_reqs with no ledger were silently
  skipped. update_state_after_alloc creates the ledger before vLLM can
  schedule anything, so this only fires on a broken hook-order
  contract: log it loudly (with resumed state and scheduled tokens)
  instead of dropping the block table invisibly.

* RequestLedger.token_len was dead state: initialized and incremented
  but never read (save sizing counts blocks from all_token_ids, whose
  one-token decode lag was the reason token_len-derived counts were
  dropped). Remove the field, its maintenance, and its tests.

* start_write_cache logged the full block_mask vector at INFO on every
  incremental save. Summarize (mask kind, offset, total/existing) at
  INFO and keep the full vector at DEBUG.
…p constants

Two review follow-ups in group parsing:

* a mamba-only model passed parse_groups (its MambaSpec groups are
  legal) and then died in register_kv_caches with an obscure
  StopIteration on the 'first attention tensor' lookup. Refuse it
  explicitly before init: pure-mamba / attention-free models are not
  supported, full-attention and hybrid (attention + mamba) only. The
  empty-group case (all groups skipped) also raises instead of
  asserting.

* ATTN_SPEC_GROUP / FULL_SPEC_GROUP read backwards: 'attn' is
  attention-only coverage and 'full' is the union of attention KV plus
  every recurrent state group. Rename the Python constants to
  ATTN_ONLY_SPEC_GROUP / ALL_SPEC_GROUP. The wire strings are frozen
  protocol -- the manager keys on the 'full' prefix (meta_searcher.cc)
  and its tests / the optimizer client emit these literals -- so only
  the code-side names move.
…2e venv model

The connector imports pydantic (config) and orjson (tp_coordinator) at
runtime, but the bazel dependency closure never declared them: the
vllm_connector py_library listed only in-repo deps, and the py312 unit
tests only passed because their runtime python (a venv) supplied the
imports. Close the gap:

* add pydantic==2.13.4 and orjson==3.12.0 to the pip_cpu requirements
  base and regenerate the lock (cp312 hashes);
* declare @pip_cpu//pydantic and @pip_cpu//orjson in vllm_connector,
  and the wheel's PyPI requires so pip resolves them on install;
* document in integration_test/vllm_e2e/BUILD that vLLM/torch/triton
  are venv-provided by design (manual + GPU-tagged targets, connector
  under test runs from source via PYTHONPATH).
@lpdink

lpdink commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

Review 意见处理汇报(2026-08-25)

谢谢 @wangxiyu191 的详细 review。基于 05714af0 的意见已逐条处理,8 条 inline comment 中 7 条已落地,剩余 1 条在调研中:

已修复(commit 对应)

意见 Commit
① load failure recovery 按 block-table 形状判断(worker + scheduler 双侧) 591699e9
② bytes 维度有界 staging / backpressure,GPU bounce buffer 评估结论:不必要(已移除,kernel 直读 pinned 零拷贝);池默认恢复 1024 块(origin/main 验证值,512 追平的 perf 结论写进 config 注释) a973a012
consume_locations() 的 None unpack:先存后解包,契约破坏 fail closed(带 req_id 与 external token 上下文) 23d1cc0a
④ async query callback 绑定 entry:查询缓存重构为单槽位 + 对象身份(offset 不再寻址,最新 ask 换代覆盖,旧答案不可写回新槽位;) 4692e772
⑤ 无 ledger 的 scheduled request:不再静默 continue,带 req_id/resumed/scheduled-tokens 的 warning bbe8643f
token_len dead state:字段、维护逻辑与相关测试删除 bbe8643f
block_mask 日志:INFO 只记 mask 类型/offset/total/existing,完整向量降 DEBUG bbe8643f
attn/full 命名:Python 侧改为 ATTN_ONLY_SPEC_GROUP / ALL_SPEC_GROUPwire 字符串冻结(manager meta_searcher.cc 依赖 "full" 前缀,协议不改) 14c2f2cc
纯 Mamba 初始化前明确拒绝(原路径在 register_kv_caches 以 StopIteration 晦涩崩溃) 14c2f2cc
pydantic/orjson 进入 pip_cpu 依赖闭包 + vllm_connector deps + wheel requires;e2e 的 torch/vLLM venv 模型写入 BUILD 注释 75bc18a6

顺带发现并修复

  • 静态检查(ty/ruff)扫出 _ingest_scheduled_reqsresumed 分支对 new_block_ids=None 无保护(上游 get_block_ids(allow_none=True) 在无新块时返回 None,会 TypeError crash)—— 已修 + 两个 interface 的回归测试(23d1cc0a)。

未决事项

  • local/remote_matched_token_num 观测结构:正在核对 origin/main 的返回结构与 vLLM kv_transfer_params 透传链,确认后要么保留原结构、要么给出等价 per-request 观测,会再回复。
  • MLA:决策为不支持(parse_groups 已显式 raise),支持矩阵文档将写明,不承诺恢复支持。
  • 跨文件结论:hybrid load failure 的 Known Limitation 已在代码 docstring 与 PR 描述披露(后续补一个醒目的失败计数器);mirror 删除的「不回退」验证与 serialization version 建议在准备中;rebase main 与支持矩阵刷新会在上述事项闭环后进行。

单测:py_connector 全套 5/5 绿(test_scheduler_state / test_location_query / test_data_transfer_results / test_kv_layouts / test_block_translation)。

lpdink added 2 commits August 26, 2026 10:08
…params

The ledger rewrite (cbe1bc4) dropped ReqState's local/remote matched
token counts together with the mirror, and _finish_request started
returning None instead of the accounting dict -- silently removing an
observable contract: vLLM threads request_finished's extra_info into
EngineCoreOutput.kv_transfer_params -> RequestOutput -> the OpenAI
response, where consumers attribute per-request TTFT and hits to the
local (vLLM prefix cache) vs remote (KVCM) sources. Found only by human
review four commits later; nothing in the unit suite pinned it.

Restore it on the ledger, group-agnostic by design (both counters are
token-granular totals, orthogonal to per-block spec coverage):

* the match hook records (local = the num_computed_tokens it was asked
  at, remote = the clamped answer) on every answer path;
* the burned-match re-ask reports local as asked and remote = 0 (the
  match is spent), matching the pre-rewrite semantics;
* _finish_request returns the dict from both branches (immediate retire
  and delayed-finish).

New tests pin the contract: both finish branches must return the dict,
re-asks overwrite with the last answer, and the burned path zeroes the
remote half while keeping the local hit.
Resolve the one conflict in cache_manager_test.cc by keeping both
sides' additions: our TestStartWriteCacheSpecGroupNamesWithTokenIdsOnly
(token-only writes with per-block spec-group names, hybrid regression)
and main's TestStartWriteCacheRecordWriteBytes (write-bytes metric
accounting) are independent tests registered against different
instances.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d265c8cc62

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +41 to +43
for name in ("torch", "triton", "triton.language", "zmq", "requests"):
if name not in sys.modules and not _importable(name):
sys.modules[name] = MagicMock(__name__=name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Give the requests stub real exception classes

When requests is absent—as intended for the dependency-light unit-test environment—this installs a MagicMock as the module, so requests.HTTPError and requests.RequestException are mock objects rather than exception classes. Importing manager_client.py then fails while defining KvCacheManagerHTTPError(requests.HTTPError, AssertionError) with a metaclass conflict, and every new connector test aborts during collection. Provide a minimal requests stub with real Exception subclasses or declare the real requests dependency for these tests.

Useful? React with 👍 / 👎.

Comment on lines +78 to +79
self._cpu = torch.empty(total, dtype=torch.uint8, device="cpu",
pin_memory=(device.type == "cuda"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pin staging buffers on MUSA devices

When the connector runs on the explicitly supported MUSA path selected by _get_device_module, this condition allocates ordinary pageable CPU memory even though the new attention transfer kernels directly dereference the staging pointer from the accelerator via UVA. Unlike CUDA-pinned—or MUSA-pinned—host storage, pageable memory is not device-addressable, so attention save/load tasks fail or fault as soon as the gather/scatter kernel uses the pool. Allocate accelerator-pinned host memory for MUSA as well, while retaining pageable memory only for CPU/test devices.

Useful? React with 👍 / 👎.

Comment on lines +337 to +346
# Wait for the engine's forward pass *before* taking a pool slot:
# the slot is needed only for the gather + SDK transfer, so holding
# it while the model still runs only extends the pool occupancy and
# queues concurrent loads behind a save that is not even staging.
ready_event.wait()
pool = self._pools[group.spec_name]
start = pool.acquire(len(valid))
try:
cpu_buffer = pool.cpu_view(start, len(valid))
with self._device_mod.stream(self._save_stream):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Make the save stream wait for forward completion

During normal asynchronous accelerator execution, ready_event.wait() without an explicit stream inserts the dependency into the IO executor thread's current stream, but the gather/state copies are subsequently enqueued on the separate _save_stream. The save stream can therefore read paged KV/state before the model's forward writes represented by ready_event have completed, publishing stale or partially written cache bytes. Insert the wait while _save_stream is current, or host-synchronize the event before gathering.

Useful? React with 👍 / 👎.

…ock count

The 1024-block default (a973a01) was derived for full-attention blocks
(~0.875 MiB each: 1024 blocks ~= 896 MiB of pinned host RAM per group).
Hybrid blocks are ~17.3 MiB, so the same count pins ~17.3 GiB per group;
four groups then die in the pinned allocator (cudaHostAlloc OOM) at
engine start -- found by the final-validation e2e smoke on Qwen3.5-4B.

Derive the per-group block count from a byte ceiling
(staging_pool_max_bytes_per_group, default 1 GiB):
  effective = clamp(staging_pool_blocks, byte_cap // block_bytes,
                     floor = one full task batch)
Full-attention sizing is unchanged (1024 blocks, the validated 8-task
concurrency); hybrid falls back to the batch size (128 blocks), the
configuration the staging-removal campaign validated. One task batch
always fits contiguously; the cap can only shrink, never grow.
@lpdink

lpdink commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

池容量修复与 merge main(最终更新)

d265c8cc:merge 最新 main(35 commits,唯一冲突在 cache_manager_test.cc——两侧各自新增的测试均保留:我们的 TestStartWriteCacheSpecGroupNamesWithTokenIdsOnly 与 main 的 TestStartWriteCacheRecordWriteBytes)。merge 后 py_connector 单测 5/5、CacheManagerTest(C++,含两侧新测试)全绿。

0db89c9astaging 池改为按每组 pinned 字节上限定容staging_pool_max_bytes_per_group,默认 1 GiB)。e2e smoke 在 Qwen3.5-4B 上抓到 a973a012 恢复的 1024 块默认在 hybrid 下按块数定容的回归:hybrid 块 17.3 MiB(state 组 = 8 层 × 2.06 MiB/层 fp32 矩阵快照;manager 块对齐 528)→ 4 组 × 1024 块 = 66 GiB pinned,cudaHostAlloc 在引擎启动即 OOM。注意 pinned 的真实约束是 GPU BAR1 映射窗口(A10 远小于此),不是 host RAM 余量。修复后:

  • 生效块数 = max(max(block_per_save_task, block_per_load_task), min(staging_pool_blocks, cap // block_bytes))
  • full-attention 模型完全不受影响(896 MiB < 1 GiB → 维持 1024 块,与既有 perf 数据可比);
  • hybrid 各组回到 ~128 块(cap + task 下限共同作用),警告日志打出实际 pinned 量;
  • 单测 pin 住该契约。

当前 PR 状态:wangxiyu191 的 8 条 inline 意见全部落地(#1 block-table 形状 591699e9#2 bytes 维度 staging a973a012+0db89c9a#3 fail-closed 23d1cc0a#4 单槽位+对象身份查询缓存 4692e772;⑤⑥⑦ ledger 卫生 bbe8643f;⑧ 命名 14c2f2cc;matched_token_num 恢复 5f033efe);纯 Mamba 拒绝、pydantic/orjson 依赖闭包、new_block_ids=None 守卫一并修复。merge main 后全部单测 + CacheManagerTest 绿,hybrid/full e2e 冒烟通过,最终 e2e 矩阵与 perf 正在跑(结果将在此追加)。

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0db89c9a33

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +239 to +242
env["PYTHONPATH"] = os.pathsep.join([
os.path.dirname(os.path.abspath(__file__)),
find_repo_root(),
env.get("PYTHONPATH", ""),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the installed pybind extension on PYTHONPATH

In the documented e2e setup, adding the checkout root ahead of site-packages causes Python to load the source tree's regular kv_cache_manager package, whose client/pybind directory contains no kvcm_py_client extension; the compiled module exists only in the installed wheel/Bazel output. Consequently, importing connector_worker.py fails before vLLM starts, so the e2e targets cannot exercise the source connector as intended. Stage the extension alongside the source package or extend the package path instead of shadowing the installed package wholesale.

Useful? React with 👍 / 👎.

…ed fix)

The merge brought in main's manager_client exceptions, which subclass
requests' (KvCacheManagerHTTPError(requests.HTTPError, AssertionError)).
In the open-source CI -- no requests installed -- vllm_stubs replaced
requests with a MagicMock module, and a MagicMock attribute cannot be
a base class: class creation died with 'metaclass conflict' before a
single test ran.

Give the requests stand-in real exception classes mirroring requests'
own hierarchy (RequestException(IOError) -> HTTPError/ConnectionError/
Timeout); Session/post stay MagicMocks because the tests patch them.
Record stand-ins in vllm_stubs.STUBBED so tests can skip on real-
behaviour requirements: test_views_slice_the_same_run needs an actual
torch tensor and now skips when torch is stubbed.

Also import vllm_stubs before the bare 'import torch' in
test_data_transfer_results (the stub must be in sys.modules first in
the CI environment), and decouple the zero-VRAM allocation guard from
torch.device -- a SimpleNamespace(type='cuda') drives the same
contract check wherever torch itself is a stand-in.

Verified in both environments: the no-deps simulation (meta-path
blocker over the five CI targets: 132 tests, 0 failures) and the full
bazel suite on the dev venv (5/5).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants