Skip to content

feat(data): extract verifier KV caches for Gemma4 MTP#758

Open
shotsan wants to merge 6 commits into
vllm-project:mainfrom
shotsan:feat/gemma4-mtp-data-pipeline
Open

feat(data): extract verifier KV caches for Gemma4 MTP#758
shotsan wants to merge 6 commits into
vllm-project:mainfrom
shotsan:feat/gemma4-mtp-data-pipeline

Conversation

@shotsan

@shotsan shotsan commented Jul 9, 2026

Copy link
Copy Markdown

Purpose

Resolves the offline data ingestion requirements for Gemma 4 MTP training by properly extracting and batching the verifier's KV caches.

The Gemma 4 MTP architecture uses a query-only attention mechanism that borrows the KV cache from the verifier model. To train this offline, the dataset must provide the extracted KV caches for every token. This PR updates the data loading and batching pipeline to handle these new 5-D KV tensors.

Code Changes:

  1. Dynamic Tensor Stacking: Updated ArrowDataset._get_raw_data and standardize_data_v1 to dynamically detect kv_last_local_k and _v keys. It uses torch.stack to combine them into the unified verifier_kv_last_local tensor that the downstream model expects.
  2. Type Casting: In BaseDataset.__getitem__, the KV caches are correctly cast to the hidden_states_dtype (bfloat16) to prevent downstream mixed-precision crashes.
  3. MTP Batch Shifting: Updated shift_batch_mtp in src/speculators/models/mtp/data.py to correctly forward the 5-D KV cache tensors through the training loop without sequence misalignment.

Tests

  • Unit Tests: Updated tests/unit/models/test_mtp_data.py. The unit tests pass locally and verify that the 5-D KV tensors (batch_size, seq_len, 2, num_kv_heads, head_dim) are correctly preserved and aligned.
  • End-to-End Test: Roderick-Wu has successfully tested this branch (alongside the two subsequent PRs) to train a full Gemma 4 MTP speculator end-to-end on vLLM.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly matches the main change: adding verifier KV cache extraction for Gemma4 MTP data loading.
Description check ✅ Passed The description is directly about the same data pipeline changes and test coverage described in the diff.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
src/speculators/train/data.py (1)

73-91: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Update docstring to document new KV cache fields.

The data structure comment at lines 73-80 doesn't mention verifier_kv_last_local or verifier_kv_last_global. Similarly, the comments at lines 188-193 and 211-218 in __getitem__ omit these new fields. Adding them will help future readers understand the full sample schema.

📝 Proposed docstring update
     # data structure: {
     #     "hidden_states": [seq_len, num_target_layers * hidden_size],
     #     "input_ids": [seq_len],
     #     "verifier_last_hidden_states": [seq_len, hidden_size],
+    #     "verifier_kv_last_local": [...],
+    #     "verifier_kv_last_global": [...],
     #     "loss_mask": [seq_len],
     #     "lengths": [1],
     #     "position_ids": [seq_len],
     # }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/speculators/train/data.py` around lines 73 - 91, Update the sample schema
