Skip to content

[Bugfix][kv-events] Making partial blocks emit all tail hashes - #62195

Open
Lucas-Fernandes-Martins wants to merge 5 commits into
vllm-project:mainfrom
Lucas-Fernandes-Martins:lfm/report-partial-kv-tail-events
Open

Lucas-Fernandes-Martins wants to merge 5 commits into
vllm-project:mainfrom
Lucas-Fernandes-Martins:lfm/report-partial-kv-tail-events

Conversation

@Lucas-Fernandes-Martins

@Lucas-Fernandes-Martins Lucas-Fernandes-Martins commented Oct 17, 2026 •

Copy link
Copy Markdown

Overview

Issue: partial full-attention cache events can reference a parent hash that was never published, as described in #58119.

Solution: have BlockStored contain all completed tail hashes of a partial full-attention block.

Claims

We fix #58119, which is particularly important for llm-d's precise KV cache report mode, in models which use prefix_match_unit, such as kimi-k3.

Validation

Conflict-resolution validation (2026-10-26; AI-assisted)

Merged upstream 71294c2e9a in dbf6ca50ed. Upstream #61589 added EAGLE lookahead-token metadata in the same event path. The resolution emits the corresponding lookahead tokens and multimodal extra keys for every published tail hash, retaining the existing local-lookup behavior and full-attention-only switch.

Extended the existing tests for lookahead lengths 0, 1 and 2, including images overlapping lookahead windows. The manager regression covers DCP world sizes 1, 2 and 4.

Validation commands below use VLLM_TEST_PYTHON to refer to the existing Python 3.12 test virtualenv. Run from the corresponding checkout:

# Candidate: 273 passed (both suites).
PYTHONPATH=. uv run --no-project --python "$VLLM_TEST_PYTHON" python -m pytest --confcutdir=tests/v1/core/prefix_cache tests/v1/core/prefix_cache/test_partial_prefix_cache_primitives.py tests/v1/core/prefix_cache/test_partial_prefix_cache_hits.py -q

# Upstream 71294c2e9a with only the updated manager test file copied in:
# all 9 cases fail on the missing-parent assertion.
PYTHONPATH=. uv run --no-project --python "$VLLM_TEST_PYTHON" python -m pytest --confcutdir=tests/v1/core/prefix_cache tests/v1/core/prefix_cache/test_partial_prefix_cache_hits.py -k test_full_attention_tail_events_preserve_local_lookup -q

Upstream failure: assert tail_event.parent_block_hash == full_event.block_hashes[-1] (9 failed, 236 deselected). Local pre-commit, including Ruff, mypy and test tethering, and commit/sign-off hooks passed. Hosted pre-commit was skipped because the upstream pre-run eligibility gate requires a qualifying label or at least four previously merged PRs; the gate found zero. These are CPU metadata/cache-manager tests using --confcutdir; no GPU serving, model evaluation or end-to-end ZMQ validation was performed during conflict resolution.

We add the following tests:

  • test_full_attention_tail_events_preserve_local_lookup: exercises cache manager with partial-prefix hits enabled. Verifies the core logic works and we emit all the hashes for all tail hashes.

  • test_dense_partial_event_splits_multimodal_extra_keys: check the main logic with image data (accounted for in extra-keys)

  • test_reported_partial_hashes_follow_block_lifecycle: checks eviction, movement, promotion to full block, replacement, and cache reset.

We add some checks on:

  • test_cache_partial_block_kv_cache_events: now also runs with report_all_hash_boundaries=True.

Details

The main motivation is to ensure consumers of kv-events can reconstruct KV cache chains. Previously, a partial block would emit an event stating a parent block that was never emitted. This is particularly consequential for precise KV cache routing on llm-d, as it relies on ZMQ kv-events to understand which pods have which KV blocks.

Hence, the main design decision is to go from this:

BlockStored(
      block_hashes=[H14],
      parent_block_hash=H12,
      token_ids=[12, 13],
      block_size=2,
      extra_keys=[
          (("B", -1),),
      ],
      group_idx=0,
      # Unrelated fields omitted.
  )

to

