Skip to content

[RL] thinker video+image input - #6

Draft
yxs wants to merge 35 commits into
mainfrom
yxs/omni-thinker-vl-input
Draft

[RL] thinker video+image input#6
yxs wants to merge 35 commits into
mainfrom
yxs/omni-thinker-vl-input

Conversation

@yxs

@yxs yxs commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

Video + image input RL for the Qwen3-Omni thinker, via the pseudo-Qwen3-VL route: extract the thinker (visual tower + text backbone, audio dropped) as a qwen3_vl_moe HF checkpoint, so miles' existing bridge-based Qwen3-VL training path (packed mRoPE + deepstack + CP, miles_plugins/models/qwen3_vl.py) trains it directly. Rollout stays on the standalone sglang-omni server through the merged processed-multimodal contract (same as the audio PR). Stacked on #1.

Why this route

Field-by-field comparison of Qwen3-VL-30B-A3B vs the omni thinker (verified against both hub model.safetensors.index.json and transformers 5.x modeling code):

  • Visual towers are the same architecture (depth 27, hidden 1152, deepstack [8,16,24], merge 2, pos_embed 2304); every shared tensor is dimension-identical.
  • The masquerade is fully mechanical: prefix rewrite (thinker.model.* -> model.language_model.*, thinker.visual.* -> model.visual.*), three merger renames (ln_q->norm, mlp.0/2->linear_fc1/2, merger_list->deepstack_merger_list), and MoE experts fused from per-expert 2D to the VL hub layout (gate_up_proj [E,H,2I], down_proj [E,I,H] — the layout transformers, Megatron-Bridge import, and sglang's fused loaders all expect).
  • Omni numerics preserved: vocab 152064, rope_theta 1e6, max_position_embeddings 65536; rope_scaling normalized to the VL whitelist {rope_type, mrope_section, mrope_interleaved}.

Image positions are identical across HF-omni, HF-VL, and the Megatron-Bridge trainer (walked all three formulas) — image RL works on the existing VL path unmodified.

Video positions differ: omni uses TM-RoPE (t_index = k * second_per_grid * position_id_per_seconds, one contiguous block); VL/bridge use per-frame grids with timestamp text tokens (t constant within a frame). On omni-layout tokens the bridge rope scan silently mis-positions everything after frame 1, so the trainer needs an omni get_rope_index override for video (fields ride along under omni_sideband in the synthesized config).

Wire-cost benchmark (jingwen's serialize/deserialize concern, fp32)

modality case raw MB json MB ser ms dump ms deser ms
image 448px 4.6 6.1 7 13 14
video 8f@448 18.4 24.5 19 53 45
video 32f@448 73.5 98.0 78 212 197
video 64f@448 147.0 196.0 178 438 457
audio 30s 1.5 2.0 1 6 4

Video at 32-64 frames is ~100-200 MB and ~0.5-1.1 s CPU per request: workable for debug-scale runs, but scaling needs bf16-cast (halves it) and/or a binary body. examples/omni_thinker/bench_mm_payload.py reproduces this (add --server for HTTP timing).

Done in this draft

  • tools/extract_qwen3_omni_thinker.py --variant vl: streaming pseudo-VL extraction (renames + per-layer expert fusion + config synthesis incl. num_position_embeddings and omni_sideband), unit-tested down to fused-tensor values.
  • examples/omni_thinker/prepare_video_r1.py: Video-R1-260k (mixed image/video MCQ, <answer>X</answer>) -> miles jsonl; filters by data_type / source prefix / media presence; scored by --rm-type gpqa. Image runs can also use the in-repo geo3k pipeline as-is.
  • Payload benchmark (numbers above).

Status

  • Trainer wiring: --qwen3-omni-vl installs the omni TM-RoPE video override on the bridge model (video segments reroute to the ported formula — parity-tested against the HF omni reference on mixed text/image/video layouts; image/text delegate to the bridge formula, proven identical) and pops video_second_per_grid pre-forward (ordered-cursor consumption)
  • Provider freeze knobs cleared (freeze_language_model defaults would silently train only the mergers on some megatron-bridge versions)
  • Example: run_qwen3_omni_thinker_vl_grpo.py — Video-R1 CLEVRER (video) / Chart (image) tiers, external omni-server topology as in [RL] Qwen3-Omni thinker: audio-input GRPO #1, bridge load (no torch_dist convert)
  • Weight-sync name map pseudo_vl_to_omni_server_name (reverse of the extraction map incl. mergers; fused experts pass through to sglang's fused loaders) with extraction-roundtrip tests — not yet wired into the update path, v1 runs --debug-skip-weight-update
  • Rename shim wired into HfWeightIteratorBridge behind --qwen3-omni-vl (v1 freezes the visual tower + projection: rollout/training vision features stay identical and the thinker stage is the only sync target; text/lm_head tensors fail loud if unmapped)
  • GPU validation: pseudo-VL checkpoint loads via AutoBridge; image debug run; video debug run with the rope override; logprob-mismatch metrics

34 unit tests across prep / extraction / rope-parity / name-map / sync-shim.

🤖 Generated with Claude Code

JingwenGu0829 and others added 26 commits July 22, 2026 01:23
…>miles loop)

First omni<->miles RL loop on the Qwen3-Omni-30B-A3B thinker (text MoE).
…eight naming

- omni_thinker.py folded into generate_hub/sglang_omni.py: output_modalities/return_omni_rollout,
  pinned repetition_penalty=1.0, metadata passthrough, replay guard; temp-1 assert dropped
  (server sampler now returns post-temperature logprobs, matching trainer recompute)
- megatron_to_hf/qwen3omni_moe.py: body.* -> thinker.* (body.* is the Higgs namespace;
  sglang-omni thinker load_weights strips thinker. and ignores tower/talker names)
- extract tool: stream shards from flush() (peak RAM = one shard), fail loud on zero tensors
- tests: converter/extract moved under tests/fast/backends/megatron_utils with explicit
  register_cpu_ci; sglang_omni variant registered in generation_fixtures, harness basic+resume
- miles_plugins/models/qwen3_omni_thinker.py: patch GPTModel.forward to fill
  input_ids==audio_token_id rows from the omni checkpoint's frozen audio tower
  (decoder_input path); audio-only prompts keep sequential position ids, so RoPE
  is untouched; image/video keys rejected loudly; tower loaded lazily from the
  omni safetensors (audio_tower.* only) and kept out of DDP/optimizer/checkpoints
- model_provider: install hook behind --qwen3-omni-audio-encoder-path (classic + bridge)
- training_utils/data.get_batch: right-pad variable-length audio tensors
  (input_features/feature_attention_mask) to the micro-batch max before dim-0 concat
- examples/omni_thinker: prepare_avqa.py converts Joysw909/AVQA (r1aqa jsonl) into the
  miles <audio>-placeholder format, letter label + choices metadata scored by --rm-type gpqa;
  run script reworked to the external-server topology (--rollout-external + addr, server
  doubles as router), audio-tower injection flag, no SP, and --sync-mode skip|distributed
- sglang_engine: --rollout-external-admin-api sglang-omni no-ops /flush_cache and
  /begin|end_weight_update (routes the omni server does not expose; it quiesces+flushes
  internally) and delegates the post-update flush to the update request;
  --rollout-weight-update-stages scopes the three weight-group requests so a multi-stage
  server does not fan group init out to every stage and break NCCL world-size accounting
…/server_tp accounting

- _init_external: sglang-omni exposes no /get_server_info; health check is the only preflight
- example: decouple actor GPUs (TP=EP=actor_tp) from ray total (actor_tp + omni_server_tp
  external attach bundles); disagg placement, colocate stays off (external servers have no
  sleep/offload routes)
…e.jinja

The trainer loads AutoProcessor/AutoTokenizer from the extracted dir: preprocessor_config
et al. make the omni processor resolvable there, and AutoTokenizer only auto-loads the
.jinja template variant (chat_template.json is processor-level), so unpack it.
The default preflight polls /health_generate then /flush_cache; the omni server exposes
neither route, so the 404s retried forever and the external attach hung before rollout.
RolloutSamplingParams is extra=forbid; miles' detok-only keys (skip_special_tokens,
no_stop_trim, spaces_between_special_tokens) 422'd every request. Those flags never reach
the token/logprob streams, so drop them; unknown keys still fail loud.
…tinue

Both abort paths listed workers via the router /workers API and POSTed /abort_request;
a bare omni server has neither route, so the cleanup after a fully successful rollout
404-crashed the job. pause_generation(mode=abort) + continue_generation is the
omni-native abort_all.
…ter)

