Skip to content

Load a Qwen3-VL checkpoint's language model as a diffusion prompt encoder - #987

Closed
vasilevklart wants to merge 4 commits into
mainfrom
users/kirill/qwen3vl-shared
Closed

vasilevklart wants to merge 4 commits into
mainfrom
users/kirill/qwen3vl-shared

Conversation

@vasilevklart

@vasilevklart vasilevklart commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

MiniMax-H3 (stacked on this) conditions on hidden_states[50] of Qwen3-VL-32B's language model, with its keyframes in the prompt. The scaffold's Qwen3 prompt encoder only took Qwen3 text configs.

The encoder now loads a Qwen3-VL checkpoint's language model: Qwen3EncoderConfig.from_qwen3_vl, the model.language_model. remap, and a skip predicate that leaves the tower and the unused layers on disk.

  • Text-only prompts keep the 1-D RoPE path, which equals interleaved MRoPE with three equal positions.
  • Prompts with images go through vl_prompt.qwen3_vl_prompt: the tower's tokens at the image pads, 3-D MRoPE positions and deepstack features. A caller's MRoPE tables bypass the encoder's cached rotary table from Performance 1/7: shared engine, conductor and component changes #858.
  • fused_qkv=False runs q/k/v as three GEMMs. Fused, cuBLAS rounds Qwen3-VL-32B's 1024-wide k/v differently from transformers.

On top of the tower that #1072 moved to components/qwen3_vit.py:

  • components/qwen_vision.py adds Qwen3-VL's deepstack mergers and fp32 rotary tables, as transformers keeps them.
  • VisionAttention.bind_resources takes a caller's resource key.
  • The MRoPE helpers move from qwen3_5 to components/mrope.py.

Qwen3.5 imports these under the old names and is otherwise unchanged: it still rounds its rotary tables to the activations' dtype and still refuses deepstack.

1×H200, real weights, through MiniMax-H3, vs transformers 5.22.1 hidden_states[50]
text prompt bit-exact
prompt with two 768x1344 keyframes bit-exact

Tests:

  • test_qwen3_vl_text_encoder.py: a tiny random Qwen3-VL, whose tap matches hidden_states[k] within 1e-5 for text, fused and unfused, and with two images of different grids.
  • test_qwen_vision_tower.py: against Qwen3VLVisionModel, deepstack on and off, bit for bit in bf16; Qwen3.5 keeps its key, its rounding and a prior binding.
  • On 6e5236766, main merged in at 3e98682, CPU Core fails only main's tests, by name. It merges cleanly with 9c05c56, where the H3, Qwen and scaffold CPU tests pass (270). ruff is clean.
  • MiniMax-H3's clips on top of this are bit-identical to the run before main was merged.

Not rerun: Qwen3.5's GPU image path (the captured block loop).

AI assistance (Claude) was used for this PR.

MiniMax-H3 conditions on hidden_states[50] of Qwen3-VL-32B's language model. For a
text-only prompt that model is the scaffold's Qwen3 encoder: interleaved MRoPE with
equal positions on its three axes rotates exactly as plain RoPE. Adds the config
reader (Qwen3EncoderConfig.from_qwen3_vl), the model.language_model. key remap, a
skip predicate that leaves the vision tower and the unused layers on disk, and an
unfused q/k/v option: fused, cuBLAS rounds the 1024-wide k/v differently from
transformers' three GEMMs.

For prompts with images: Qwen3.5's vision tower moves to components/qwen_vision.py,
with Qwen3-VL's deepstack mergers as an option, and the interleaved-MRoPE helpers to
components/mrope.py. qwen3_5 imports both under its old names, binds its own resource
key, keeps its rotary tables in the activations' dtype (transformers keeps fp32, the
new default) and still refuses deepstack. A tower layer keeps an attention bound
before when its node has no resource for it. vl_prompt.qwen3_vl_prompt splices the
tower's tokens in at the image pads, positions the prompt in 3-D and hands the
encoder its deepstack features.
No behaviour change. The encoder's docstring names Qwen3-VL, not the
model stacked on it. The key-name test of the skip predicate goes: the
parity tests load through it, and the loader refuses any key it lets
through or drops. The tower tests share one setup, and the Qwen3.5 test
checks the binding and the rounding, not the constructor's arguments.
Main's #1072 moved the Qwen3-VL ViT's shared parts into components/qwen3_vit.py
and built Qwen3.5's and Qwen3-Omni's towers on it. qwen3_5/components/vision.py
takes main's version; components/qwen_vision.py keeps only what Qwen3-VL adds on
that ViT (its patch mergers and deepstack, fp32 rotary tables), the vision config
Qwen3.5 shares, and FrameSDPA. The shared VisionAttention keeps its binding on a
node that doesn't declare the resource, since the Qwen3-VL prompt encoder binds
per-frame SDPA itself.

Claude-Session: https://claude.ai/code/session_01EM3QYUVD2VuhDBCfazeGcy
Conflicts with #858 in the Qwen3 text encoder, both sides kept: main's
cached rotary table, max_positions cap and host-known `padded`; this
branch's fused_qkv choice and the Qwen3-VL inputs (spliced embeddings,
caller-supplied 3-D MRoPE tables, deepstack). A caller's own rotary
tables bypass the cached one; the cap applies either way.

Claude-Session: https://claude.ai/code/session_01EM3QYUVD2VuhDBCfazeGcy
@mstar-review mstar-review Bot added size/M and removed size/L labels Nov 1, 2026
@vasilevklart vasilevklart changed the title Share Qwen3.5's vision tower with a Qwen3-VL prompt encoder Load a Qwen3-VL checkpoint's language model as a diffusion prompt encoder Nov 1, 2026
@vasilevklart

Copy link
Copy Markdown
Collaborator Author

Folded into #1155.

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.

1 participant