BlockStored(
      block_hashes=[H10, H12, H14],
      parent_block_hash=H8,
      token_ids=[8, 9, 10, 11, 12, 13],
      block_size=2,
      extra_keys=[
          (("A", -2),),             # [8, 10)
          (("A", -4), ("B", 1)),    # [10, 12)
          (("B", -1),),             # [12, 14)
      ],
      group_idx=0,
      # Unrelated fields omitted.
  )

We introduce the report_all_hash_boundaries arg on cache_partial_block.

Therefore, with the latter, the consumer of kv-events can know the parent of H14 (H12). Before, we'd just receive H14, and H12 was never announced. Consequently, H14 had a missing parent.

Most importantly, this does not impact the hashes registered for local lookup, only the hashes published in kv-events through ZMQ. Thus, there's no effect in how cache hits are computed.

Thanks @andylolu2 for helping on iterating on the solution.


Pull Request Checklist
  • [x ] I used vLLM's /pr-checklist skill. (Mandatory for agents, optional for humans).

  • [ x] AI assistance was used during the creation of this PR.
    Code had AI assistince by was reviewed manually. PR description was human written.

  • [x ] Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.

  • [ x] Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.

  • Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.

  • Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.

Publish the complete partial-tail hash chain for external cache consumers
while preserving local prefix lookup. Track published intermediate hashes
with their physical block so removal events cover every advertised hash.

Assisted-by: OpenAI Codex
Signed-off-by: Lucas-Fernandes-Martins <lucasfmartins16@gmail.com>
Co-authored-by: Andy Lo <andy@mistral.ai>
Assisted-by: OpenAI Codex
Signed-off-by: Lucas-Fernandes-Martins <lucasfmartins16@gmail.com>

@claude claude 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.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mergify mergify Bot added bug Something isn't working kv-cache-manager labels Oct 17, 2026
@github-actions

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban.

🚀

@mergify

mergify Bot commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @Lucas-Fernandes-Martins.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Oct 17, 2026
Preserve upstream event-key conversion and adapt the tail regression to the reusable prompt boundary.

Assisted-by: OpenAI Codex
Signed-off-by: Lucas-Fernandes-Martins <lucasfmartins16@gmail.com>
@mergify mergify Bot removed the needs-rebase label Oct 17, 2026
@Lucas-Fernandes-Martins Lucas-Fernandes-Martins changed the title [Bugfix][kv-events] Emitted hashes without emitting its parent [Bugfix][kv-events] Making partial blocks emit all tail hashes Oct 17, 2026

@sagearc sagearc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for digging into this @Lucas-Fernandes-Martins, amazing work