comments in data.py to include the new KV cache fields: the top-level data
structure docstring near the fallback return in the data-loading helper should
list verifier_kv_last_local and verifier_kv_last_global alongside the existing
tensors, and the __getitem__ comments should also mention these fields so the
documented sample shape matches the actual dict returned by the data pipeline.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/speculators/train/data.py`:
- Around line 73-91: Update the sample schema comments in data.py to include the
new KV cache fields: the top-level data structure docstring near the fallback
return in the data-loading helper should list verifier_kv_last_local and
verifier_kv_last_global alongside the existing tensors, and the __getitem__
comments should also mention these fields so the documented sample shape matches
the actual dict returned by the data pipeline.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a8fbffaa-9a48-4de1-a9d1-0005f0a30c68

📥 Commits

Reviewing files that changed from the base of the PR and between 73ec09f and bf35ba5.

📒 Files selected for processing (1)
  • src/speculators/train/data.py

@orestis-z orestis-z 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.

The passthrough plumbing looks correct — optional fields are conditionally propagated and dtype-converted alongside other hidden states.

Two questions before this can be reviewed properly:

  1. Missing context: The PR description is empty (just a signed-off-by). What is the motivation? Which data generation flow produces verifier_kv_last_local / verifier_kv_last_global, and which model forward consumes them? grep -rn 'verifier_kv_last' src/ returns zero hits outside this PR, so the producer and consumer don't appear to exist in the codebase yet. Is this meant to land alongside another PR?

  2. No tests: Can you add at least a unit test that round-trips a sample with these optional fields through standardize_data_v1 and verifies they survive collation?

🤖 Generated with Claude Code using the /pr-review skill

@santosh-agebold

Copy link
Copy Markdown

Hi @orestis-z, thanks for the review!

To answer your first question: This PR is part 1 of a 3-part rollout to introduce Gemma4 MTP support (Issue #586).

  1. Data Pipeline (This PR): Extracts verifier_kv_last_local and verifier_kv_last_global from offline datasets so they are available during training.
  2. Multi-level LM Head (Next): Adds the centroid-masked hierarchical softmax head needed by Gemma4.
  3. MTP Draft Model (Final): Implements the query-only attention and wires everything together.

By splitting this into three PRs, we wanted to keep the review size manageable and isolate the data plumbing from the model math. I'll push the unit tests and docstring fixes shortly!

@mergify

mergify Bot commented Jul 10, 2026

Copy link
Copy Markdown

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

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 Jul 10, 2026

@orestis-z orestis-z 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.

Thanks for the follow-up commit. The test covers the standardize_data_v1collate_fn round-trip for the optional KV fields, which was the main gap. The conditional propagation, dtype conversion, and backward compatibility with non-Gemma4 datasets all look correct. LGTM. Recommend approving once the merge conflict is resolved.

🤖 Generated with Claude Code using the /pr-review skill

@orestis-z orestis-z 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.

Data pipeline changes (KV cache extraction, dtype conversion, backward compatibility, tests) are unchanged from the previous round and still correct. Merge conflict is resolved.

The merge commit adds two new config fields to MTPSpeculatorConfig that weren't in the prior rounds — see inline comment.

🤖 Generated with Claude Code using the /pr-review skill

Comment thread src/speculators/models/mtp/config.py Outdated
Comment on lines +55 to +66
num_centroids: int | None = Field(
default=None,
description=(
"Number of centroids for a centroid-masked multi-level LM head. "
"If None, standard flat LM head is used."
),
)

top_k_centroids: int = Field(
default=32,
description="Number of top centroids to select during multi-level LM head decoding.",
)

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.

These two fields (num_centroids, top_k_centroids) are new in this merge commit and weren't in the previous rounds. Per your rollout plan, centroid-masked LM head support is part 2, while this PR is part 1 (data pipeline). No code in this PR or on main references these fields.

Consider deferring them to the multi-level LM head PR where they'll have a consumer and can be reviewed alongside the code that uses them. If there's a reason to land them here (e.g., checkpoint format compatibility), please note it in the PR description.

shotsan added a commit to shotsan/speculators that referenced this pull request Jul 13, 2026
…dels

This commit introduces the architecture modifications required for Gemma4 MTP support.
Unlike Qwen-style MTP which maintains its own full attention layers and KV cache,
Gemma4 MTP borrows the last local and global KV caches from the verifier base model.

- Registered Gemma2 components in base_components.py
- Implemented QueryOnlyGemma2Attention to bypass K/V projections and route external KV caches
- Updated MTPDraftModel.forward to pass verifier_kv_last_local/global to layers
- Added unit tests for KV cache routing in Gemma4 MTP

Relates to vllm-project#586, vllm-project#758

Signed-off-by: santosh-agebold <12378904+shotsan@users.noreply.github.com>
@shotsan

shotsan commented Jul 13, 2026

Copy link
Copy Markdown
Author

Good catch @orestis-z! I accidentally leaked those two config fields from the subsequent ML Head PR when resolving merge conflicts. I just pushed a fix to remove them entirely from this PR so it remains strictly focused on the data pipeline.

@orestis-z orestis-z 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.

The leaked config fields from the previous round have been removed. Data pipeline changes (conditional KV cache propagation, dtype conversion, backward compatibility with non-Gemma4 datasets, tests) remain correct and unchanged. LGTM. Recommend approving.

🤖 Generated with Claude Code using the /pr-review skill

@mergify

mergify Bot commented Jul 14, 2026

Copy link
Copy Markdown

The quality checks have failed. Please run make style and make quality under
the root directory to address the lint failures. You will need to install the
dev optional install to get the required linting packages:
https://github.com/vllm-project/speculators/blob/main/CONTRIBUTING.md

Comment on lines 118 to +123
}
if "verifier_kv_last_local" in data:
res["verifier_kv_last_local"] = data["verifier_kv_last_local"]
if "verifier_kv_last_global" in data:
res["verifier_kv_last_global"] = data["verifier_kv_last_global"]
return res

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.

Can we modify these keys to match this pr:
#674

Left a comment

@shotsan

shotsan commented Jul 14, 2026

Copy link
Copy Markdown
Author

Good catch! Updated standardize_data_v1 to dynamically read kv_last_local_k and _v keys (as outputted by PR #674) and stack them into the unified verifier_kv_last_local tensor that the downstream dataloader and models expect. I've also updated the unit tests to verify the shape matches.

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

The data pipeline plumbing for SampleFileDataset (via standardize_data_v1) correctly reads PR #674's key names (kv_last_local_k/_v) and stacks them. However, the ArrowDataset path — which is the primary consumer of Gemma4KVConnector's safetensors output — has a key name mismatch that will silently drop the KV caches. One inline comment below.

🤖 Generated with Claude Code using the /pr-review skill

Comment thread src/speculators/train/data.py Outdated
Comment on lines +418 to +421
if "verifier_kv_last_local" in loaded_hs:
res["verifier_kv_last_local"] = loaded_hs["verifier_kv_last_local"]
if "verifier_kv_last_global" in loaded_hs:
res["verifier_kv_last_global"] = loaded_hs["verifier_kv_last_global"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ArrowDataset._get_raw_data looks for verifier_kv_last_local / verifier_kv_last_global in the loaded safetensors, but PR #674's Gemma4KVConnector writes the KV caches as four separate tensors: kv_last_local_k, kv_last_local_v, kv_last_global_k, kv_last_global_v (see GEMMA4_KV_KEYS and the _submit_async_write save path). These keys will never match verifier_kv_last_local, so the condition is always False and the KV caches are silently dropped.

This was already fixed in standardize_data_v1 (line 119) after @Roderick-Wu's comment, but the same transformation needs to be applied here. Something like:

if "kv_last_local_k" in loaded_hs and "kv_last_local_v" in loaded_hs:
    res["verifier_kv_last_local"] = torch.stack(
        [loaded_hs["kv_last_local_k"], loaded_hs["kv_last_local_v"]], dim=1
    )
if "kv_last_global_k" in loaded_hs and "kv_last_global_v" in loaded_hs:
    res["verifier_kv_last_global"] = torch.stack(
        [loaded_hs["kv_last_global_k"], loaded_hs["kv_last_global_v"]], dim=1
    )

(Or extract the stacking into a shared helper to avoid duplicating it.)

@shotsan

shotsan commented Jul 17, 2026

Copy link
Copy Markdown
Author

I have pushed a fix to apply the same torch.stack extraction logic for the separated K and V tensors to ArrowDataset._get_raw_data.

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

The ArrowDataset._get_raw_data key name mismatch I flagged last round is fixed — it now uses the PR #674 key names (kv_last_local_k/_v) with torch.stack, matching standardize_data_v1. LGTM. Recommend approving.

🤖 Generated with Claude Code using the /pr-review skill

speculatorsbot

This comment was marked as duplicate.

@mergify

mergify Bot commented Jul 20, 2026

Copy link
Copy Markdown

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

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 Jul 20, 2026
shotsan and others added 4 commits July 20, 2026 10:52
Signed-off-by: santosh-agebold <12378904+shotsan@users.noreply.github.com>

Signed-off-by: shotsan <shotsan@users.noreply.github.com>
Signed-off-by: santosh-agebold <12378904+shotsan@users.noreply.github.com>

Signed-off-by: shotsan <shotsan@users.noreply.github.com>
Signed-off-by: shotsan <shotsan@users.noreply.github.com>
Signed-off-by: shotsan <shotsan@users.noreply.github.com>
shotsan added 2 commits July 20, 2026 10:52
Signed-off-by: shotsan <shotsan@users.noreply.github.com>
Signed-off-by: shotsan <shotsan@users.noreply.github.com>
@shotsan
shotsan force-pushed the feat/gemma4-mtp-data-pipeline branch from 97c7018 to 5bf4793 Compare July 20, 2026 17:58
shotsan added a commit to shotsan/speculators that referenced this pull request Jul 20, 2026
…dels

This commit introduces the architecture modifications required for Gemma4 MTP support.
Unlike Qwen-style MTP which maintains its own full attention layers and KV cache,
Gemma4 MTP borrows the last local and global KV caches from the verifier base model.

- Registered Gemma2 components in base_components.py
- Implemented QueryOnlyGemma2Attention to bypass K/V projections and route external KV caches
- Updated MTPDraftModel.forward to pass verifier_kv_last_local/global to layers
- Added unit tests for KV cache routing in Gemma4 MTP

Relates to vllm-project#586, vllm-project#758

Signed-off-by: santosh-agebold <12378904+shotsan@users.noreply.github.com>

Signed-off-by: shotsan <shotsan@users.noreply.github.com>
@shotsan

shotsan commented Jul 20, 2026

Copy link
Copy Markdown
Author

@orestis-z @Roderick-Wu I just rebased this PR to resolve the merge conflicts with the latest main. We addressed all feedback last week and CodeRabbit has fully approved. Could we please get this merged as soon as possible? We have been stuck rebasing this for a while, and the next two PRs in the stack (#767 and #768) are completely blocked until this lands. Thank you!

@mergify mergify Bot removed the needs-rebase label Jul 20, 2026
@Roderick-Wu

Copy link
Copy Markdown
Collaborator

Hi @shotsan, sorry for the wait. We'll have to wait for other reviewers. In the meantime, I have been trying to test all 3 prs + this earlier one:
#674

I've put them under this branch
https://github.com/vllm-project/speculators/tree/Roderick-Wu/gemma4-mtp-test-combined

There are a few small issues here and there that come up when we actually try running the whole thing end to end, so I'll keep you updated.

@shanjiaz

shanjiaz commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

@shotsan Thanks for breaking up the PRs into smaller pieces! It's very helpful. Could you please add a PR description to help reviewers understand which issue you're trying to resolve and attach test results as well? Thanks!

@santosh-agebold

Copy link
Copy Markdown

@shanjiaz Thanks for checking in! Since this PR has been open for a bit and already went through extensive review (we addressed all feedback and CodeRabbit has fully approved it), could I kindly ask you to read through the earlier comments for the full context? We are just trying to keep the comment flow clean without repeating the full picture, as this is part of a 3-PR stack (#758, #767, #768) that we are eager to land.

@Roderick-Wu Just checking in—did you have any updates on your end-to-end testing with the combined branch? We managed to track down a small unit test bug (a missing verifier config causing a Pydantic validation error) and are fixing it on our end now. Let us know if there are any specific stack traces or issues from your end-to-end run that we can help look into!

@Roderick-Wu

Copy link
Copy Markdown
Collaborator

@shotsan @santosh-agebold

I've been able to train a full gemma4 mtp speculator fully on the merged branch. However, it has some compatibility issues with actually testing it on vllm. The native vllm gemma4 supported architecture is found in vllm/vllm/model_executor/models/gemma4_mtp.py. This one is kind of based off of the Qwen3 version? Is this correct?

@shotsan

shotsan commented Jul 22, 2026

Copy link
Copy Markdown
Author

@Roderick-Wu Yes, that is exactly correct. The current implementation is based on the Qwen3 version.

The reason for this design is that the core MTP training pipeline (MTPDraftModel) was originally built around Qwen3's architecture, which assumes the draft model is a single layer that handles its own projection. Reusing the Qwen3 projection structure (MTPLayerMixin) allowed the Gemma 4 KV-sharing attention to be seamlessly plugged into the existing, highly-optimized training loop with minimal code changes.

However, to support testing on vLLM using the native architecture, a separate, new PR will be opened to add support for the native vLLM Gemma 4 layout (with explicit pre_projection and post_projection layers). This will allow training and testing to be done natively without compatibility issues. Please advise if this sounds like a good approach.

@Roderick-Wu

Copy link
Copy Markdown
Collaborator

Hi @shotsan
Can you take a look at this draft pr?
#845

I got an agent to put some bandaid fixes for mapping the model to the gemma4 mtp version accepted by vllm. This version is able to train a speculator e2e and run it in vllm. Is this desired behavior?
If you get the chance, please diff it against your branches and identify parts where I may have made mistakes or might be useful fixes for your branches.

@shotsan

shotsan commented Jul 23, 2026

Copy link
Copy Markdown
Author

@Roderick-Wu The draft PR looks excellent. The agent did a fantastic job implementing the Gemma4NativeMTPLayer exactly as vLLM expects it natively (with explicit pre_projection and post_projection layers), along with flawless integrations for the centroid masking and KV connector.

More importantly, the agent correctly added this under a new gemma4_text architecture rather than overwriting the existing Gemma2MTPLayer. This is the perfect approach, as it allows both the lightweight Qwen3-style variant and the native vLLM variant to coexist without any compatibility issues.

After a thorough diff against the current branches, only one minor issue was identified: the agent appears to have worked off a slightly older commit and accidentally reverted a recent fix applied to tests/unit/models/test_mtp_head.py.
Because the new config.py correctly adds strict Pydantic validation (ensuring centroid_intermediate_top_k <= num_centroids), the reverted test will currently crash in CI. The test defines num_centroids=4 but falls back to the default centroid_intermediate_top_k=32, triggering a pydantic.ValidationError.

A quick patch to test_mtp_head.py (explicitly passing centroid_intermediate_top_k=2 and a mock VerifierConfig) will resolve this immediately.

Overall, these changes represent the exact desired behavior. The commits from PR 845 can be pulled into the existing branches, and once the minor test fix is reapplied, the comprehensive Gemma 4 MTP support will be ready.

@mergify

mergify Bot commented Jul 24, 2026

Copy link
Copy Markdown

Merge Protections

🔴 1 of 1 protections blocking · waiting on 👀 reviews

Protection Waiting on
🔴 Require approval from approved reviewers list 👀 reviews

🔴 Require approval from approved reviewers list

Waiting for any of

  • approved-reviews-by = dsikka
  • approved-reviews-by = fynnsu
  • approved-reviews-by = orestis-z
  • approved-reviews-by = rahul-tuli
  • approved-reviews-by = shanjiaz
This rule is failing.

All pull requests must have at least one approving review from a member of the approved reviewers list before merging.

  • any of:
    • approved-reviews-by = dsikka
    • approved-reviews-by = fynnsu
    • approved-reviews-by = orestis-z
    • approved-reviews-by = rahul-tuli
    • approved-reviews-by = shanjiaz

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

Data pipeline changes for the KV cache passthrough are correct on the happy path — conditional propagation, key name alignment with PR #674, dtype conversion in collate_fn, and test coverage all look solid. The utils import cleanup is fine.

One shape issue in the empty-sample fallback — see inline comment.

🤖 Generated with Claude Code using the /pr-review skill

Comment on lines +82 to +83
"verifier_kv_last_local": torch.empty(0, dtype=dtype),
"verifier_kv_last_global": torch.empty(0, dtype=dtype),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The other empty tensors in create_empty_sample preserve their inner dimensions — e.g. torch.empty(0, hidden_size) for verifier_last_hidden_states, torch.empty(0, num_target_layers * hidden_size) for hidden_states. These KV tensors are 1-dimensional (0,) instead.

When the empty fallback is used in collate_fn (all samples in a batch failed), the collation infers the output shape from first.shape[1:]. For these 1D tensors that yields (), so the padded output becomes [max_len] instead of the expected [max_len, 2, head_dim]. Downstream model code that indexes into the KV tensors by dimension will get a shape error.

Since these fields are optional (non-Gemma4 datasets never produce them), the simplest fix is to remove them from create_empty_sample entirely. The collation iterates for key in batch[0], so when the empty sample is batch[0] without KV keys, those keys are simply absent from the output — which is consistent with how non-KV datasets already behave. Adding them here also has the side effect of injecting unexpected KV keys into the fallback output for non-Gemma4 datasets that never produce them.

@shotsan

shotsan commented Jul 24, 2026

Copy link
Copy Markdown
Author

@shanjiaz I have updated the PR description with the detailed breakdown of the issue being resolved, the specific code changes, and the test results as requested! Please let me know if there is anything else needed for your approval.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants