Skip to content

add Qwen3-VL support for DFlash training - #1975

Merged
h-guo18 merged 5 commits into
NVIDIA:mainfrom
skierat:skierat/qwen3-vl-dflash-final
Jul 30, 2026
Merged

add Qwen3-VL support for DFlash training#1975
h-guo18 merged 5 commits into
NVIDIA:mainfrom
skierat:skierat/qwen3-vl-dflash-final

Conversation

@skierat

@skierat skierat commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: new feature

Adds online DFlash training support for Qwen3-VL–style vision-language models.

Changes include:

  • Load VLMs through the Transformers 5 AutoModelForImageTextToText API, while retaining compatibility with the legacy VLM auto-model API.
  • Run the base model through its top-level multimodal forward when image/video inputs are present, ensuring vision embeddings are injected before collecting DFlash target hidden states.
  • Extend VisionLanguageDataCollator to:
    • propagate answer_only_loss, chat-template, and DFlash label-alignment settings;
    • apply VLM_MIN_PIXELS / VLM_MAX_PIXELS processor limits;
    • derive assistant-only masks from ChatML/Llama chat boundaries when processor generation masks are unavailable;
    • enforce the fixed training_seq_len required by DFlash block training.
  • Preserve the existing text-only DFlash path.

Usage

python -m torch.distributed.run \                                      
--nproc_per_node 4 \                                                 
examples/speculative_decoding/main.py \                              
--config modelopt_recipes/general/speculative_decoding/dflash.yaml \
model.model_name_or_path=/path/to/qwen3-vl-model \
model.trust_remote_code=true \
data.data_path=/path/to/train.jsonl \
data.vlm_processor=/path/to/qwen3-vl-model \                        
data.vlm_img_dir=/path/to/image/root \
training.training_seq_len=4096 \
training.answer_only_loss=true \
dflash.dflash_block_size=8 \
dflash.dflash_mask_token_id=151669


### Testing
- git diff --check
- Parsed all modified Python modules successfully.
- Ran iterative multi-node Slurm smoke tests with a Qwen3-VL-family model and mixed multimodal data:
    - validated VLM model loading with Transformers 5;
    - validated distributed initialization, DFlash conversion, and VLM collation paths;
    - identified and addressed processor padding/truncation behavior required by fixed-size DFlash blocks.
This PR remains draft pending a completed end-to-end training smoke test and automated regression coverage.

