Fix qwen3-vl model update in non-colocate mode - #111
Merged
Conversation
Contributor
There was a problem hiding this comment.
Code Review
This pull request adds weight conversion mappings for Qwen3-VL vision models to Hugging Face format and integrates Megatron-Bridge HF weight export for non-colocate vLLM weight synchronization. A critical feedback points out a potential deadlock issue when pipeline parallelism is greater than 1, caused by calling dist.barrier inside the chunk loop where different ranks may have a different number of chunks; moving the barrier outside the loop is recommended to resolve this.
andakai
force-pushed
the
adk/fix-qwen3vl-modelupdate
branch
from
June 1, 2026 15:38
faa4ed8 to
6daf3fb
Compare
Signed-off-by: Dakai An <dakaian108@gmail.com>
andakai
force-pushed
the
adk/fix-qwen3vl-modelupdate
branch
from
June 1, 2026 15:40
6daf3fb to
adc9e5e
Compare
Signed-off-by: Dakai An <dakaian108@gmail.com>
aoshen02
added a commit
that referenced
this pull request
Jun 3, 2026
The built-in OPD reward_func (vime/rollout/on_policy_distillation.py) sent vime's
disaggregated /inference/v1/generate request body (token_ids + nested
sampling_params, top-level prompt_logprobs response) but the CI test pointed
--rm-url at /v1/completions, so the teacher rejected the unknown schema with HTTP
400 — which resp.raise_for_status() turned into an aiohttp ClientResponseError whose
CIMultiDictProxy headers fail to pickle across Ray, masking the real error as
"can't pickle CIMultiDictProxy".
Fix: align the test URL to /inference/v1/generate (the endpoint the body targets and
the one vime's own rollout uses), and harden + extend reward_func:
- text: POST {token_ids, sampling_params{max_tokens:1, temperature:0,
prompt_logprobs:1, skip_special_tokens:False}}; model only when --opd-teacher-model
is set (vLLM accepts a missing model -> default served model).
- multimodal: render the (text + image_url) messages via the teacher's
/v1/chat/completions/render to get token_ids + features, attach the student's
canonical full prompt+response token_ids (re-aligning the feature placeholders),
and score with prompt_logprobs — reusing vime.rollout.vllm_rollout's proven
render->features helpers. This is why /inference/v1/generate is used over the
OpenAI /v1/completions: it is the only vLLM endpoint that carries multimodal
features, so one code path scores both text and image teachers.
- replace resp.raise_for_status() with an explicit non-200 -> RuntimeError carrying
the status + body (picklable across Ray; surfaces the real teacher error).
- post_process reads top-level prompt_logprobs (GenerateResponse shape).
Validated on gb200, EXIT_RC=0 both:
- text (Qwen2.5-0.5B, gsm8k): teacher_log_probs flow into opd_reverse_kl.
- multimodal (Qwen3-VL-8B self-distill, geo3k): render->features->generate scores
image samples, teacher_log_probs flow into opd_reverse_kl. (MM weight sync needs
PR #111's qwen3-vl non-colocate update_weights fix.)
Fixes #12.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>
aoshen02
added a commit
that referenced
this pull request
Jun 3, 2026
The built-in OPD reward_func (vime/rollout/on_policy_distillation.py) sent vime's
disaggregated /inference/v1/generate request body (token_ids + nested
sampling_params, top-level prompt_logprobs response) but the CI test pointed
--rm-url at /v1/completions, so the teacher rejected the unknown schema with HTTP
400 — which resp.raise_for_status() turned into an aiohttp ClientResponseError whose
CIMultiDictProxy headers fail to pickle across Ray, masking the real error as
"can't pickle CIMultiDictProxy".
Fix: align the test URL to /inference/v1/generate (the endpoint the body targets and
the one vime's own rollout uses), and harden + extend reward_func:
- text: POST {token_ids, sampling_params{max_tokens:1, temperature:0,
prompt_logprobs:1, skip_special_tokens:False}}; model only when --opd-teacher-model
is set (vLLM accepts a missing model -> default served model).
- multimodal: render the (text + image_url) messages via the teacher's
/v1/chat/completions/render to get token_ids + features, attach the student's
canonical full prompt+response token_ids (re-aligning the feature placeholders),
and score with prompt_logprobs — reusing vime.rollout.vllm_rollout's proven
render->features helpers. This is why /inference/v1/generate is used over the
OpenAI /v1/completions: it is the only vLLM endpoint that carries multimodal
features, so one code path scores both text and image teachers.
- replace resp.raise_for_status() with an explicit non-200 -> RuntimeError carrying
the status + body (picklable across Ray; surfaces the real teacher error).
- post_process reads top-level prompt_logprobs (GenerateResponse shape).
Validated on gb200, EXIT_RC=0 both:
- text (Qwen2.5-0.5B, gsm8k): teacher_log_probs flow into opd_reverse_kl.
- multimodal (Qwen3-VL-8B self-distill, geo3k): render->features->generate scores
image samples, teacher_log_probs flow into opd_reverse_kl. (MM weight sync needs
PR #111's qwen3-vl non-colocate update_weights fix.)
Fixes #12.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>
aoshen02
added a commit
that referenced
this pull request
Jun 3, 2026
The built-in OPD reward_func (vime/rollout/on_policy_distillation.py) sent vime's
disaggregated /inference/v1/generate request body (token_ids + nested
sampling_params, top-level prompt_logprobs response) but the CI test pointed
--rm-url at /v1/completions, so the teacher rejected the unknown schema with HTTP
400 — which resp.raise_for_status() turned into an aiohttp ClientResponseError whose
CIMultiDictProxy headers fail to pickle across Ray, masking the real error as
"can't pickle CIMultiDictProxy".
Fix: align the test URL to /inference/v1/generate (the endpoint the body targets and
the one vime's own rollout uses), and harden + extend reward_func:
- text: POST {token_ids, sampling_params{max_tokens:1, temperature:0,
prompt_logprobs:1, skip_special_tokens:False}}; model only when --opd-teacher-model
is set (vLLM accepts a missing model -> default served model).
- multimodal: render the (text + image_url) messages via the teacher's
/v1/chat/completions/render to get token_ids + features, attach the student's
canonical full prompt+response token_ids (re-aligning the feature placeholders),
and score with prompt_logprobs — reusing vime.rollout.vllm_rollout's proven
render->features helpers. This is why /inference/v1/generate is used over the
OpenAI /v1/completions: it is the only vLLM endpoint that carries multimodal
features, so one code path scores both text and image teachers.
- replace resp.raise_for_status() with an explicit non-200 -> RuntimeError carrying
the status + body (picklable across Ray; surfaces the real teacher error).
- post_process reads top-level prompt_logprobs (GenerateResponse shape).
Validated on gb200, EXIT_RC=0 both:
- text (Qwen2.5-0.5B, gsm8k): teacher_log_probs flow into opd_reverse_kl.
- multimodal (Qwen3-VL-8B self-distill, geo3k): render->features->generate scores
image samples, teacher_log_probs flow into opd_reverse_kl. (MM weight sync needs
PR #111's qwen3-vl non-colocate update_weights fix.)
Fixes #12.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: aoshen02 <aoshen@inferact.ai>
CalvinXKY
pushed a commit
that referenced
this pull request
Jun 3, 2026
…ultimodal) (#141) * fix(opd): score teacher over /inference/v1/generate (text + multimodal) The built-in OPD reward_func (vime/rollout/on_policy_distillation.py) sent vime's disaggregated /inference/v1/generate request body (token_ids + nested sampling_params, top-level prompt_logprobs response) but the CI test pointed --rm-url at /v1/completions, so the teacher rejected the unknown schema with HTTP 400 — which resp.raise_for_status() turned into an aiohttp ClientResponseError whose CIMultiDictProxy headers fail to pickle across Ray, masking the real error as "can't pickle CIMultiDictProxy". Fix: align the test URL to /inference/v1/generate (the endpoint the body targets and the one vime's own rollout uses), and harden + extend reward_func: - text: POST {token_ids, sampling_params{max_tokens:1, temperature:0, prompt_logprobs:1, skip_special_tokens:False}}; model only when --opd-teacher-model is set (vLLM accepts a missing model -> default served model). - multimodal: render the (text + image_url) messages via the teacher's /v1/chat/completions/render to get token_ids + features, attach the student's canonical full prompt+response token_ids (re-aligning the feature placeholders), and score with prompt_logprobs — reusing vime.rollout.vllm_rollout's proven render->features helpers. This is why /inference/v1/generate is used over the OpenAI /v1/completions: it is the only vLLM endpoint that carries multimodal features, so one code path scores both text and image teachers. - replace resp.raise_for_status() with an explicit non-200 -> RuntimeError carrying the status + body (picklable across Ray; surfaces the real teacher error). - post_process reads top-level prompt_logprobs (GenerateResponse shape). Validated on gb200, EXIT_RC=0 both: - text (Qwen2.5-0.5B, gsm8k): teacher_log_probs flow into opd_reverse_kl. - multimodal (Qwen3-VL-8B self-distill, geo3k): render->features->generate scores image samples, teacher_log_probs flow into opd_reverse_kl. (MM weight sync needs PR #111's qwen3-vl non-colocate update_weights fix.) Fixes #12. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: aoshen02 <aoshen@inferact.ai> * Update on_policy_distillation.py Signed-off-by: aoshen02 <aoshen@inferact.ai> * Update on_policy_distillation.py Signed-off-by: aoshen02 <aoshen@inferact.ai> --------- Signed-off-by: aoshen02 <aoshen@inferact.ai> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Summary
Try to solve this issue: #106
This PR fixes Qwen3-VL online weight update when Megatron training and vLLM
rollout run in non-colocate mode.
The main change is to make non-colocate vLLM weight update honor
--megatron-to-hf-mode bridge. When bridge mode is selected, the trainer nowuses Megatron-Bridge to export Hugging Face compatible weight names and tensors,
then sends those exported tensors to vLLM through the existing non-colocate NCCL
weight transfer path.
The PR also fixes the legacy Qwen3-VL raw converter's visual-tower name mapping
so raw mode remains usable for dense Qwen3-VL models.
Problem
Before this PR, colocate mode and non-colocate mode did not handle
--megatron-to-hf-mode bridgeconsistently.This was fragile for Qwen3-VL because its visual tower uses names that differ
between Megatron internals and the HF/vLLM loader layout. For example, the old
raw converter could produce visual names under a Megatron-style
decoder.layers.*layout, while vLLM expects HF-style names undermodel.visual.blocks.*.The transport layer itself was not the root cause. Non-colocate NCCL transfer
only sends the names and tensors it receives. The problem was that the
non-colocate bridge configuration still bypassed Megatron-Bridge conversion and
could send vLLM parameter names that its Qwen3-VL loader did not recognize.
Changes
1. Use Megatron-Bridge in non-colocate bridge mode
File:
Changes:
weights_getterinUpdateWeightFromDistributed.HfWeightIteratorBasewhenargs.megatron_to_hf_mode == "bridge"._sync_weights_to_rollout_engines()to a bridge-specific sync pathwhen the iterator exists.
self.weights_getter()to get Megatron local weights,self._hf_weight_iterator.get_hf_weight_chunks(...),helpers.
The bridge path is now:
The raw path still uses the existing non-colocate flow:
2. Fix Qwen3-VL raw visual-tower mappings
File:
The legacy raw converter now maps Qwen3-VL visual parameters to the HF/vLLM
layout more accurately:
This keeps raw mode runnable for dense Qwen3-VL while bridge remains the
preferred path for Qwen3-VL non-colocate runs.
Why Megatron-Bridge
Megatron-Bridge is model-aware. For models like Qwen3-VL, the conversion from
Megatron parameter names to HF/vLLM names depends on multimodal model structure:
language layers, vision blocks, merger layers, patch embedding, multimodal
rotary embeddings, and model-specific layernorm names.
Maintaining all of that as hand-written slime conversion logic is brittle.
Using Megatron-Bridge in bridge mode makes Bridge the source of truth for
model-specific HF export, while slime remains responsible for transport to
vLLM.
This matches the same separation used by SkyRL-style Megatron weight sync:
Behavior after this PR
For Qwen3-VL geo3k scripts, the configured mode is bridge:
Validation
Functional smoke: Qwen3-VL non-colocate bridge
Goal: verify that non-colocate vLLM rollout can update Qwen3-VL weights through
Megatron-Bridge.
Result:
Qwen3-VL-2B-Instruct--megatron-to-hf-mode bridge/update_weights/update_weightsKey successful log signals:
Update-time comparison: Qwen3-4B
Goal: measure the overhead of bridge mode on a text-only dense model.
Setup:
Qwen3-4Bactor_model.update_weights()callsResult:
1.5, 0.3, 0.3, 0.3, 0.31.7, 0.5, 0.5, 0.5, 0.5Interpretation:
0.2ssteady-state overhead in this setup.on models where it is known to be correct.
Update-time comparison: Qwen3-VL-4B-Instruct
Goal: measure the same overhead on the target multimodal model.
Setup:
Qwen3-VL-4B-Instruct/mnt/data2/dakai/huggingface_cache/hub/models--Qwen--Qwen3-VL-4B-Instruct/snapshots/ebb281ec70b05090aa6165b016eac8ec08e71b17/mnt/data2/dakai/vime-fix_qwen3vl_modelupdate/codex_tmp/Qwen3-VL-4B-Instruct_torch_distactor_model.update_weights()callsResult:
1.9, 0.5, 0.5, 0.4, 0.53.1, 0.8, 0.8, 0.8, 0.8Both modes completed:
/start_weight_updatecalls/finish_weight_updatecalls/update_weightschunk updatesKeyErrorInterpretation:
0.3ssteady-state overhead in this setup.hand-written visual parameter mapping in the correctness-sensitive path.
TP compatibility
Goal: verify non-colocate update with Megatron TP>1.
Result:
1.8scodex_tmp/qwen3_4b_update_compare_logs/bridge_tp2_eager_repeat1.log2.1scodex_tmp/qwen3vl_4b_update_compare_logs/bridge_tp4_repeat1.log2.3scodex_tmp/qwen3vl_4b_update_compare_logs/raw_tp4_repeat1_retry.logBoth Qwen3-VL-4B-Instruct TP=4 runs completed one repeat update with:
/start_weight_update200/finish_weight_update200/update_weightschunk updatesKeyErrorChecklist