Skip to content
Open
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
3bd6ea0
[py_connector] rewrite vLLM connector around per-group transfer for h…
lpdink Jul 27, 2026
991d0b5
[integration_test] add vLLM e2e KV verification for full + hybrid att…
lpdink Jul 27, 2026
f702cd6
[py_connector] cap full-prompt external hit and fix canceled-save Key…
lpdink Jul 28, 2026
e0718d9
[py_connector] add unit tests for translation, transfer results and s…
lpdink Jul 28, 2026
d462dcb
[py_connector] fix load-failure retry loop and null mamba state transfer
lpdink Jul 28, 2026
645e58a
[integration_test] harden vllm e2e harness and fix hybrid prompt cove…
lpdink Jul 28, 2026
f909e82
[integration_test] add mutation meta-test and four vllm e2e scenarios
lpdink Jul 28, 2026
4052b4c
[py_connector] make connector unit tests runnable without torch
lpdink Jul 29, 2026
729e95f
[integration_test] drop dev-machine assumptions from the vllm e2e har…
lpdink Jul 29, 2026
9dc5961
[py_connector] restore pre-0.26 vLLM KV layouts via shape-based detec…
lpdink Jul 29, 2026
031a784
[integration_test] add missing requests dep to vllm e2e py_library
lpdink Jul 30, 2026
9af6081
[integration_test] bind vllm e2e harness sockets to loopback only
lpdink Jul 30, 2026
1b8c148
[py_connector] index block tables by transferred group, not raw position
lpdink Jul 30, 2026
09582a9
[py_connector] size attention transfers from the compact page size
lpdink Jul 30, 2026
a3fa71d
[py_connector] reject windowed FullAttentionSpec instead of mispublis…
lpdink Jul 30, 2026
d2998c1
[py_connector] fail closed when the hybrid capability probe is inconc…
lpdink Jul 30, 2026
9925b52
[py_connector] bound pinned staging memory with a byte budget
lpdink Jul 30, 2026
ec86f36
[py_connector] document hybrid load-failure and none-mode state limits
lpdink Jul 30, 2026
0d70939
[integration_test] add cross-request prefix e2e scenario for null mam…
lpdink Jul 31, 2026
3979907
[py_connector] publish per-block spec coverage instead of faking stat…
lpdink Jul 31, 2026
6238d3b
[py_connector] replace vacuous-success unit tests with abstention sem…
lpdink Jul 31, 2026
c0a1a48
[manager] count blocks, not block_keys, when validating spec group names
lpdink Jul 31, 2026
1b9b792
[integration_test] pick the always-present spec when resolving block …
lpdink Jul 31, 2026
3c2bf77
[py_connector] re-truncate external match after the full-hit cap
lpdink Aug 17, 2026
4039096
[py_connector] report all-failed when a transfer task crashes
lpdink Aug 17, 2026
4eec9b3
[py_connector] make get_num_new_matched_tokens self-explanatory
lpdink Aug 17, 2026
ab89a1b
[py_connector] document why hybrid load failures stay unreported
lpdink Aug 17, 2026
97b6265
[py_connector] fast-fail re-queries on explicit load failure, not att…
lpdink Aug 17, 2026
b336061
[py_connector] split the vLLM connector into per-role cores
lpdink Aug 17, 2026
060dac9
[py_connector] split build_connector_meta into single-purpose stages
lpdink Aug 18, 2026
7bd0413
[py_connector] revert the connector-level pinned staging budget
lpdink Aug 18, 2026
cb7fe68
[py_connector] split transfer types by group kind with explicit KV la…
lpdink Aug 18, 2026
4dcb734
[py_connector] encapsulate TransferPlan and make task slicing read-only
lpdink Aug 18, 2026
95fc44f
[py_connector] drop the worker-side request mirror
lpdink Aug 18, 2026
cbe1bc4
[py_connector] replace the request mirror with a ledger and side tables
lpdink Aug 18, 2026
f0b9c25
[py_connector] rename cores to ConnectorScheduler/ConnectorWorker wit…
lpdink Aug 18, 2026
9418a10
[integration_test] split the vllm e2e harness and adapt it to the cor…
lpdink Aug 18, 2026
93fcb63
[py_connector] document the gather/scatter kernel's two addressing modes
lpdink Aug 18, 2026
5e7f6de
[py_connector] fix the source-checkout run path and residual split re…
lpdink Aug 18, 2026
c91fab9
[py_connector] count save blocks by key material, not scheduled tokens
lpdink Aug 18, 2026
b8ddf63
[integration_test] OR the hybrid detection signals and guard the muta…
lpdink Aug 18, 2026
05714af
[py_connector] dedupe in-flight getCacheLocation re-asks
lpdink Aug 19, 2026
67e1a98
[py_connector] key location queries by full identity, not req_id alone
lpdink Aug 20, 2026
a0d6289
[py_connector] stage transfers from a bounded pool, not per-task allocs
lpdink Aug 20, 2026
715a655
[py_connector] drop the GPU side of the staging pool, zero-VRAM data …
lpdink Aug 20, 2026
591699e
[py_connector] gate load-failure reporting on block-table shape, not …
lpdink Aug 25, 2026
a973a01
[py_connector] restore the 1024-block staging pool and release save s…
lpdink Aug 25, 2026
23d1cc0
[py_connector] fail closed on a broken match/alloc contract; guard No…
lpdink Aug 25, 2026
4692e77
[py_connector] one query slot per request: supersede by the newest as…
lpdink Aug 25, 2026
bbe8643
[py_connector] scheduler hygiene: log unledgered reqs, drop dead toke…
lpdink Aug 25, 2026
14c2f2c
[py_connector] refuse pure-mamba models before init; rename spec-grou…
lpdink Aug 25, 2026
75bc18a
[deps] close the pydantic/orjson bazel dependency gap; document the e…
lpdink Aug 25, 2026
5f033ef
[py_connector] restore the per-request hit accounting in kv_transfer_…
lpdink Aug 26, 2026
d265c8c
Merge branch 'main' into feature/vllm-hybrid-attention-rewrite
lpdink Aug 26, 2026
0db89c9
[py_connector] cap the staging pool by pinned bytes per group, not bl…
lpdink Aug 26, 2026
10bf0e8
[py_connector] make the test stubs survive requests subclassing (CI r…
lpdink Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions integration_test/vllm_e2e/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package(default_visibility = ["//integration_test:__subpackages__"])

# Shared library: orchestration (manager + vLLM + driver + comparison) and the
# verifying connector injected into vLLM via kv_connector_module_path.
#
# NOTE: vLLM / torch / triton are NOT bazel deps: the e2e harness runs the
# tests inside a dedicated venv (one per supported vLLM era) and puts the
# worktree root first on PYTHONPATH -- the connector under test runs from
# source, and torch/vLLM resolve from the venv. These targets are manual +
# GPU-tagged for the same reason; bazel only stages the driver and the
# manager binary.
py_library(
name = "e2e_lib",
srcs = [
"e2e_lib.py",
"lib_utils.py",
"servers.py",
"test_connector.py",
],
imports = ["."],
tags = ["no-remote-exec"],
deps = ["@pip_cpu//requests"],
)

py_test(
name = "test_basic",
timeout = "eternal",
srcs = ["test_basic.py"],
data = [
"//kv_cache_manager:kv_cache_manager_bin",
],
imports = ["."],
tags = [
"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",
],
deps = [":e2e_lib"],
)

py_test(
name = "test_concurrent",
timeout = "eternal",
srcs = ["test_concurrent.py"],
data = [
"//kv_cache_manager:kv_cache_manager_bin",
],
imports = ["."],
tags = [
"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.

],
deps = [":e2e_lib"],
)

py_test(
name = "test_tp",
timeout = "eternal",
srcs = ["test_tp.py"],
data = [
"//kv_cache_manager:kv_cache_manager_bin",
],
imports = ["."],
tags = [
"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",
],
deps = [":e2e_lib"],
)

py_test(
name = "test_full_hit",
timeout = "eternal",
srcs = ["test_full_hit.py"],
data = [
"//kv_cache_manager:kv_cache_manager_bin",
],
imports = ["."],
tags = [
"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",
],
deps = [":e2e_lib"],
)

py_test(
name = "test_partial_hit",
timeout = "eternal",
srcs = ["test_partial_hit.py"],
data = [
"//kv_cache_manager:kv_cache_manager_bin",
],
imports = ["."],
tags = [
"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",
],
deps = [":e2e_lib"],
)

py_test(
name = "test_load_failure",
timeout = "eternal",
srcs = ["test_load_failure.py"],
data = [
"//kv_cache_manager:kv_cache_manager_bin",
],
imports = ["."],
tags = [
"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",
],
deps = [":e2e_lib"],
)

py_test(
name = "test_multi_turn",
timeout = "eternal",
srcs = ["test_multi_turn.py"],
data = [
"//kv_cache_manager:kv_cache_manager_bin",
],
imports = ["."],
tags = [
"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",
],
deps = [":e2e_lib"],
)

py_test(
name = "test_cross_request_prefix",
timeout = "eternal",
srcs = ["test_cross_request_prefix.py"],
data = [
"//kv_cache_manager:kv_cache_manager_bin",
],
imports = ["."],
tags = [
"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",
],
deps = [":e2e_lib"],
)

# Meta-test: injects an off-by-one into the connector's token translation and
# asserts the KV verification FAILS -- proof the harness is not vacuous.
py_test(
name = "test_mutation",
timeout = "eternal",
srcs = ["test_mutation.py"],
data = [
"//kv_cache_manager:kv_cache_manager_bin",
],
imports = ["."],
tags = [
"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",
],
deps = [":e2e_lib"],
)

# Wildcards skip "manual" targets; run the whole suite explicitly with
# bazelisk test //integration_test/vllm_e2e:e2e_tests
test_suite(
name = "e2e_tests",
tags = ["manual"], # keep wildcard builds from expanding into the GPU tests
tests = [
":test_basic",
":test_concurrent",
":test_cross_request_prefix",
":test_full_hit",
":test_load_failure",
":test_multi_turn",
":test_mutation",
":test_partial_hit",
":test_tp",
],
)
121 changes: 121 additions & 0 deletions integration_test/vllm_e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# vLLM <-> KVCM End-to-End KV Cache Verification

End-to-end integration tests for the KVCM vLLM connector
(`kv_cache_manager/py_connector/vllm`). Each test starts a real KVCM manager
(local-file storage backend) and a real vLLM OpenAI server, drives prompts
through the OpenAI API and verifies that the KV cache data saved to / loaded
from KVCM is correct.

Requires 1-2 GPUs and vLLM (0.22.1, 0.23.0 and 0.26.0 are e2e-verified; the
connector detects the KV cache layout of each era from the tensor shape).

## What is verified

The connector translates between three block spaces per `kv_cache_group`:

```
KVCM manager block idx -> global token idx -> group logical block
(step 1, connector-only) (step 2/3, shared with vLLM)
```

A bug in step 1 is *symmetric*: save gathers from the wrong slots and load
scatters back to the same wrong slots, so a transport round trip alone cannot
detect it. The test breaks the symmetry with `VerifyingConnector`
(`test_connector.py`), a subclass of the production connector that
independently captures KV data from vLLM's paged cache using only vLLM's own
block-table mapping:

1. **Phase 1** — fresh prompts: prefill -> connector saves to KVCM. The saved
token ranges are captured from the paged cache (**reference** captures).
2. **Phase 2** — same prompts + suffix: connector reports an external match and
loads from KVCM. The loaded blocks are captured (**loaded** captures).
3. The driver (`e2e_lib.py`) matches loaded captures against references by
token content and compares per layer, requiring bit-exact equality (the
transfer is a verbatim byte round trip; all scenarios achieve it).

## Model coverage

The same test targets run against either model kind, selected by
`KVCM_E2E_MODEL`:

| Kind | Example | Groups | Orchestration |
|---|---|---|---|
| Full attention | Qwen2.5-7B-Instruct | 1 `FullAttentionSpec` | prefix caching off, one server for both phases |
| Hybrid | Qwen3.5-4B | 3 `MambaSpec` + 1 `FullAttentionSpec` | prefix caching on (`mamba_cache_mode="align"`), server restarted between phases so phase 2 loads from KVCM instead of the local prefix cache |

Hybrid specifics verified:

* Per-group location specs (`tp{rank}_g{group}`) and per-group block tables.
* Attention groups: token-granular gather/scatter through the Triton kernel.
* Mamba/linear groups: per-block opaque state copy, where a manager block's
*last* token selects the state block (`_state_block_ids`).

## Scenarios

| Test | TP | Prompts | Notes |
|---|---|---|---|
| `test_basic` | 1 | 1 | Minimal save -> load round trip |
| `test_concurrent` | 1 | 4 | Concurrent requests: ReqState tracking, per-request block attribution |
| `test_tp` | 2 | 2 | TP coordination; for full-attention models also `preferred_block_size=32` != vLLM block size (16), forcing real cross-block translation |
| `test_partial_hit` | 1 | 1 | Phase 2 extends the prompt mid-block: partial external hit |
| `test_full_hit` | 1 | 1 | Phase 2 resends the identical prompt: full-prompt hit is capped so >= 1 token is recomputed |
| `test_multi_turn` | 1 | 1 | Growing conversation: each turn loads the previous turns' blocks and saves new ones |
| `test_cross_request_prefix` | 1 | 2 | Request B is a strict token prefix of saved request A, ending inside one of A's blocks. Hybrid: B's match must be truncated to the last block whose recurrent state was really materialized, and B's output must be token-identical to a no-cache reference |
| `test_load_failure` | 1 | 1 | Storage files deleted between phases: load fails, retry loop must not spin, request still completes |
| `test_mutation` | 1 | 1 | Meta-test: injected off-by-one in the slot translation must make verification FAIL (proves the harness is not vacuous) |

## Running

These targets are tagged `manual`: they need a GPU machine with a prepared
vLLM venv and a local model, so `bazelisk test //integration_test/...` skips
them and they must be requested explicitly (see below).

Build prerequisites (from the repo root):

```bash
bazelisk build //kv_cache_manager:kv_cache_manager_bin \
//kv_cache_manager/client/pybind:kvcm_py_client_lib_wheel \
//kv_cache_manager/py_connector/vllm:kvcm_vllm_connector_wheel \
--per_file_copt='external/jsoncpp_git/.*@-Wno-error'
```

Install both wheels into the vLLM venv (rename them first: the Bazel output
name contains unstamped `{STABLE_*}` template variables; read the real version
from the wheel's `METADATA`).

Run (tagged `exclusive`, so they execute serially):

```bash
bazelisk test //integration_test/vllm_e2e:e2e_tests \
--cache_test_results=no --test_output=errors \
--test_env=KVCM_E2E_PYTHON=/path/to/vllm-venv/bin/python \
--test_env=KVCM_E2E_MODEL=/path/to/model \
--per_file_copt='external/jsoncpp_git/.*@-Wno-error'
```

## Environment variables

All environment variables used by the e2e harness:

| Variable | Required | Meaning |
|---|---|---|
| `KVCM_E2E_MODEL` | yes | Path to a local HF model directory (`config.json` + weights). Full-attention coverage needs a plain attention model (e.g. Qwen2.5-7B-Instruct); hybrid coverage needs a mamba/linear + attention model (e.g. Qwen3.5-4B). Hybrid models are auto-detected from `config.json`. |
| `KVCM_E2E_PYTHON` | yes | Python interpreter of a venv with vLLM (any supported version, see above) and both KVCM wheels (`kvcm_py_client`, `kvcm_vllm_connector`) installed. |
| `KVCM_E2E_CAPTURE_DIR` | internal | Set by the driver for the vLLM subprocess; tells `VerifyingConnector` where to write `.pt` captures. Do not set manually. |

The driver also sets vLLM knobs for the spawned server (`VLLM_KV_CACHE_LAYOUT=NHD`,
`VLLM_ATTENTION_BACKEND=FLASH_ATTN`, `VLLM_USE_FLASHINFER_SAMPLER=0`,
`FLASHINFER_DISABLE_VERSION_CHECK=1`) via `env.setdefault`, so a value you
export yourself wins.

## Debugging

Bazel's `test.log` only shows the driver's view (e.g. HTTP 500). The real
tracebacks live in the scenario workdir under `$TEST_TMPDIR`:

```
<TEST_TMPDIR>/kvcm_vllm_e2e/<scenario>/
manager/manager.stdout|stderr # KVCM manager
vllm/vllm*.stdout|stderr # vLLM (EngineCore tracebacks are here)
captures/{ref|loaded}_tp{rank}_{token_hash}.pt
```
17 changes: 17 additions & 0 deletions integration_test/vllm_e2e/e2e_lib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Compatibility facade over the split harness modules.

Pure helpers live in lib_utils.py, process lifecycle (manager binary,
vLLM server, ScenarioEnv) in servers.py; this module re-exports the
original surface so existing scenario files keep their imports.
"""

from lib_utils import ( # noqa: F401
is_hybrid_model, _runfiles_root, find_repo_root, find_manager_binary,
find_python, free_port, wait_http, tokenize, get_manager_block_size,
block_token_hash, full_block_hashes, wait_for_prefix_cached,
send_completions, count_captures, wait_for_captures, compare_captures,
assert_report_ok, make_base_prompts, shared_token_prefix_len,
)
from servers import ( # noqa: F401
ManagerProcess, VllmServer, ScenarioEnv, run_e2e,
)
Loading
Loading