diff --git a/docs/start/models.md b/docs/start/models.md index 277329538..b29ed8e1a 100644 --- a/docs/start/models.md +++ b/docs/start/models.md @@ -222,7 +222,6 @@ For version requirements and detailed setup instructions, see | GSPO (text) | `examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_lora_v1.sh` | 4×H100/H200 80GB | | GSPO (image) | `examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_lora_mmk12_v1.sh` | 4×H100/H200 80GB | | GSPO (AVQA, NPU) | `examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_npu_avqa_v1.sh` | 16×NPU (Atlas 800T A3) | -| GSPO (full, NPU) | `examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_npu.sh` | 16×NPU (Atlas 800T A3) | | Offline DPO (LoRA) | `examples/dpo_trainer/qwen3_omni/qwen3_omni/run_qwen3_omni_omni_preference_lora.sh` | 4×H800 | | [GSPO OPD (image, NPU)](../algo/omni_opd.md) | `examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_lora_mmk12_v1_opd_npu.sh` | 32×NPU (2 x Atlas 800T A3) | diff --git a/examples/gspo_trainer/README.md b/examples/gspo_trainer/README.md index d2d01b315..28a37becb 100644 --- a/examples/gspo_trainer/README.md +++ b/examples/gspo_trainer/README.md @@ -12,9 +12,6 @@ Both **GPU** and **NPU** training platforms are supported: - `examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_lora_v1.sh` — **GPU**, **LoRA (r=32)** on a single node with **4 × H800 80GB**. -- `examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_npu.sh` - — **NPU**, **full-parameter** on a single **Atlas 800T A3** node with - **16 × Ascend 910C 64GB**. - [`run_qwen3_omni_thinker_gspo_lora_avqa_v1.sh`](qwen3_omni/run_qwen3_omni_thinker_gspo_lora_avqa_v1.sh) — **GPU**, **LoRA (r=32) V1** for text + image + audio AVQA training. - [`run_qwen3_omni_thinker_gspo_npu_avqa_v1.sh`](qwen3_omni/run_qwen3_omni_thinker_gspo_npu_avqa_v1.sh) @@ -47,21 +44,15 @@ python -c "import verl, verl_omni, vllm, vllm_omni; print('OK')" The GPU V1 and AVQA NPU launchers use `verl_omni.trainer.main_omni` and set `VERL_USE_EXTERNAL_MODULES=verl_omni`. Processor/model setup is handled by the -registered Qwen3-Omni V1 adapter, so these launchers do not load the deprecated -model monkey-patches through `external_lib`. The existing generic NPU launcher -is left unchanged for backward compatibility. +registered Qwen3-Omni V1 adapter, so these launchers do not load model +monkey-patches through `external_lib`. The launchers colocate the FSDP actor and the `vllm-omni` rollout on the same devices. `run_qwen3_omni_thinker_gspo_lora_v1.sh` targets a single node with -**4 × H800 80GB**; `run_qwen3_omni_thinker_gspo_npu.sh` targets a single -**Atlas 800T A3** node with **16 × Ascend 910C 64GB** (full-parameter FSDP -actor, rollout TP=2). The AVQA NPU launcher dynamically generates a thinker-only -deploy config for each rollout replica from that replica's visible devices, -avoiding cross-replica device-rank collisions. - -> **Deprecated:** `run_qwen3_omni_thinker_gspo_lora.sh` retains the old -> `verl.trainer.main_ppo` and model monkey-patch path for backward compatibility. -> New development should use the V1 launchers. +**4 × H800 80GB**; `run_qwen3_omni_thinker_gspo_npu_avqa_v1.sh` targets a single +**Atlas 800T A3** node with **16 × Ascend 910C 64GB**. It dynamically generates +a thinker-only deploy config for each rollout replica from that replica's +visible devices, avoiding cross-replica device-rank collisions. ## Prepare the model @@ -94,14 +85,10 @@ Launch from the repository root — pick the flavor that matches your hardware: # GPU, LoRA (r=32), 4 × H800 — V1 trainer (recommended) bash examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_lora_v1.sh -# NPU, full-parameter, Atlas 800T A3 (16 × Ascend 910C 64GB) -bash examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_npu.sh +# NPU, AVQA, Atlas 800T A3 (16 × Ascend 910C 64GB) — V1 trainer +bash examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_npu_avqa_v1.sh ``` -> **Deprecated:** `run_qwen3_omni_thinker_gspo_lora.sh` (old -> `verl.trainer.main_ppo` entrypoint with `external_lib` monkey-patches) -> is kept for backward compatibility but no longer recommended. - The V1 launchers use pure CLI overrides on `verl_omni.trainer.main_omni` (no `--config-path/--config-name`, no recipe YAML). Config precedence, lowest to highest: @@ -378,13 +365,7 @@ examples/gspo_trainer/ │ ├── run_qwen3_omni_thinker_gspo_lora_v1.sh ← V1 launch script (GPU, LoRA r=32, text) │ ├── run_qwen3_omni_thinker_gspo_lora_mmk12_v1.sh ← V1 launch script (GPU, LoRA r=32, image) │ ├── run_qwen3_omni_thinker_gspo_lora_avqa_v1.sh ← V1 launch script (GPU, LoRA r=32, audio + image) -│ ├── run_qwen3_omni_thinker_gspo_lora.sh ← deprecated (old main_ppo entrypoint) -│ ├── run_qwen3_omni_thinker_gspo_npu.sh ← launch script (NPU, full-parameter) │ ├── run_qwen3_omni_thinker_gspo_npu_avqa_v1.sh ← V1 launch script (NPU, AVQA) -│ ├── config/ -│ │ └── qwen3_omni_thinker_gspo.yaml ← old recipe config (deprecated path only) -│ ├── qwen3_omni_thinker_only.yaml ← old vllm-omni stage config (deprecated path only) -│ └── qwen3_omni_thinker_only_npu.yaml ← old vllm-omni stage config (deprecated path only) ├── data_process/ │ ├── mmk12.py ← MMK12 → verl RL parquet converter │ └── avqa.py ← AVQA → verl RL parquet converter diff --git a/examples/gspo_trainer/qwen3_omni/config/qwen3_omni_thinker_gspo.yaml b/examples/gspo_trainer/qwen3_omni/config/qwen3_omni_thinker_gspo.yaml deleted file mode 100644 index 8af8b229c..000000000 --- a/examples/gspo_trainer/qwen3_omni/config/qwen3_omni_thinker_gspo.yaml +++ /dev/null @@ -1,114 +0,0 @@ -# Qwen3-Omni Thinker GSPO + LoRA training config (FSDP + vLLM-Omni AR rollout). -# -# Inherits verl's default ppo_trainer config and overrides the recipe-specific -# fields below. Volatile values (data/model paths, GPU/node counts, the -# vLLM-Omni stage config path) are left to be set via CLI in the run script. -# -# Launch with: -# python3 -m verl.trainer.main_ppo \ -# --config-path= --config-name=qwen3_omni_thinker_gspo - -hydra: - searchpath: - - pkg://verl.trainer.config - -defaults: - - ppo_trainer - - _self_ - -data: - train_batch_size: 8 - max_prompt_length: 1024 - max_response_length: 8192 - val_max_samples: 1000 - validation_shuffle: true - filter_overlong_prompts: true - truncation: left - -actor_rollout_ref: - - model: - override_config: - attn_implementation: sdpa - lora_rank: 64 - lora_alpha: 32 - target_modules: "q_proj,k_proj,v_proj,o_proj,gate_proj,up_proj,down_proj" - exclude_modules: ".*talker.*|.*code2wav.*|.*code_predictor.*|.*visual.*|.*audio_tower.*" - use_remove_padding: true - enable_gradient_checkpointing: true - - actor: - freeze_vision_tower: true - optim: - lr: 1e-6 - lr_warmup_steps: 10 - weight_decay: 0.1 - clip_grad: 1.0 - ppo_mini_batch_size: 8 - ppo_micro_batch_size_per_gpu: 2 - use_kl_loss: true - kl_loss_coef: 0.001 - kl_loss_type: low_var_kl - entropy_coeff: 0 - strategy: fsdp - fsdp_config: - param_offload: true - optimizer_offload: true - model_dtype: bf16 - use_orig_params: true - wrap_policy: - min_num_params: 100000000 - policy_loss: - loss_mode: gspo - clip_ratio_low: 3e-4 - clip_ratio_high: 4e-4 - loss_agg_mode: seq-mean-token-mean - - rollout: - name: vllm_omni - mode: async - n: 8 - temperature: 0.8 - top_p: 0.9 - top_k: -1 - tensor_model_parallel_size: 4 - # NOTE: engine memory/batching (gpu_memory_utilization, max_num_seqs, dtype, - # load_format, LoRA, ...) is set in the vLLM-Omni stage config - # (qwen3_omni_thinker_only.yaml). vLLM-Omni ignores these top-level args when - # stage_configs_path is set, so they are intentionally not duplicated here. - calculate_log_probs: true - load_format: safetensors - layered_summon: true - log_prob_micro_batch_size_per_gpu: 4 - engine_kwargs: - vllm_omni: - output_mode: ar - - ref: - log_prob_micro_batch_size_per_gpu: 4 - strategy: fsdp - fsdp_config: - param_offload: true - model_dtype: bf16 - use_orig_params: true - wrap_policy: - min_num_params: 100000000 - -algorithm: - adv_estimator: grpo - use_kl_in_reward: false - -reward: - reward_manager: - name: dapo - -trainer: - val_before_train: false - critic_warmup: 0 - logger: ["console", "wandb"] - project_name: qwen3_omni_thinker_rl - experiment_name: gspo_lora_math - nnodes: 1 - save_freq: 20 - test_freq: 25 - total_epochs: 5 diff --git a/examples/gspo_trainer/qwen3_omni/qwen3_omni_thinker_only.yaml b/examples/gspo_trainer/qwen3_omni/qwen3_omni_thinker_only.yaml deleted file mode 100644 index 0131ec4b0..000000000 --- a/examples/gspo_trainer/qwen3_omni/qwen3_omni_thinker_only.yaml +++ /dev/null @@ -1,49 +0,0 @@ -# Thinker-only stage config for Qwen3-Omni RL training with verl. -# -# vLLM-Omni normally loads 3 stages (Thinker, Talker, Code2Wav), splitting -# GPUs across them. For Thinker-only RL (GSPO/GRPO), we need a single -# stage that uses ALL available GPUs for tensor parallelism. -# -# Adjust `devices` and `tensor_parallel_size` to match your GPU count. -stage_args: - - stage_id: 0 - runtime: - devices: "0,1,2,3" - engine_args: - # ── Stage identity ── - model_stage: thinker - model_arch: Qwen3OmniMoeThinkerForConditionalGeneration - worker_type: ar - scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler - engine_output_type: text - hf_config_name: thinker_config - - # ── Parallelism & backend ── - tensor_parallel_size: 4 - distributed_executor_backend: "mp" - - # ── Memory & batching (critical: top-level CLI args are stripped) ── - gpu_memory_utilization: 0.4 - max_num_seqs: 64 - max_num_batched_tokens: 32768 - enable_chunked_prefill: true - - # ── Model loading ── - dtype: bfloat16 - load_format: safetensors - trust_remote_code: true - enable_prefix_caching: false - enforce_eager: true - - # ── LoRA (required for adapter hot-loading) ── - enable_lora: true - max_lora_rank: 64 - max_loras: 1 - - # ── verl integration ── - enable_sleep_mode: true - logprobs_mode: processed_logprobs - disable_log_stats: true - final_output: true - final_output_type: text - is_comprehension: true diff --git a/examples/gspo_trainer/qwen3_omni/qwen3_omni_thinker_only_npu.yaml b/examples/gspo_trainer/qwen3_omni/qwen3_omni_thinker_only_npu.yaml deleted file mode 100644 index c3f1f6977..000000000 --- a/examples/gspo_trainer/qwen3_omni/qwen3_omni_thinker_only_npu.yaml +++ /dev/null @@ -1,50 +0,0 @@ -# Thinker-only stage config for Qwen3-Omni RL training with verl. -# -# vLLM-Omni normally loads 3 stages (Thinker, Talker, Code2Wav), splitting -# GPUs across them. For Thinker-only RL (GSPO/GRPO), we need a single -# stage that uses ALL available GPUs for tensor parallelism. -# -# Adjust `devices` and `tensor_parallel_size` to match your GPU count. -stage_args: - - stage_id: 0 - runtime: - devices: "0,1" - engine_args: - # ── Stage identity ── - model_stage: thinker - model_arch: Qwen3OmniMoeThinkerForConditionalGeneration - worker_type: ar - scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler - engine_output_type: text - hf_config_name: thinker_config - - # ── Parallelism & backend ── - tensor_parallel_size: 2 - distributed_executor_backend: "mp" - - # ── Memory & batching (critical: top-level CLI args are stripped) ── - max_model_len: 16384 - gpu_memory_utilization: 0.6 - max_num_seqs: 32 - max_num_batched_tokens: 32768 - enable_chunked_prefill: true - - # ── Model loading ── - dtype: bfloat16 - load_format: safetensors - trust_remote_code: true - enable_prefix_caching: false - enforce_eager: false - - # ── MoE backend ── - moe_backend: auto - - # ── verl integration ── - enable_sleep_mode: true - logprobs_mode: processed_logprobs - disable_log_stats: true - additional_config: - weight_nz_mode: 0 - final_output: true - final_output_type: text - is_comprehension: true diff --git a/examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_lora.sh b/examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_lora.sh deleted file mode 100755 index 82728379d..000000000 --- a/examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_lora.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env bash -# DEPRECATED: This script uses the legacy verl.trainer.main_ppo entrypoint with -# external_lib monkey-patches, and is deprecated. It will be removed in v0.3.0. -# Please use the V1 trainer instead: -# bash examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_lora_v1.sh -# For image-input (MMK12) training on the V1 trainer: -# bash examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_lora_mmk12_v1.sh -# -# Qwen3-Omni Thinker GSPO + LoRA training (FSDP + vLLM-Omni AR rollout). -# Hardware: 4× H100 80GB. -# -# Recipe config lives in config/qwen3_omni_thinker_gspo.yaml (inherits verl's -# ppo_trainer). Only volatile values (paths, GPU/node counts) are set here. -set -x - -export NCCL_IB_DISABLE=1 -export CPATH=/usr/include${CPATH:+:$CPATH} -export RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0 - -# Load verl_omni on the driver (rollout adapter) + the Qwen3-Omni patches (processor / automodel); workers also load the model patch via external_lib in the launch args. -export VERL_USE_EXTERNAL_MODULES=verl_omni,verl_omni.models.transformers.qwen3_omni_thinker - -MODEL_PATH=${MODEL_PATH:-"Qwen/Qwen3-Omni-30B-A3B-Instruct"} -TRAIN_FILE=${TRAIN_FILE:-"$HOME/data/math/train.parquet"} -VAL_FILE=${VAL_FILE:-"$HOME/data/math/test.parquet"} - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -STAGE_CONFIG="${SCRIPT_DIR}/qwen3_omni_thinker_only.yaml" - -python3 -m verl.trainer.main_ppo \ - --config-path="${SCRIPT_DIR}/config" \ - --config-name=qwen3_omni_thinker_gspo \ - data.train_files="${TRAIN_FILE}" \ - data.val_files="${VAL_FILE}" \ - actor_rollout_ref.model.path="${MODEL_PATH}" \ - actor_rollout_ref.model.external_lib=verl_omni.models.transformers.qwen3_omni_thinker \ - ++actor_rollout_ref.rollout.engine_kwargs.vllm_omni.stage_configs_path="${STAGE_CONFIG}" \ - trainer.n_gpus_per_node=4 \ - trainer.nnodes=1 \ - "$@" diff --git a/examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_npu.sh b/examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_npu.sh deleted file mode 100644 index 7ebf182b1..000000000 --- a/examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_npu.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash -# DEPRECATED: This NPU full-parameter training script uses the legacy -# verl.trainer.main_ppo entrypoint with external_lib monkey-patches, and is -# deprecated. It will be removed in v0.3.0. -# There is no direct NPU V1 equivalent without AVQA data setup. For the AVQA -# recipe on the V1 trainer, use: -# bash examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_npu_avqa_v1.sh -# For the general V1 trainer pattern, see: -# bash examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_lora_v1.sh -# Developers can adapt verl_omni.trainer.main_omni for new NPU training runs. -# -# Qwen3-Omni Thinker GSPO full-parameter training (FSDP + vLLM-Omni AR rollout). -# Hardware: Atlas 800T A3 (16 × Ascend 910C 64GB). -set -x - -export CPATH=/usr/include${CPATH:+:$CPATH} -export VLLM_ASCEND_ENABLE_NZ=0 -export VERL_DATAPROTO_SERIALIZATION_METHOD=numpy -export VERL_USE_EXTERNAL_MODULES=verl_omni,verl_omni.models.transformers.qwen3_omni_thinker - -MODEL_PATH=${MODEL_PATH:-"Qwen/Qwen3-Omni-30B-A3B-Instruct"} -TRAIN_FILE=${TRAIN_FILE:-"$HOME/data/math/train.parquet"} -VAL_FILE=${VAL_FILE:-"$HOME/data/math/test.parquet"} - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -STAGE_CONFIG="${SCRIPT_DIR}/qwen3_omni_thinker_only_npu.yaml" - -NUM_GPUS_ACTOR_ROLLOUT_REWARD=16 -ROLLOUT_TP=2 - -python3 -m verl.trainer.main_ppo \ - --config-path="${SCRIPT_DIR}/config" \ - --config-name=qwen3_omni_thinker_gspo \ - data.train_files="${TRAIN_FILE}" \ - data.val_files="${VAL_FILE}" \ - data.filter_overlong_prompts_workers=64 \ - actor_rollout_ref.model.lora_rank=0 \ - actor_rollout_ref.model.path="${MODEL_PATH}" \ - actor_rollout_ref.model.external_lib=verl_omni.models.transformers.qwen3_omni_thinker \ - ++actor_rollout_ref.rollout.engine_kwargs.vllm_omni.stage_configs_path="${STAGE_CONFIG}" \ - actor_rollout_ref.actor.fsdp_config.use_torch_compile=False \ - actor_rollout_ref.rollout.tensor_model_parallel_size=${ROLLOUT_TP} \ - actor_rollout_ref.rollout.gpu_memory_utilization=0.6 \ - actor_rollout_ref.rollout.agent.num_workers=$((NUM_GPUS_ACTOR_ROLLOUT_REWARD / ROLLOUT_TP)) \ - trainer.logger='["console","tensorboard"]' \ - trainer.project_name='qwen3_omni_thinker_rl' \ - trainer.experiment_name='gspo_math_npu' \ - trainer.n_gpus_per_node=${NUM_GPUS_ACTOR_ROLLOUT_REWARD} \ - trainer.nnodes=1 \ - trainer.save_freq=100 \ - "$@" diff --git a/tests/gpu_smoke/select_gpu_smoke_groups.py b/tests/gpu_smoke/select_gpu_smoke_groups.py index 17c980338..53d913289 100644 --- a/tests/gpu_smoke/select_gpu_smoke_groups.py +++ b/tests/gpu_smoke/select_gpu_smoke_groups.py @@ -68,7 +68,6 @@ class SmokeGroup: "ci-e2e-omni": ( "tests/gpu_smoke/run_gpu_smoke_omni_e2e.sh", "tests/special_e2e/*omni*", - "verl_omni/models/transformers/qwen3_omni_thinker.py", "verl_omni/trainer/config/omni/**", "verl_omni/trainer/omni/**", ), diff --git a/tests/special_e2e/qwen3_omni_thinker_only_smoke.yaml b/tests/special_e2e/qwen3_omni_thinker_only_smoke.yaml deleted file mode 100644 index bc8b319bd..000000000 --- a/tests/special_e2e/qwen3_omni_thinker_only_smoke.yaml +++ /dev/null @@ -1,57 +0,0 @@ -# 2-GPU Thinker-only stage config for the Qwen3-Omni GSPO smoke test. -# -# Uses 2 GPUs (tensor_parallel_size 2, devices "0,1") so FSDP shards the actor -# (FULL_SHARD) instead of falling into the single-GPU NO_SHARD path, which does -# not support the offload_to_cpu summon used during LoRA weight sync. -# vLLM-Omni strips the top-level CLI engine args when a stage config is provided, -# so memory/batching live here. -stage_args: - - stage_id: 0 - runtime: - devices: "0,1" - engine_args: - # ── Stage identity ── - model_stage: thinker - model_arch: Qwen3OmniMoeThinkerForConditionalGeneration - worker_type: ar - scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler - engine_output_type: text - hf_config_name: thinker_config - - # ── Parallelism & backend ── - tensor_parallel_size: 2 - distributed_executor_backend: "mp" - - # ── Text-only smoke test: allow no multimodal inputs so vLLM skips the - # dummy image/video/audio profiling forward (the tiny vision tower is - # not dimensionally exercised; text generation never touches it). ── - limit_mm_per_prompt: - image: 0 - video: 0 - audio: 0 - - # ── Memory & batching (top-level CLI args are stripped) ── - gpu_memory_utilization: 0.4 - max_num_seqs: 16 - max_num_batched_tokens: 8192 - enable_chunked_prefill: true - - # ── Model loading ── - dtype: bfloat16 - load_format: safetensors - trust_remote_code: true - enable_prefix_caching: false - enforce_eager: true - - # ── LoRA (required for adapter hot-loading) ── - enable_lora: true - max_lora_rank: 64 - max_loras: 1 - - # ── verl integration ── - enable_sleep_mode: true - logprobs_mode: processed_logprobs - disable_log_stats: true - final_output: true - final_output_type: text - is_comprehension: true diff --git a/tests/special_e2e/run_gspo_qwen3_omni_thinker_lora_smoke.sh b/tests/special_e2e/run_gspo_qwen3_omni_thinker_lora_smoke.sh deleted file mode 100755 index bb9eca9a4..000000000 --- a/tests/special_e2e/run_gspo_qwen3_omni_thinker_lora_smoke.sh +++ /dev/null @@ -1,144 +0,0 @@ -#!/usr/bin/env bash -# DEPRECATED: This e2e smoke test uses the legacy verl.trainer.main_ppo -# entrypoint with external_lib monkey-patches, and is deprecated. It will be -# removed in v0.3.0. Please use the V1 equivalent instead: -# bash tests/special_e2e/run_gspo_qwen3_omni_thinker_lora_v1_smoke.sh -# -# Qwen3-Omni Thinker GSPO + LoRA e2e smoke test (minimal runtime). -# -# Builds a tiny random-weight Qwen3-Omni model, then runs a couple of training -# steps end-to-end to verify the full path our PR adds wires up correctly: -# parquet load -> vLLM-Omni AR rollout (output_mode=ar) -> GSPO advantage/loss -# -> FSDP LoRA actor back-prop -> LoRA weight sync -> validation. -# -# This is a smoke test (random weights, 2 steps): it checks the pipeline runs -# without errors, NOT model quality. -# -# Requires: verl, verl-omni, vllm-omni installed. -# * dummy model built at MODEL_PATH (auto-built by this script if missing) -# * a small math parquet dataset at DATA_DIR/{train,test}.parquet -# (same default location as examples/gspo_trainer/qwen3_omni/run_qwen3_omni_thinker_gspo_lora.sh) -# -# Override via env: NUM_GPUS, MODEL_PATH, DATA_DIR, TOTAL_TRAIN_STEPS -set -xeuo pipefail - -# NCCL / accelerator env guards (mirror the example recipe; without these the -# NCCL net plugin can segfault at init on some single-node setups). -export NCCL_IB_DISABLE=1 -export CPATH=/usr/include${CPATH:+:$CPATH} -export RAY_ACCEL_ENV_VAR_OVERRIDE_ON_ZERO=0 - -# Load verl_omni on the driver (rollout adapter) + the Qwen3-Omni patches (processor / automodel); workers also load the model patch via external_lib below. -export VERL_USE_EXTERNAL_MODULES=verl_omni,verl_omni.models.transformers.qwen3_omni_thinker - -# Force exact versions: image ships older TransferQueue/accelerate and `.[gpu]`'s `>=` won't upgrade them (tf5 meta-init needs accelerate>=1.14). -pip install --no-cache-dir TransferQueue==0.1.8 accelerate==1.14.0 -python3 -c "import transformers, accelerate; print('smoke deps: transformers', transformers.__version__, '| accelerate', accelerate.__version__)" - -NUM_GPUS=${NUM_GPUS:-2} -# Tiny model: prefer the community-hosted Hub checkpoint; build one locally if it -# is not available yet (not uploaded / offline CI). Override with MODEL_PATH. -MODEL_REPO=${MODEL_REPO:-ShowMaker27/Qwen3-Omni-tiny-random} -MODEL_PATH=${MODEL_PATH:-} -DATA_DIR=${DATA_DIR:-${HOME}/data/math} -TOTAL_TRAIN_STEPS=${TOTAL_TRAIN_STEPS:-2} - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -# 2-GPU AR stage config (output_mode=ar). FSDP needs >1 GPU to shard (FULL_SHARD); -# on a single GPU it falls into NO_SHARD, which can't run the offload_to_cpu summon -# used during LoRA weight sync. TP is fixed in the stage YAML (vLLM-Omni strips -# top-level CLI engine args when a stage config is set). -STAGE_CONFIG="${REPO_ROOT}/tests/special_e2e/qwen3_omni_thinker_only_smoke.yaml" - -# Same Thinker-only module filter as the example recipe. -EXCLUDE_MODULES=".*talker.*|.*code2wav.*|.*code_predictor.*|.*visual.*|.*audio_tower.*" - -# ── Resolve the tiny model: Hub checkpoint if present, else build locally ────── -if [ -z "${MODEL_PATH}" ]; then - if python3 -c "from huggingface_hub import snapshot_download; snapshot_download('${MODEL_REPO}')" 2>/dev/null; then - MODEL_PATH="${MODEL_REPO}" - else - MODEL_PATH="${HOME}/models/tiny-random/Qwen3-Omni" - python3 "${REPO_ROOT}/tests/special_e2e/build_qwen3_omni_tiny_random.py" \ - --output-dir "${MODEL_PATH}" --force - fi -fi - -# ── Build dummy math dataset if not present ─────────────────────────────────── -if [ ! -f "${DATA_DIR}/train.parquet" ]; then - python3 "${REPO_ROOT}/tests/special_e2e/create_dummy_math_data.py" \ - --local_save_dir "${DATA_DIR}" -fi - -# ── Run training (tiny: 2 steps, LoRA, GSPO, vLLM-Omni AR rollout) ──────────── -python3 -m verl.trainer.main_ppo \ - data.train_files="${DATA_DIR}/train.parquet" \ - data.val_files="${DATA_DIR}/test.parquet" \ - data.train_batch_size=4 \ - data.max_prompt_length=256 \ - data.max_response_length=512 \ - data.val_max_samples=4 \ - data.truncation='left' \ - \ - actor_rollout_ref.model.path="${MODEL_PATH}" \ - actor_rollout_ref.model.external_lib=verl_omni.models.transformers.qwen3_omni_thinker \ - +actor_rollout_ref.model.override_config.attn_implementation=sdpa \ - actor_rollout_ref.model.lora_rank=8 \ - actor_rollout_ref.model.lora_alpha=16 \ - 'actor_rollout_ref.model.target_modules="q_proj,k_proj,v_proj,o_proj,gate_proj,up_proj,down_proj"' \ - actor_rollout_ref.model.exclude_modules="${EXCLUDE_MODULES}" \ - actor_rollout_ref.model.use_remove_padding=True \ - ++actor_rollout_ref.actor.freeze_vision_tower=True \ - \ - actor_rollout_ref.actor.optim.lr=1e-6 \ - actor_rollout_ref.actor.ppo_mini_batch_size=4 \ - actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=1 \ - actor_rollout_ref.actor.use_kl_loss=True \ - actor_rollout_ref.actor.kl_loss_coef=0.001 \ - actor_rollout_ref.actor.strategy=fsdp \ - actor_rollout_ref.actor.fsdp_config.param_offload=True \ - actor_rollout_ref.actor.fsdp_config.optimizer_offload=True \ - actor_rollout_ref.actor.fsdp_config.model_dtype=bf16 \ - actor_rollout_ref.actor.fsdp_config.use_orig_params=True \ - actor_rollout_ref.actor.fsdp_config.wrap_policy.min_num_params=100000000 \ - actor_rollout_ref.actor.policy_loss.loss_mode=gspo \ - actor_rollout_ref.actor.clip_ratio_low=3e-4 \ - actor_rollout_ref.actor.clip_ratio_high=4e-4 \ - actor_rollout_ref.actor.loss_agg_mode=seq-mean-token-mean \ - \ - actor_rollout_ref.rollout.name=vllm_omni \ - actor_rollout_ref.rollout.mode=async \ - actor_rollout_ref.rollout.n=2 \ - actor_rollout_ref.rollout.temperature=0.8 \ - actor_rollout_ref.rollout.tensor_model_parallel_size="${NUM_GPUS}" \ - actor_rollout_ref.rollout.gpu_memory_utilization=0.4 \ - actor_rollout_ref.rollout.max_num_seqs=16 \ - actor_rollout_ref.rollout.calculate_log_probs=True \ - actor_rollout_ref.rollout.load_format=safetensors \ - actor_rollout_ref.rollout.layered_summon=True \ - actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=2 \ - ++actor_rollout_ref.rollout.engine_kwargs.vllm_omni.stage_configs_path="${STAGE_CONFIG}" \ - ++actor_rollout_ref.rollout.engine_kwargs.vllm_omni.output_mode=ar \ - \ - actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=2 \ - actor_rollout_ref.ref.strategy=fsdp \ - actor_rollout_ref.ref.fsdp_config.param_offload=True \ - \ - algorithm.adv_estimator=grpo \ - algorithm.use_kl_in_reward=False \ - \ - reward.reward_manager.name=dapo \ - \ - trainer.logger=console \ - trainer.project_name=verl-test \ - trainer.experiment_name=gspo-qwen3-omni-thinker-lora-e2e \ - trainer.n_gpus_per_node="${NUM_GPUS}" \ - trainer.nnodes=1 \ - trainer.val_before_train=False \ - trainer.test_freq=1 \ - trainer.save_freq=-1 \ - trainer.resume_mode=disable \ - trainer.total_training_steps="${TOTAL_TRAIN_STEPS}" \ - "$@" - -echo "Qwen3-Omni Thinker GSPO+LoRA e2e smoke test passed (training completed successfully)." diff --git a/verl_omni/models/transformers/__init__.py b/verl_omni/models/transformers/__init__.py deleted file mode 100644 index 878236e2f..000000000 --- a/verl_omni/models/transformers/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright 2026 Bytedance Ltd. and/or its affiliates -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# DEPRECATED: This package will be removed in v0.3.0. -# The legacy Qwen3-Omni monkey-patches are no longer needed with the V1 trainer -# (verl_omni.trainer.main_omni). Please see run_qwen3_omni_thinker_gspo_lora_v1.sh -# for the V1 migration path. diff --git a/verl_omni/models/transformers/qwen3_omni_thinker.py b/verl_omni/models/transformers/qwen3_omni_thinker.py deleted file mode 100644 index 8a9860755..000000000 --- a/verl_omni/models/transformers/qwen3_omni_thinker.py +++ /dev/null @@ -1,425 +0,0 @@ -# Copyright 2026 Bytedance Ltd. and/or its affiliates -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Qwen3-Omni Thinker patches: route via AutoModelForMultimodalLM, fix FSDP-init -blockers, unfuse MoE experts for PEFT LoRA (tf5+), and extend verl's hf_processor -for Qwen3-Omni. - -DEPRECATED: This module will be removed in v0.3.0. The V1 trainer -(verl_omni.trainer.main_omni) handles Qwen3-Omni natively — no monkey-patches -needed. Please migrate to V1 scripts (e.g., run_qwen3_omni_thinker_gspo_lora_v1.sh). -""" - -import logging -import warnings - -warnings.warn( - "The module 'verl_omni.models.transformers.qwen3_omni_thinker' is deprecated " - "and will be removed in v0.3.0. The V1 trainer (verl_omni.trainer.main_omni) " - "handles Qwen3-Omni natively — no monkey-patches needed. " - "Please migrate to V1 scripts (e.g., run_qwen3_omni_thinker_gspo_lora_v1.sh).", - FutureWarning, - stacklevel=2, -) -logger = logging.getLogger(__name__) - - -def _register_qwen3_omni_automodel() -> None: - """Point Omni at AutoModelForMultimodalLM and patch FSDP-init blockers.""" - try: - from transformers import AutoModelForMultimodalLM - from transformers.models.qwen3_omni_moe import ( - Qwen3OmniMoeConfig, - Qwen3OmniMoeForConditionalGeneration, - ) - except ImportError: - return - - from verl.utils.model import _architecture_to_auto_class - - # Official HF mapping for qwen3_omni_moe (not AutoModelForCausalLM). - _architecture_to_auto_class["Qwen3OmniMoeForConditionalGeneration"] = AutoModelForMultimodalLM - - def _qwen3_omni_get_input_embeddings(self): - return self.thinker.get_input_embeddings() - - def _qwen3_omni_set_input_embeddings(self, value): - self.thinker.set_input_embeddings(value) - - def _qwen3_omni_forward( - self, - input_ids=None, - attention_mask=None, - position_ids=None, - past_key_values=None, - inputs_embeds=None, - labels=None, - use_cache=None, - output_attentions=None, - output_hidden_states=None, - return_dict=None, - **kwargs, - ): - return self.thinker( - input_ids=input_ids, - attention_mask=attention_mask, - position_ids=position_ids, - past_key_values=past_key_values, - inputs_embeds=inputs_embeds, - labels=labels, - use_cache=use_cache, - output_attentions=output_attentions, - output_hidden_states=output_hidden_states, - return_dict=return_dict, - **kwargs, - ) - - Qwen3OmniMoeForConditionalGeneration.forward = _qwen3_omni_forward - Qwen3OmniMoeForConditionalGeneration.get_input_embeddings = _qwen3_omni_get_input_embeddings - Qwen3OmniMoeForConditionalGeneration.set_input_embeddings = _qwen3_omni_set_input_embeddings - # Upstream lists Qwen3OmniMoeDecoderLayer which does not exist; fix to the real class. - Qwen3OmniMoeForConditionalGeneration._no_split_modules = [ - "Qwen3OmniMoeThinkerTextDecoderLayer", - "Qwen3OmniMoeVisionBlock", - ] - # _verl_strip_modules: verl's FSDPEngine drops these sub-modules for Thinker-only training. - Qwen3OmniMoeForConditionalGeneration._verl_strip_modules = [ - "talker", - "code2wav", - "code_predictor", - ] - - # tie_word_embeddings=True disables FSDP meta-tensor init and OOMs on 30B-A3B. - logger.warning( - "verl_omni: forcing tie_word_embeddings=False on Qwen3OmniMoeConfig — tied " - "embeddings disable the FSDP meta-tensor init path and OOM on 30B-A3B." - ) - - class _FalseTieDescriptor: - def __get__(self, obj, objtype=None): - return False - - def __set__(self, obj, value): - pass - - Qwen3OmniMoeConfig.tie_word_embeddings = _FalseTieDescriptor() - - -def patch_hf_processor_for_qwen3_omni() -> None: - """Wrap verl.utils.tokenizer.hf_processor to recognize Qwen3OmniMoeProcessor. - Installs a fallback that handles Qwen3-Omni only when the original returns None.""" - try: - from transformers.models.qwen3_omni_moe import Qwen3OmniMoeThinkerForConditionalGeneration - except ImportError: - return - - import types - - import verl.utils.tokenizer as _vt - - _original_hf_processor = _vt.hf_processor - - def _patched_hf_processor(name_or_path, **kwargs): - result = _original_hf_processor(name_or_path, **kwargs) - if result is not None: - return result - - import numpy as np - from transformers import AutoConfig, AutoProcessor, PreTrainedTokenizerBase - - processor = AutoProcessor.from_pretrained(name_or_path, **kwargs) - if isinstance(processor, PreTrainedTokenizerBase): - return None - if processor.__class__.__name__ != "Qwen3OmniMoeProcessor": - return None - - config = AutoConfig.from_pretrained(name_or_path, **kwargs) - # Token IDs / spatial_merge_size live on thinker_config, not the top-level config. - processor.config = config.thinker_config - processor.spatial_merge_size = config.thinker_config.vision_config.spatial_merge_size - processor.config.vision_start_token_id = config.talker_config.vision_start_token_id - model_class = Qwen3OmniMoeThinkerForConditionalGeneration - - # cast to int64 to avoid BF16 large-int rounding in fsdp. - _ori_get_rope_index = types.MethodType(model_class.get_rope_index, processor) - - def _get_rope_index_long(*args, **kwargs): - vision_position_ids, deltas = _ori_get_rope_index(*args, **kwargs) - return vision_position_ids.long(), deltas - - processor.get_rope_index = _get_rope_index_long - processor.get_llm_pos_ids_for_vision = types.MethodType(model_class.get_llm_pos_ids_for_vision, processor) - - def _dedup_pad_tokens(self, prompt_ids: list[int]) -> list[int]: - """Collapse consecutive multimodal pad tokens to one.""" - tokenizer = getattr(self, "tokenizer", None) - if tokenizer is None: - return prompt_ids - - pad_ids: set[int] = set() - for tok_attr in ("image_token", "video_token", "audio_token"): - tok = getattr(self, tok_attr, None) - if tok is None: - continue - try: - tid = tokenizer.convert_tokens_to_ids(tok) - except (TypeError, ValueError): - continue - if tid is None or tid == getattr(tokenizer, "unk_token_id", None): - continue - pad_ids.add(int(tid)) - if not pad_ids: - return prompt_ids - - arr = np.asarray(prompt_ids, dtype=np.int64) - if arr.size == 0: - return prompt_ids - is_pad = np.isin(arr, list(pad_ids)) - keep = np.ones(arr.size, dtype=bool) - same_as_prev = is_pad[1:] & is_pad[:-1] & (arr[1:] == arr[:-1]) - keep[1:] &= ~same_as_prev - return arr[keep].tolist() - - processor.dedup_pad_tokens = types.MethodType(_dedup_pad_tokens, processor) - return processor - - _vt.hf_processor = _patched_hf_processor - # Also refresh verl.utils's stale re-export (callers use `from verl.utils import hf_processor`). - import sys as _sys - - for _mod_name in ("verl.utils", "verl.workers.config.model"): - _mod = _sys.modules.get(_mod_name) - if _mod is not None and hasattr(_mod, "hf_processor"): - _mod.hf_processor = _patched_hf_processor - - -_EXPERTS_UNFUSE_APPLIED = False - - -def _patch_unfuse_qwen3_omni_thinker_experts() -> None: - """Hook peft.get_peft_model to unfuse tf5 fused MoE experts before LoRA (tf5+ only). - Converts Qwen3OmniMoeThinkerTextExperts (fused 3D params) to per-expert nn.Linear.""" - global _EXPERTS_UNFUSE_APPLIED - if _EXPERTS_UNFUSE_APPLIED: - return - - # tf5 sentinel: transformers.integrations.moe only exists in transformers >= 5.x - try: - import transformers.integrations.moe # noqa - import peft as _peft - except ImportError: - return - - import torch - import torch.nn as nn - import torch.nn.functional as F - - class _Expert(nn.Module): - def __init__(self, hidden: int, intermediate: int) -> None: - super().__init__() - self.gate_proj = nn.Linear(hidden, intermediate, bias=False) - self.up_proj = nn.Linear(hidden, intermediate, bias=False) - self.down_proj = nn.Linear(intermediate, hidden, bias=False) - - class _Qwen3OmniMoeThinkerTextExpertsUnfused(nn.Module): - """Per-expert nn.Linear replacement for the tf5 fused Qwen3OmniMoeThinkerTextExperts. - Weights are cloned from fused params at conversion time so the original can be GC'd.""" - - def __init__(self, n: int, hidden: int, intermediate: int, act_fn) -> None: - super().__init__() - self.num_experts = n - self.act_fn = act_fn - self.experts = nn.ModuleList([_Expert(hidden, intermediate) for _ in range(n)]) - - def forward( - self, - hidden_states: torch.Tensor, - top_k_index: torch.Tensor, - top_k_weights: torch.Tensor, - ) -> torch.Tensor: - final = torch.zeros_like(hidden_states) - with torch.no_grad(): - mask = F.one_hot(top_k_index, self.num_experts).permute(2, 1, 0) - hits = mask.sum(dim=(-1, -2)).gt(0).nonzero() - for row in hits: - i = row[0].item() - if i >= self.num_experts: - continue - top_k_pos, tok_idx = torch.where(mask[i]) - x = hidden_states[tok_idx] - e = self.experts[i] - out = e.down_proj(self.act_fn(e.gate_proj(x)) * e.up_proj(x)) - out = out * top_k_weights[tok_idx, top_k_pos, None] - final.index_add_(0, tok_idx, out.to(final.dtype)) - return final - - def _convert_model_experts(model) -> None: - """Replace all fused Thinker expert modules with unfused per-expert nn.Linear.""" - for path, module in list(model.named_modules()): - if type(module).__name__ != "Qwen3OmniMoeThinkerTextExperts": - continue - gate_up = module.gate_up_proj.data # (n, 2*intermediate, hidden) - down = module.down_proj.data # (n, hidden, intermediate) - n = gate_up.shape[0] - di = gate_up.shape[1] // 2 - h = gate_up.shape[2] - - new_mod = _Qwen3OmniMoeThinkerTextExpertsUnfused(n, h, di, module.act_fn) - for i, e in enumerate(new_mod.experts): - e.gate_proj.weight = nn.Parameter(gate_up[i, :di, :].clone()) - e.up_proj.weight = nn.Parameter(gate_up[i, di:, :].clone()) - e.down_proj.weight = nn.Parameter(down[i].clone()) - - parent_path, _, child_name = path.rpartition(".") - parent = model.get_submodule(parent_path) if parent_path else model - setattr(parent, child_name, new_mod) - - _orig_get_peft_model = _peft.get_peft_model - - # No-op PEFT's gate_proj/up_proj → gate_up_proj remap for Qwen3-Omni, else expert LoRA won't attach. - try: - import peft.utils.transformers_weight_conversion as _twc - - _orig_get_mapping = _twc.get_model_conversion_mapping - _orig_convert = _twc.convert_peft_config_for_transformers - - def _patched_get_mapping(model): - if type(model).__name__ == "Qwen3OmniMoeForConditionalGeneration": - return [] - return _orig_get_mapping(model) - - def _patched_convert(peft_config, model=None, conversions=None): - if model is not None and type(model).__name__ == "Qwen3OmniMoeForConditionalGeneration": - return - return _orig_convert(peft_config, model=model, conversions=conversions) - - _twc.get_model_conversion_mapping = _patched_get_mapping - _twc.convert_peft_config_for_transformers = _patched_convert - except (ImportError, AttributeError) as e: - logger.warning("verl_omni: could not patch PEFT tf5 name remapping (%s); MoE expert LoRA may not attach", e) - - def _patched_get_peft_model(model, peft_config, **kwargs): - if type(model).__name__ == "Qwen3OmniMoeForConditionalGeneration": - _convert_model_experts(model) - # verl passes target_modules as a comma-separated string; PEFT treats it as regex — split to set. - if isinstance(peft_config.target_modules, str) and "," in peft_config.target_modules: - peft_config.target_modules = set(peft_config.target_modules.split(",")) - return _orig_get_peft_model(model, peft_config, **kwargs) - - _peft.get_peft_model = _patched_get_peft_model - # Also update verl's module-level binding if it was already imported before us. - import sys as _sys - - _vi = _sys.modules.get("verl.workers.engine.fsdp.transformer_impl") - if _vi is not None: - _vi.get_peft_model = _patched_get_peft_model - _EXPERTS_UNFUSE_APPLIED = True - logger.info("verl_omni: installed get_peft_model hook for Qwen3-Omni MoE expert unfusing (tf5+)") - - -def patch_hf_tokenizer_for_qwen3_omni() -> None: - """Wrap ``verl.utils.tokenizer.hf_tokenizer`` to auto-load chat_template from chat_template.json. - - Some models (e.g., Qwen3-Omni) store chat_template in a separate file - instead of tokenizer_config.json. This patch ensures the tokenizer - has a valid chat_template before returning it. - """ - import functools - import json - import os - - try: - import verl.utils.tokenizer as _vt - except ImportError: - return - - _original_hf_tokenizer = _vt.hf_tokenizer - - @functools.wraps(_original_hf_tokenizer) - def _patched_hf_tokenizer(name_or_path, *args, **kwargs): - tokenizer = _original_hf_tokenizer(name_or_path, *args, **kwargs) - - if getattr(tokenizer, "chat_template", None) is None and isinstance(name_or_path, str): - chat_template_path = os.path.join(name_or_path, "chat_template.json") - if os.path.exists(chat_template_path): - try: - with open(chat_template_path) as f: - data = json.load(f) - chat_template = data.get("chat_template") - if chat_template: - tokenizer.chat_template = chat_template - except (OSError, json.JSONDecodeError): - pass - - return tokenizer - - _vt.hf_tokenizer = _patched_hf_tokenizer - - # Patch sys.modules entries that already imported hf_tokenizer - import sys - - for mod_name in list(sys.modules.keys()): - if not mod_name.startswith("verl"): - continue - mod = sys.modules.get(mod_name) - if ( - mod is not None - and hasattr(mod, "hf_tokenizer") - and mod.__dict__.get("hf_tokenizer") is _original_hf_tokenizer - ): - mod.hf_tokenizer = _patched_hf_tokenizer - - -def patch_register_vllm_moe_model_weight_loader() -> None: - """Register Qwen3-Omni Thinker with verl's ``SUPPORTED_MOE_MODELS`` whitelist. - - verl's ``patch_vllm_moe_model_weight_loader`` re-attaches ``weight_loader`` - on FusedMoE ``w13_weight``/``w2_weight`` before IPC weight sync (the attr - that vllm-ascend's ``process_weights_after_loading`` drops when rebuilding - the params). But it early-returns unless the model class is in - ``SUPPORTED_MOE_MODELS``. Qwen3-Omni Thinker isn't there by default, so we - append it. - """ - import importlib - - try: - from verl.utils.vllm import patch as _vp - except ImportError: - return - - for mod_path, cls_name in [ - ( - "vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker", - "Qwen3OmniMoeThinkerForConditionalGeneration", - ), - ]: - try: - mod = importlib.import_module(mod_path) - cls = getattr(mod, cls_name, None) - if cls is not None and cls not in _vp.SUPPORTED_MOE_MODELS: - _vp.SUPPORTED_MOE_MODELS.append(cls) - except ImportError: - continue - - -def apply_qwen3_omni_thinker_patches() -> None: - """Apply all Qwen3-Omni Thinker patches (idempotent registrations).""" - _register_qwen3_omni_automodel() - patch_hf_processor_for_qwen3_omni() - _patch_unfuse_qwen3_omni_thinker_experts() - patch_hf_tokenizer_for_qwen3_omni() - patch_register_vllm_moe_model_weight_loader() - - -# Apply on import so this module works as a verl ``external_lib`` target. -apply_qwen3_omni_thinker_patches()