Comment on lines +535 to +550
if report_all_hash_boundaries:
block_start = round_down(num_tokens, block_size)
first_hash_idx = block_start // self.hash_block_size
block_hashes = request.block_hashes[first_hash_idx:num_hash_blocks]
parent_hash = (
request.block_hashes[first_hash_idx - 1]
if first_hash_idx > 0
else None
)
if len(block_hashes) > 1:
self.reported_block_hashes_by_block.setdefault(
block.block_id, set()
).update(
make_block_hash_with_group_id(block_hash, kv_cache_group_id)
for block_hash in block_hashes[:-1]
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is there a reason to handle a single tail hash separately from multiple tail hashes here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

So I added this condition as the final tail hash is already tracked by the cache index. But we need extra reporting for the additional tail hashes. Basically, if we didn't have the if, the only consequence would be self.reported_block_hashes_by_block dict having an entry with an an empty set as value when len(block_hashes) == 1.
Basically, self.reported_block_hashes_by_block does the accouting for all the intermediate tail hashes of a partial block, so we can emit evict events for them later.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

right, so looking up an intermediate hash returns no hit even though its tokens are present in the physical block? maybe it is worth documenting that behavior in a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Exactly, I did it this way to 'solve' the routing issue without changing the current lookup behaviour. I felt it would be slightly tricky to align with mamba events in the edge case where we the final hash block was evicted, and we lost the mamba checkpoint, but are left with a non-tail KV block.

Added the following comment block in block_pool.py before if len(block_hashes) > 1:

  # Intermediate hashes provide ancestry for KV-event consumers.
  # They are not registered as lookup aliases for this block, even when
  # the corresponding KV is present. Only the final hash is registered
  # for prefix lookup.

Happy to update other docs if necessary!

num_tokens=boundary_tokens,
kv_cache_group_id=self.kv_cache_group_id,
block_size=self.block_size,
report_all_hash_boundaries=True,

@sagearc sagearc Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

in what scenario would we want report_all_hash_boundaries=False? isn't True always preferable?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

For mamba events my reasoning is that flushing those intermediate tail hashes could be misleading. I agree that, for both full attention and mamba, the cache lookup stays the same (the additional intermediate hashes reported for the partial block are not registered for prefix lookup). But in the case of mamba, since those non-tail hash states are not retained as cache checkpoints, it seemed weird to flush them.

Either way, if you believe mamba events should get the same treatment, happy to remove the flag.

@Lucas-Fernandes-Martins

Copy link
Copy Markdown
Author

Thanks for the review @sagearc, responded to the comments! If you feel mamba should have the same behaviour of full attention in what regards flushing tail blocks, I can remove report_all_hash_boundaries.

Signed-off-by: Lucas-Fernandes-Martins <lucasfmartins16@gmail.com>
Assisted-by: OpenAI Codex
@mergify

mergify Bot commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @Lucas-Fernandes-Martins.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Oct 26, 2026
Resolve conflicts with vllm-project#61589 by generating lookahead tokens and
multimodal extra keys for each published partial-tail hash. Retain
upstream lookahead test configuration and extend regressions across
lookahead lengths 0, 1, and 2 and DCP world sizes 1, 2, and 4.

Assisted-by: OpenAI Codex
Signed-off-by: Lucas-Fernandes-Martins <lucasfmartins16@gmail.com>
@mergify mergify Bot removed the needs-rebase label Oct 26, 2026
@Lucas-Fernandes-Martins

Lucas-Fernandes-Martins commented Oct 26, 2026 •

Copy link
Copy Markdown
Author

Hi @sagearc, do you think it makes sense to add the ready-run-all-tests label so I can check it passes CI?

@sagearc

sagearc commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

cc @NickLucche @orozery

@orozery orozery added the ready ONLY add when PR is ready to merge/full CI is needed label Oct 26, 2026
@orozery

orozery commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

cc @Change72

@github-actions

Copy link
Copy Markdown

✅ @Lucas-Fernandes-Martins, CI is now available for this PR.

  • /ci run starts upstream CI; /amd-ci run starts AMD CI only.
  • Run commands test your branch with every commit currently on its upstream target branch. If it is behind, the bot merges the target branch into it first (pull before pushing again). Append --allow-stale to a run command to test an outdated branch as is, at your own risk.
  • /ci retry retries failed jobs in the CI build for the current PR head. If the current head has no CI build, it starts a new CI build for the current head containing only jobs that failed in the latest earlier CI build for this PR.
  • /amd-ci retry retries failed jobs in AMD CI for the current PR head. Use /amd-ci run when the current head has no AMD CI build.
  • /ci cancel cancels scheduled or running CI builds for this PR branch; /amd-ci cancel does the same for AMD CI only.

@Change72

Copy link
Copy Markdown
Contributor

@vMaroon, how should consumers handle these ancestry-only hashes? Dynamo supports only one block size and would either index every hash as reusable or drop the event on a size mismatch. llm-d seems to have the same issue.

@Lucas-Fernandes-Martins

Copy link
Copy Markdown
Author

Hi @Change72 I don't believe this is an issue for llm-d, as it just needs the mapping between the engine hashes and token_ids. I've been using llm-d with this commit for many weeks now and it works well.
On the other hand, without this fix, precise KV cache routing in llm-d doesn't work at all when using prefix_match_unit, as llm-d doesn't get the correct parenth hashes to build the prefix.

@mergify

mergify Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @Lucas-Fernandes-Martins.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Nov 3, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working kv-cache-manager needs-rebase ready ONLY add when PR is ready to merge/full CI is needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Partial cache events can reference unreported parent hashes

4 participants