Skip to content

add Qwen3-VL support for DFlash training#1975

Open
skierat wants to merge 2 commits into
NVIDIA:mainfrom
skierat:skierat/qwen3-vl-dflash-final
Open

add Qwen3-VL support for DFlash training#1975
skierat wants to merge 2 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**
  * Improved Qwen3-VL video handling for speculative decoding with newer Transformers versions.
  * Added support for reading RoPE configuration from modern model configuration formats.
  * Improved multimodal training, label shifting, answer-only loss, and loss-mask behavior.

* **Bug Fixes**
  * Improved validation and error handling for multimodal inputs.
  * Improved training stability when no valid anchor blocks are available.

* **Tests**
  * Added coverage for RoPE configuration export, Qwen3-VL position IDs, and vision-language training 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 11691ebe-87df-4f7f-be7f-9f4706cc3289

📥 Commits

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

📒 Files selected for processing (6)
  • examples/speculative_decoding/eagle_utils.py
  • modelopt/torch/export/plugins/hf_spec_export.py
  • modelopt/torch/speculative/plugins/hf_dflash.py
  • tests/unit/torch/export/test_hf_spec_rope_export.py
  • tests/unit/torch/speculative/plugins/test_hf_dflash.py
  • tests/unit/torch/speculative/plugins/test_hf_speculative_offline.py

📝 Walkthrough

Walkthrough

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 now 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
examples/speculative_decoding/eagle_utils.py, tests/unit/torch/speculative/plugins/test_hf_speculative_offline.py
The online VLM collator receives answer-only loss, label-shifting, and chat-template settings, with corresponding test coverage.

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

Suggested reviewers: h-guo18, yeyu-nvidia

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 and concisely matches the main change: adding Qwen3-VL support for DFlash training.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% 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 No critical anti-patterns were introduced: the only new torch.load(weights_only=False) has an inline safety comment; no hardcoded trust_remote_code/eval/nosec or unsafe numpy.load additions.
✨ 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?

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.

2 participants