[Feature] Multimodal (image+text) DFlash training via server-side capture (Qwen3.5) - #730
Open
curnane-lab wants to merge 4 commits into
Open
[Feature] Multimodal (image+text) DFlash training via server-side capture (Qwen3.5)#730curnane-lab wants to merge 4 commits into
curnane-lab wants to merge 4 commits into
Conversation
Contributor
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
curnane-lab
marked this pull request as ready for review
July 29, 2026 15:40
curnane-lab
requested review from
FlamingoPg,
FrankLeeeee,
shuaills,
sleepcoo and
zyksir
as code owners
July 29, 2026 15:40
Contributor
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
curnane-lab
marked this pull request as draft
July 30, 2026 10:22
curnane-lab
force-pushed
the
vl-dflash-pr
branch
from
July 30, 2026 11:55
9e0d57c to
95ecc24
Compare
curnane-lab
marked this pull request as ready for review
July 31, 2026 14:12
Contributor
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Port of the DFlash draft-model changes from sgl-project/SpecForge PR sgl-project#585: partial rotation in apply_rotary_pos_emb (rotary_dim < head_dim, for Qwen3.5/Qwen3.6 partial_rotary_factor=0.25) and Qwen3InterleavedMultiRotaryEmbedding selected by rope_scaling.mrope_interleaved. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> (cherry picked from commit 9323a51; scoped to the draft model only, draft configs land with the recipe)
Follow-up adaptation of 9323a51 to the server-only architecture: - specforge/modeling/target/target_utils.py: add QWEN3_VL_MODEL_TYPES and resolve_target_weight_keys(); TargetEmbeddingsAndHead.from_pretrained now auto-selects model.language_model.embed_tokens.weight for VLM targets (Qwen3-VL / Qwen3.5 / Qwen3.6) when the embed key is unset or left at the LLM default. Explicit keys are honored as-is. - specforge/algorithms/model_providers.py: populate_dflash_generated_config reads the language-model depth via the text_config fallback so VLM draft config auto-generation uses the correct num_hidden_layers. (cherry picked from commit e2280f4)
Implements end-to-end VLM DFlash training on the server-only runtime:
- contracts/providers: dflash registers a STREAMING 'multimodal'
FeatureContract ({input_ids, loss_mask, hidden_states, position_ids}) and a
ServerStreamingProvider with a VLM ServerInputAdapter;
ServerCaptureLayout/ServerCaptureSchema gain position_ids_feature.
- data: specforge/data/vlm_preprocessing.py renders conversations with the
target's own chat template (image attached to the first user turn) and
expands the image region in id space via the target's HF processor, yielding
expanded input_ids/loss_mask (trainer + passthrough) and collapsed
request_input_ids + base64 image_data (capture request). One image per
sample (v1); text-only samples work in the same run.
- inference: the capture request's features map gains 'position_ids'; the
regenerated sglang v0.5.14 spec-capture patch writes the request's mRoPE
positions (1, L, 3) int64 into Mooncake from
req.multimodal_inputs.mrope_positions (arange fallback for text prompts).
Managed capture servers set SGLANG_MM_AVOID_RETOKENIZE=1 for
input_modality=multimodal so the server re-expands placeholders in id space
with no retokenization drift.
- training: build_vlm_collator pads position_ids; OnlineDFlashModel.forward
accepts optional (B, S, 3) position ids and gathers 3D mRoPE positions for
context + anchor-offset draft slots; DFlashTrainStrategy passes the tensor
through. Text paths are unchanged.
- recipe: examples/configs/qwen3.5-4b-vl-dflash-multimodal-disaggregated.yaml
+ configs/qwen3.5-4b-vl-dflash.json (Qwen3.5-4B VLM draft geometry).
- tests: new tests/test_algorithms/test_dflash_multimodal.py; update the
text-only assertions in test_builtin_providers / test_server_only_online /
test_unified_feature_reachability / test_launch_topology /
test_example_draft_config_wiring for the new modality and recipe.
GPU/NPU end-to-end validation (live ViT capture + training) is pending and
tracked in docs/advanced_features/vlm_dflash.md.
curnane-lab
force-pushed
the
vl-dflash-pr
branch
3 times, most recently
from
August 27, 2026 07:41
85ca930 to
f82627d
Compare
Co-authored-by: danaodai <chenjiayuan1077@163.com>
curnane-lab
force-pushed
the
vl-dflash-pr
branch
from
August 27, 2026 08:45
f82627d to
6f93319
Compare
Contributor
Author
|
Hi @jiapingW, could you take a review pass on this PR when free? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Image-text DFlash training is a long-standing request (#583, #461) and a high-priority item on the 2026 roadmap (#374: "General VLM training"). The pre-#678 HF-backend stack had community VLM prototypes (#585, #495, #505); after the unified-runtime consolidation (#678), training runs exclusively on server-side capture, and none of those prototypes apply anymore. The serving side already runs DFlash speculative decoding on both text and image-text inputs - the training side (SpecForge DFlash draft training) is text-only. This PR closes that training-side gap end-to-end on the current architecture.
Design: plain-rope draft (mRoPE-free)
The draft consumes visual information exclusively through the captured target hidden states - the target forward (ViT fusion + the target's own mRoPE) already encodes vision into the context features. The draft's rope is only an internal distance metric for its own attention, decoupled from the target's position scheme, so the target's (3, N) mRoPE positions carry no signal for the draft.
The draft therefore keeps the plain 1D rope convention - byte-identical to the text path - and reuses the official draft geometry (
configs/qwen3.5-4b-dflash.json, matching z-lab/Qwen3.5-4B-DFlash). Two hard wins:Modifications
Contracts/providers (
algorithms/dflash/providers.py,common/providers.py): DFlash registers a STREAMINGmultimodalFeatureContract ({input_ids, loss_mask, hidden_states}- the same three tensors as text capture) and aServerStreamingProviderwith a VLMServerInputAdapter.Data (
data/vlm_preprocessing.py,common/vlm_input.py): ShareGPT-style JSONL with an optionalimage/image_path/imagesfield (path or base64;imagestakes the single list element). The target's own chat template renders the image into the first user turn; the target's HF processor expands the image region in id space, yielding expandedinput_ids/loss_mask(trainer + passthrough) and collapsedrequest_input_ids+data:-URIimage_data(capture request). One image per sample (v1); text-only samples can mix in the same run. Unreadable images and multi-image samples fail loudly (ImageDataError), never a silent text-only downgrade.Capture (
inference/adapters/server_capture.py,training/disaggregated.py,launch_plan.py,patches/sglang/v0.5.14/spec-capture.patch): the patched server expands the image, runs the ViT, and writes aux hidden states straight to Mooncake; the capture request rides the stockinput_ids+image_data/generatepath. Managed capture servers setSGLANG_MM_AVOID_RETOKENIZE=1forinput_modality=multimodalso the server re-expands placeholders in id space with zero retokenization drift; client/server expansion mismatch fails loudly at the seq-len check. On Ascend hosts the capture servers default to--mm-attention-backend ascend_attnfor non-text modalities (sdpa vision backend materializes [heads, N, N] scores and OOMs on large images);model.sglang_mm_attention_backendoverrides.Draft model (
modeling/draft/dflash.py, partial rotation ported from [Feature] VLM DFlash Training: Multi-Model Support for Qwen3-VL / Qwen3.5 / Qwen3.6 #585 by @zyk42, authorship preserved): partial rotation inapply_rotary_pos_emb(rotary_dim < head_dim, for Qwen3.5/3.6partial_rotary_factor=0.25). The draft always uses the stockQwen3RotaryEmbedding; [Feature] VLM DFlash Training: Multi-Model Support for Qwen3-VL / Qwen3.5 / Qwen3.6 #585's interleaved-mRoPE rotary is deliberately dropped (see Design).Target handling (
modeling/target/target_utils.py,algorithms/model_providers.py): auto-selectmodel.language_model.embed_tokens.weightfor VLM targets; language-model depth via thetext_configfallback for draft config auto-generation.Training (
common/dflash_family_model.py):OnlineDFlashModelbuilds positions with the unconditional text-path 1D convention - multimodal batches take the byte-identical forward as text. Text-only runs are unchanged.Recipe:
examples/configs/online/disaggregated/external/qwen3.5-4b-vl-dflash-disaggregated.yaml, using the stock plain-ropeconfigs/qwen3.5-4b-dflash.json. Warm-starting from the official draft viamodel.draft_checkpoint_path(weights-only) is supported out of the box.Tests/docs:
tests/test_algorithms/test_dflash_multimodal.py(registration/provider gates, expansion math, request construction, 1D-positions regression); text-only assertions intest_configsuites updated; status docdocs/advanced_features/vlm_dflash.md.Scope notes (deliberate): DFlash only; one image per sample; offline (precomputed) multimodal capture not included; online evaluation for multimodal runs not included.
Related Issues
Accuracy Test
End-to-end (Ascend NPU, live run - PASSED): Qwen3.5-4B target + plain-rope DFlash draft, disaggregated managed run (5 capture servers + 5 trainer ranks, 32k context). Training runs end-to-end with healthy numerics: draft loss decays from ~5.7 to ~1, draft token accuracy rises from ~0.10 to ~0.65+, grad_norm stays bounded (0.4–1.1);
accuracy_denomoscillates with image/text batch composition as expected for mixed VLM batches.Unit/integration (CPU): registration parity and provider gates, request/payload construction, image-expansion math, loud image errors, data-URI transport, golden topology/recipe tests -
tests/test_algorithms/test_dflash_multimodal.pyplus updatedtest_configsuites pass.Patch integrity: the regenerated
spec-capture.patchapplies cleanly both ways to pristine sglang v0.5.14 (git apply --check/--reverse --check).Benchmark & Profiling
Serving-side validation of the trained plain-rope draft (stock serving stack, no engine patches). Dataset: CC-OCR (~6.7K image-text samples; responses greedy-regenerated with the target; test split N=200, disjoint from train; image modality, K=7). Baseline = the official z-lab/Qwen3.5-4B-DFlash production draft served as-is.
Metric definitions (accept metrics use the server-side convention; the client-side per-request accept length is not reliably reported):
SpecDecoding metricsSpecDecoding metricsExperiment 1: from-scratch (random init)
Draft trained from random initialization on the ~6.7K-sample train split, evaluated at step 2400 with 16 concurrent decoding instances.

Findings:
Experiment 2: warm-start (official draft init + in-domain fine-tune)
Draft warm-started from the official z-lab/Qwen3.5-4B-DFlash weights (

model.draft_checkpoint_path, weights-only), fine-tuned on the same ~6.7K data, early-stopped at step 1000. Evaluated on the test split with 8 decoding instances per side. (Official-baseline numbers differ slightly from Experiment 1 due to different instance counts/batches; each table's comparison is same-batch.)Findings:
Checklist