### Before your PR is "Ready for review"
Make sure you read and follow Contributor guidelines (https://github.com/NVIDIA/Model-Optimizer/blob/main/CONTRIBUTING.md) and your commits are signed (git commit -s -S).
Make sure you read and follow the Security Best Practices (https://github.com/NVIDIA/Model-Optimizer/blob/main/SECURITY.md#security-coding-practices-for-contributors) (e.g. avoiding hardcoded trust_remote_code=True, torch.load(...,
weights_only=False), pickle, etc.).
- Is this change backward compatible?: ✅
- If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A
- Did you write any new necessary tests?: ❌ — automated Qwen3-VL/DFlash regression coverage still needs to be added before review.
- Did you update Changelog (https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?: ❌ — evaluate and add an entry before marking ready for review if this is considered user-facing speculative-decoding support.
- Did you get Claude approval on this PR?: N/A
### Additional Information
The PR intentionally excludes local Slurm launch scripts, logs, model paths, datasets, and environment-specific configuration.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Expanded VLM data-collation controls, including `shift_labels` and more robust `answer_only_loss` masking.
  * Improved Qwen3-VL speculative decoding for Transformers 5.3+ with correct video frame grouping and safer position-id handling.
  * Improved DFlash RoPE export to reliably read `rope_theta` from newer config formats.

* **Bug Fixes**
  * Hardened multimodal preprocessing and training loss masking to keep label/attention alignment consistent.
  * Improved behavior when anchor sampling yields no valid blocks.
  * More resilient VLM model loading when certain Transformers auto classes are unavailable.

* **Tests**
  * Added coverage for RoPE export, Qwen3-VL position-id logic across Transformers versions, and VLM label-mode/collator options.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

@skierat
skierat requested review from a team as code owners July 14, 2026 14:02
@skierat
skierat requested review from h-guo18 and shengliangxu July 14, 2026 14:02
@copy-pr-bot

copy-pr-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The changes update DFlash Qwen3-VL multimodal training, Transformers 5 compatibility, RoPE export configuration, and vision-language data collation. Tests cover version-specific position IDs, multimodal forwarding, label handling, nested RoPE parameters, and VLM model loading fallback behavior.

Changes

DFlash and RoPE updates

Layer / File(s) Summary
RoPE configuration extraction
modelopt/torch/export/plugins/hf_spec_export.py, tests/unit/torch/export/test_hf_spec_rope_export.py
RoPE export reads legacy and nested configuration fields, with coverage for rope_parameters.
Qwen3-VL DFlash training flow
modelopt/torch/speculative/plugins/hf_dflash.py, tests/unit/torch/speculative/plugins/test_hf_dflash.py
Qwen3-VL video grids and position IDs are validated and expanded; multimodal forward calls, loss masks, and empty-anchor gradients are updated.
Vision-language data wiring
modelopt/torch/utils/plugins/transformers_dataset.py, examples/speculative_decoding/eagle_utils.py, tests/unit/torch/speculative/plugins/test_hf_speculative_offline.py
The VLM collator supports label shifting, ChatML assistant-mask derivation, content normalization, and online wiring.
Transformers VLM loading compatibility
modelopt/torch/speculative/utils.py, tests/unit/torch/speculative/plugins/test_fakebase.py
VLM loading falls back to AutoModelForImageTextToText when AutoModelForVision2Seq is unavailable.

Estimated code review effort: 4 (Complex) | ~45 minutes

Suggested reviewers: shengliangxu

Sequence Diagram(s)

sequenceDiagram
  participant HFDFlashModel
  participant Qwen3VLModel
  participant get_rope_index
  HFDFlashModel->>Qwen3VLModel: recompute position_ids from multimodal inputs
  Qwen3VLModel->>get_rope_index: pass expanded video_grid_thw and mm_token_type_ids
  get_rope_index-->>HFDFlashModel: return position_ids
  HFDFlashModel->>Qwen3VLModel: forward multimodal inputs
  Qwen3VLModel-->>HFDFlashModel: return hidden_states
Loading
🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding Qwen3-VL support for DFlash training.
Docstring Coverage ✅ Passed Docstring coverage is 88.24% which is sufficient. The required threshold is 80.00%.
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.
Security Anti-Patterns ✅ Passed Reviewed changed modelopt/examples Python files: no hardcoded torch.load/numpy.load/eval/exec/nosec/new deps, and trust_remote_code stays caller-configurable.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@h-guo18

h-guo18 commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

/claude review

@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 review — 1 CRITICAL, 0 IMPORTANT, 0 SUGGESTION.

Most impactful finding

[CRITICAL Algorithm] shift_labels crashes the new VLM online path (examples/speculative_decoding/eagle_utils.py:145)

The PR passes shift_labels=shift_labels into VisionLanguageDataCollator(...), but that class (modelopt/torch/utils/plugins/transformers_dataset.py:321) declares (processor, train_len, chat_template, add_generation_prompt, answer_only_loss, local_image_path, return_labels) — no shift_labels parameter and no **kwargs. Any real Qwen3-VL online run therefore fails immediately with a TypeError about an unexpected keyword argument shift_labels.

DFlash specifically requires shift_labels=False (unshifted labels), so this is exactly the value the feature must pass — the crash blocks the PR headline capability end-to-end. The text-only branch (LanguageDataCollator, line 135) is unaffected because that class does declare shift_labels.

The new test_vlm_data_module_passes_dflash_label_mode does not catch this: it swaps VisionLanguageDataCollator for a MagicMock, which accepts arbitrary kwargs and hides the real signature mismatch.

Fix: add shift_labels to VisionLanguageDataCollator.init and forward it to super().init(...) (the parent already stores/uses it). Consider exercising the real collator signature in a test.

Assessment

The RoPE-theta extraction, mRoPE frame-expansion, top-level multimodal forward, loss-mask intersection with attention_mask, and the all-parameter DDP dummy-loss change all look correct and well-reasoned. Risk is concentrated in the single collator signature mismatch, which is a hard crash but a small, contained fix. The PR is also explicitly marked draft pending an e2e smoke test and regression coverage. Blocking on the one CRITICAL.

local_image_path=data_args.vlm_img_dir,
return_labels=True,
answer_only_loss=answer_only_loss,
shift_labels=shift_labels,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[CRITICAL Algorithm] VisionLanguageDataCollator does not accept shift_labels, so this call raises TypeError: __init__() got an unexpected keyword argument 'shift_labels' at runtime — crashing the exact VLM online path this PR adds.

VisionLanguageDataCollator.__init__ (modelopt/torch/utils/plugins/transformers_dataset.py:321) is declared as (self, processor, train_len, chat_template, add_generation_prompt, answer_only_loss, local_image_path, return_labels) — it has neither a shift_labels parameter nor **kwargs, and it never forwards shift_labels to super().__init__. The text-only branch above (LanguageDataCollator, line 135) is fine because that class does declare shift_labels, but the VLM subclass does not.

This isn't caught by test_vlm_data_module_passes_dflash_label_mode because that test replaces VisionLanguageDataCollator with a MagicMock, which accepts any kwargs — so the real signature mismatch is masked.

Impact: DFlash for Qwen3-VL requires shift_labels=False, and passing it is the only way to get the unshifted-label behavior DFlash needs — the crash blocks the feature entirely for any real run.

Fix: add shift_labels to VisionLanguageDataCollator.__init__ and forward it to super().__init__(...) (the parent already stores and uses it). For example, in transformers_dataset.py:

def __init__(
    self,
    processor: str,
    train_len: int = 8192,
    chat_template: str | None = None,
    add_generation_prompt: bool = False,
    answer_only_loss: bool = False,
    shift_labels: bool = True,
    local_image_path: str = "",
    return_labels: bool = False,
):
    ...
    super().__init__(
        tokenizer=self.processor.tokenizer,
        train_len=train_len,
        chat_template=chat_template,
        add_generation_prompt=add_generation_prompt,
        answer_only_loss=answer_only_loss,
        shift_labels=shift_labels,
        return_labels=return_labels,
    )

Consider having the test construct the real collator (or assert against its actual signature) so this class of mismatch is caught.

if (
position_ids is not None
or getattr(self.config, "model_type", None) != "qwen3_vl"
or not transformers.__version__.startswith("5.3.")

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.

question: is this patch only needed for transformers 5.3, or for all transformers 5.x or >5.3?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Only for Transformers 5.3.x.
Starting with 5.4.0, upstream added that same repeat_interleave(...); grid[:, 0] = 1 expansion internally in get_rope_index(), and it remains in 5.12.

@h-guo18 h-guo18 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.

[AI review] Supplementing the earlier claude[bot] review (which flagged the shift_labels TypeError): a few remaining issues, details inline.

Also noting a test-coverage gap for the pending regression work: nothing currently exercises the multimodal top-level-forward branch, the loss-mask ∩ attention_mask change, or the all-parameter DDP dummy-loss path.

local_image_path=data_args.vlm_img_dir,
return_labels=True,
answer_only_loss=answer_only_loss,
shift_labels=shift_labels,

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.

[AI review] The PR description says VisionLanguageDataCollator was extended (propagating answer_only_loss/chat-template/label-alignment settings, VLM_MIN_PIXELS/VLM_MAX_PIXELS limits, ChatML-boundary assistant masks, fixed training_seq_len enforcement), but modelopt/torch/utils/plugins/transformers_dataset.py is not part of this diff — on the current head the class still has its old signature. It looks like that file may not have been committed/pushed.

Note this is broader than the shift_labels TypeError already flagged: even after adding that one parameter, the described mask/pixel/seq-len behaviors would still be missing from the PR.

if (
position_ids is not None
or getattr(self.config, "model_type", None) != "qwen3_vl"
or not transformers.__version__.startswith("5.3.")

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.

[AI review] Two robustness concerns with this gate:

  1. Version pin. Tracing transformers 5.3.0, the per-video vs per-frame grid mismatch looks like a 5.3 bug rather than a stable contract: get_rope_index consumes one grid row per rendered visual group (next(grid_iters[...])), while the processor renders T timestamp-separated frame groups per video and the video processor emits one [T, H, W] row per video. Even the vanilla generation path (_prepare_position_ids_for_generation) passes the raw grid, so upstream video inference appears equally broken on 5.3 — meaning upstream will likely fix this inside get_rope_index (as in 4.x, where the expansion was internal). If that fix lands in a 5.3.x patch release, this external expansion becomes a double expansion and silently corrupts positions; on 5.4+ the helper silently disables itself and multimodal batches fall back to the broken internal path. Suggest linking the upstream issue/commit here, and failing loudly (frame-group count ≠ grid-row count) instead of silently returning None when the gate doesn't match.

  2. model_type != "qwen3_vl" exact match. Family variants like qwen3_vl_moe skip the precomputation but still enter the multimodal top-level forward below — landing exactly in the broken path this helper works around. Consider covering the family or at least warning.

- Label alignment: position k predicts token at anchor+k
- Optional loss decay weighting
"""
position_ids = self._qwen3_vl_position_ids(

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.

[AI review] This runs before the if not self.training branch, so it also fires on eval prefill (multimodal inputs, no cache). Passing explicit position_ids makes Qwen3VLModel.forward skip compute_3d_position_ids, so self.rope_deltas is never established; a subsequent cached decode step (position_ids=None, past_key_values not None → this helper returns None) then falls into the rope_deltas-based branch with a stale/None delta and computes wrong positions.

Since the docstring says this construction is only needed for DFlash training, consider gating on self.training.

attention_mask=attention_mask,
output_hidden_states=True,
if use_top_level_forward:
base_forward_kwargs = dict(kwargs)

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.

[AI review] base_forward_kwargs forwards everything except assistant_masks/loss_mask. Anything else that reaches forward's **kwargs — e.g. Trainer-injected num_items_in_batch (Trainer detects that this forward accepts **kwargs and adds loss kwargs), or stray dataset columns — gets passed to the HF multimodal forward and can raise TypeError or be silently swallowed depending on version. The text-only branch below is immune because it forwards a fixed argument set.

Consider an allowlist (the multimodal keys + known model kwargs) or filtering against the base forward's signature.

import logging

import torch
import transformers

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.

[AI review] nit: import transformers lands between import torch and import torch.nn.functional as F; ruff/isort (I001) will flag this in pre-commit/CI.

@skierat
skierat requested a review from a team as a code owner July 27, 2026 10:46
@skierat

skierat commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

/claude review

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

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 1

🧹 Nitpick comments (3)
tests/unit/torch/speculative/plugins/test_hf_dflash.py (1)

123-340: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Solid, well-verified coverage of the Qwen3-VL mRoPE workaround.

Cross-checked each assertion (video-frame-group expansion math, patch-release RuntimeError, 5.4.0 native delegation, malformed mm_token_type_ids validation, _multimodal_forward_kwargs filtering, eval-time no-op) against the _qwen3_vl_position_ids/_multimodal_forward_kwargs implementation and all are consistent.

One minor nit: the fake_model = SimpleNamespace(config=SimpleNamespace(model_type="qwen3_vl"), model=SimpleNamespace(get_rope_index=MagicMock(...))) construction is repeated near-verbatim across 7 test functions (lines 129-135, 166-169, 194-197, 218-221, 243-246, 300-303, 320-323). Consider extracting a small fake_qwen3_vl_model(get_rope_index=None, model_type="qwen3_vl") helper/fixture to reduce duplication.

🤖 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 `@tests/unit/torch/speculative/plugins/test_hf_dflash.py` around lines 123 -
340, Extract the repeated Qwen3-VL SimpleNamespace construction from the
affected tests into a small fake_qwen3_vl_model helper or fixture, accepting
optional get_rope_index and model_type arguments. Replace each duplicated
fake_model setup with this helper while preserving each test’s configured model
type and mocked rope method behavior.
tests/unit/torch/speculative/plugins/test_hf_speculative_offline.py (1)

95-111: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test only checks attribute assignment, not the actual unshifted-label behavior it claims to validate.

The docstring says "must support DFlash's unshifted labels," but the assertion only checks collator.shift_labels is False — it never calls the collator on sample messages and verifies the resulting labels actually equal input_ids (unshifted), as opposed to the shifted default. As per path instructions, "Tests must exercise the behavior they claim to validate... include an end-to-end test using the real implementation rather than replacing it with a fake."

Consider extending this test (with return_labels=True and a minimal message list plus a trivial/mock apply_chat_template) to assert labels equals input_ids for the unshifted path.

🤖 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 `@tests/unit/torch/speculative/plugins/test_hf_speculative_offline.py` around
lines 95 - 111, The test test_vlm_data_collator_accepts_unshifted_labels only
verifies configuration, not label generation. Invoke the real
VisionLanguageDataCollator with return_labels=True using minimal messages and a
trivial apply_chat_template setup, then assert the produced labels equal
input_ids when shift_labels=False; retain the existing processor setup without
replacing the collator implementation.

Source: Path instructions

modelopt/torch/utils/plugins/transformers_dataset.py (1)

433-474: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Native {% generation %} fallback path can silently zero the entire loss for multimodal batches on transformers 5.3.0.

When derive_masks_from_markers is False (i.e. the chat template uses HF's native {% generation %} tags rather than ChatML markers), return_assistant_tokens_mask=True is passed straight to self.processor.apply_chat_template. There is an open transformers bug (issue #44521, "transformers==5.3.0") where apply_chat_template returns all-zero assistant_masks for multimodal inputs because extra placeholder tokens such as <|image_pad|> are inserted later by the processor/tokenizer path, so offset_mapping corresponds to the expanded multimodal text while generation_indices still refers to the pre-expanded text, making the assistant span lookup fail so assistant_masks ends up all zeros.

With this collator's current handling, an all-zero assistant_masks silently falls into labels[:] = IGNORE_TOKEN_ID — masking the entire batch's loss with no warning, indistinguishable from the legitimate "fully truncated" case. For a training run with real multimodal data and a native-tag template on an affected transformers version, this would silently produce zero-loss batches instead of surfacing the misconfiguration.

Consider at least logging a distinct warning (or raising) when assistant_masks is all-zero but the batch clearly contains assistant turns, so this doesn't fail silently in production training.

🐛 Example: warn distinctly when native tag mask looks suspiciously empty
             assistant_mask = tokenized_messages["assistant_masks"]
             if not isinstance(assistant_mask, torch.Tensor) or not assistant_mask.any():
+                print_rank_0(
+                    "WARNING: assistant_masks is empty for a multimodal batch; masking all "
+                    "loss. If this is unexpected, check for HF apply_chat_template offset "
+                    "misalignment with multimodal placeholder tokens (transformers>=5.3.0)."
+                )
                 labels[:] = IGNORE_TOKEN_ID

Additionally, this label-construction block (lines 451-472) duplicates the logic in LanguageDataCollator._process_chat_sample (lines 236-265) almost verbatim (shift vs. unshifted labels, assistant-mask application, all-masked fallback). Consider extracting a shared helper (e.g. _labels_from_assistant_mask(input_ids, assistant_mask, shift_labels)) on the base class to avoid the two copies drifting apart.

🤖 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 `@modelopt/torch/utils/plugins/transformers_dataset.py` around lines 433 - 474,
Update _process_multimodal_sample to detect an all-zero assistant_masks result
when the batch contains assistant turns, and emit a distinct warning or raise
instead of silently treating it as legitimate full truncation. Extract the
duplicated label construction into a shared base-class helper such as
_labels_from_assistant_mask, including shift/unshifted handling and all-masked
behavior, then reuse it from both _process_multimodal_sample and
LanguageDataCollator._process_chat_sample.
🤖 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.

Inline comments:
In `@tests/unit/torch/speculative/plugins/test_fakebase.py`:
- Around line 155-156: Update the transformer auto-class setup in the test
around load_vlm_or_llm() to assign transformers.AutoModelForVision2Seq to None
instead of deleting the attribute, while preserving the _FakeVLM assignment for
AutoModelForImageTextToText. This must prevent the lazy lookup from recreating
the legacy class and ensure the fake model is used deterministically.

---

Nitpick comments:
In `@modelopt/torch/utils/plugins/transformers_dataset.py`:
- Around line 433-474: Update _process_multimodal_sample to detect an all-zero
assistant_masks result when the batch contains assistant turns, and emit a
distinct warning or raise instead of silently treating it as legitimate full
truncation. Extract the duplicated label construction into a shared base-class
helper such as _labels_from_assistant_mask, including shift/unshifted handling
and all-masked behavior, then reuse it from both _process_multimodal_sample and
LanguageDataCollator._process_chat_sample.

In `@tests/unit/torch/speculative/plugins/test_hf_dflash.py`:
- Around line 123-340: Extract the repeated Qwen3-VL SimpleNamespace
construction from the affected tests into a small fake_qwen3_vl_model helper or
fixture, accepting optional get_rope_index and model_type arguments. Replace
each duplicated fake_model setup with this helper while preserving each test’s
configured model type and mocked rope method behavior.

In `@tests/unit/torch/speculative/plugins/test_hf_speculative_offline.py`:
- Around line 95-111: The test test_vlm_data_collator_accepts_unshifted_labels
only verifies configuration, not label generation. Invoke the real
VisionLanguageDataCollator with return_labels=True using minimal messages and a
trivial apply_chat_template setup, then assert the produced labels equal
input_ids when shift_labels=False; retain the existing processor setup without
replacing the collator implementation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: cc216def-bb97-4a19-8858-0bcc96f2b68b

📥 Commits

Reviewing files that changed from the base of the PR and between 7b7c17b and 4db6e60.

📒 Files selected for processing (6)
  • modelopt/torch/speculative/plugins/hf_dflash.py
  • modelopt/torch/speculative/utils.py
  • modelopt/torch/utils/plugins/transformers_dataset.py
  • tests/unit/torch/speculative/plugins/test_fakebase.py
  • tests/unit/torch/speculative/plugins/test_hf_dflash.py
  • tests/unit/torch/speculative/plugins/test_hf_speculative_offline.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • modelopt/torch/speculative/plugins/hf_dflash.py

Comment thread tests/unit/torch/speculative/plugins/test_fakebase.py Outdated
@h-guo18
h-guo18 self-requested a review July 28, 2026 06:50

@h-guo18 h-guo18 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.

lgtm

@skierat
skierat force-pushed the skierat/qwen3-vl-dflash-final branch from 4db6e60 to 76ed5d1 Compare July 28, 2026 09:12
@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 50.67568% with 73 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.77%. Comparing base (a3ac475) to head (19c23eb).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
...delopt/torch/utils/plugins/transformers_dataset.py 6.66% 70 Missing ⚠️
modelopt/torch/speculative/plugins/hf_dflash.py 95.08% 3 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##             main    #1975       +/-   ##
===========================================
+ Coverage   66.83%   77.77%   +10.94%     
===========================================
  Files         519      519               
  Lines       58916    59720      +804     
===========================================
+ Hits        39376    46449     +7073     
+ Misses      19540    13271     -6269     
Flag Coverage Δ
examples 43.10% <8.78%> (-0.28%) ⬇️
gpu 59.00% <16.21%> (+37.89%) ⬆️
regression 15.08% <22.97%> (+0.09%) ⬆️
unit 55.06% <42.56%> (+0.16%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@h-guo18
h-guo18 enabled auto-merge (squash) July 29, 2026 07:56
kevalmorabia97 pushed a commit that referenced this pull request Jul 29, 2026
### What does this PR do?

Type of change: Bug fix (CI)

The `deploy-preview` job in the `Docs` workflow fails on **every pull
request opened from a fork**, which blocks merging for all external
contributors.

**Root cause.** `deploy-preview` runs `rossjrw/pr-preview-action@v1`,
which pushes the built HTML to the `gh-pages` branch. The workflow
declares `permissions: contents: write`, but for a `pull_request` event
originating from a forked repository GitHub caps the `GITHUB_TOKEN` at
**read-only** — the `permissions:` block cannot elevate above that cap.
The push is therefore rejected:

```
remote: Permission to NVIDIA/Model-Optimizer.git denied to github-actions[bot].
fatal: unable to access 'https://github.com/NVIDIA/Model-Optimizer.git/': The requested URL returned error: 403
```

The job's `if:` condition gated on `github.event_name`,
`github.event.action` and the `changes` path filter, but never on
whether the PR came from a fork — so it always ran and always failed.

Because the `changes` filter matches `docs/**`, `modelopt/**` and
`.github/workflows/pages.yml`, essentially any substantive fork PR trips
this.

**Fix.** Restrict `deploy-preview` to PRs whose head branch lives in
this repository:

```yaml
github.event.pull_request.head.repo.full_name == github.repository
```

A skipped job is not a failed job, so fork PRs are no longer blocked by
it.

### Usage

N/A — CI-only change.

### Testing

Behaviour by scenario:

| Scenario | Before | After |
| --- | --- | --- |
| PR from a branch in this repo | preview deployed | preview deployed
(**unchanged**) |
| PR from a fork | ❌ fails with 403 | ⏭️ skipped |
| Fork deleted (`head.repo` is `null`) | ❌ fails | ⏭️ skipped |

- Confirmed against workflow history: recent `Docs` runs on in-repo
branches (`main`, `chenjiel/nvfp4-act-headroom`, `mxin/qad-skill`,
`haoguo/dspark-ptq-script`) all succeed, while fork-branch runs fail
with the 403 above.
- `build-docs` was already passing on the affected PRs — only the deploy
step failed, so documentation builds are unaffected either way.
- YAML parses; `pre-commit run --files .github/workflows/pages.yml`
passes. (`yamlfmt` excludes `^.github/workflows/`, so this file is not
auto-formatted.)
- This PR edits `.github/workflows/pages.yml`, which is itself in the
`changes` filter, so it exercises `deploy-preview` on the in-repo path —
the preview deploy on this PR passing is a self-check that the unchanged
path still works.

The `closed` cleanup path is gated by the same condition. That is
intentional: a fork PR never deployed a preview directory, so there is
nothing to remove.

### Before your PR is "*Ready for review*"

- Is this change backward compatible?: ✅
- If you copied code from any other sources or added a new PIP
dependency, did you follow guidance in `CONTRIBUTING.md`: N/A
- Did you write any new necessary tests?: N/A — workflow-condition
change; not unit-testable
- Did you update
[Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?:
N/A — CI infrastructure, not user-facing
- Did you get Claude approval on this PR?: ❌ — not yet run

### Additional Information

Currently blocking #1975 (`add Qwen3-VL support for DFlash training`),
which is approved with every other check green and sits at
`mergeStateStatus: BLOCKED` solely because of this job.

Note that a PR only picks up this fix once its branch contains it, since
workflows run from the PR branch's own definitions.

A follow-up option, if doc previews for external contributors are
wanted: build in the `pull_request` workflow and deploy from a separate
`workflow_run`-triggered workflow, which executes in the base-repo
context and does get a write token. Deliberately not using
`pull_request_target` here — that would run unreviewed PR code with
write permissions.

Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
skierat and others added 5 commits July 29, 2026 11:05
Signed-off-by: Slawomir Kierat <skierat@nvidia.com>
Signed-off-by: Slawomir Kierat <skierat@nvidia.com>
Signed-off-by: Slawomir Kierat <skierat@nvidia.com>
Signed-off-by: Slawomir Kierat <skierat@nvidia.com>
Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
@h-guo18
h-guo18 force-pushed the skierat/qwen3-vl-dflash-final branch from 9f59816 to 19c23eb Compare July 29, 2026 15:24
@h-guo18
h-guo18 merged commit c94405e into NVIDIA:main Jul 30, 2026
54 checks passed
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