mcore hard-requires SP for MoE + TP>1 in training, so the no-SP constraint made every
2-GPU layout either invalid (TP2 no-SP) or OOM (TP1: full expert grad buffers). The
embedding output under SP is the rank's contiguous chunk; the scatter now slices the
placeholder mask and the encoder rows to that window. Debug tier drops the zero-coef
KL ref copy and trims the token budget.
@yxs
yxs force-pushed the yxs/omni-thinker-vl-input branch from cf2259b to 1a5fe8c Compare July 26, 2026 02:48
@yxs
yxs force-pushed the yxs/omni-thinker-vl-input branch from 1a5fe8c to c8b1619 Compare July 26, 2026 02:50
@yxs yxs changed the title [Omni] thinker video+image input RL: pseudo-Qwen3-VL bridge route (stacked on #1) [RL] thinker video+image input Jul 27, 2026
yxs added 8 commits July 27, 2026 12:09
- extract tool: drop the --variant vl path (belongs to the video/image PR #6); this
  branch keeps only the text-variant + processor artifacts
- remove test_extract_thinker_vl.py (lives on #6)
- sglang_omni.py: move the sampling-key frozensets below the import block
- audio injection: note the TP-redundant tower forward tradeoff under SP
…headroom

- miles_plugins/models/qwen3_omni_thinker_vl.py: video segments reroute to the ported
  omni get_rope_index (parity-tested against the HF reference on mixed layouts);
  image/text delegate to the bridge formula (proven identical); video_second_per_grid
  popped pre-forward and consumed via an ordered cursor; provider freeze knobs cleared
- example: expandable_segments for MoE dispatch allgather headroom
…oPE video override,

clears provider freeze knobs; Video-R1 example (CLEVRER video / Chart image tiers,
external omni server, sync skip until the rename shim is wired); pseudo-VL -> omni
server name map with extraction roundtrip tests
…er.* (frozen visual skipped)

v1 trains the text backbone only (visual tower + projection frozen), so rollout and
training vision features stay identical and the thinker stage is the only sync target;
the bridge iterator renames exports behind --qwen3-omni-vl and fails loud on unmapped
tensors.
…from #1)

The pseudo-Qwen3-VL extraction path (map_thinker_param_name_vl, synthesize_vl_config,
fuse_layer_experts_vl, _VlExpertAccumulator) belongs to this PR, not the audio PR #1;
carry it here explicitly so #1 stays the clean audio milestone and #6 owns the VL delta.
@yxs
yxs force-pushed the yxs/omni-thinker-vl-input branch from 6ce8101 to 039d1c1 Compare July 27, 2026 04:11
@yxs
yxs force-pushed the main branch 2 times, most recently from bb21444 to 6c2fe27 Compare August 1, 2026 16:13
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