Repository navigation
refactor(trainer)!: fold multimodal into trainer/vlm.py; one VLM registry; drop vlm attr fields - #3861
Conversation
Stacked on #3861. Deletes the Kimi K2.5 and Qwen3-VL image paths from `trainer/vlm.py`. `materialize_images(refs, processor)` now always uses the Qwen3.5 image processor (`pixel_values` + `image_grid_thw`, with the placeholder-length check). ## Why - Neither path can run. `get_model` rejects any `[model.vlm]` model whose `model_type` is not in `_CUSTOM_VLM_MAPPING` (`qwen3_5`, `qwen3_5_moe`, `qwen4_exp`). There is no Kimi K2.5 or Qwen3-VL trainer model. - RL also requires `supports_packed_multimodal_training`, which only the Qwen3.5 VLM sets. - A Kimi image path belongs with a future Kimi VLM model class. Re-adding it there is about 15 lines. ## Behavior None for trainable models. Qwen3.5 image batches go through the same processor call and the same length check as before. Kimi/Qwen3-VL `model_type`s used to fail in `get_model`, and they still do. ## Checks - `pytest -m "not gpu" tests/unit/orchestrator tests/unit/test_configs.py tests/unit/train tests/unit/transports`: all pass (440 + `test_cpu_adam.py` 3/3, run with `ninja` on `PATH`) - `ruff check` + `ruff format --check`: clean. - GPU: none needed beyond #3861. The Qwen3.5 code path is unchanged from #3861, whose SFT check covered it. ## Conflicts None beyond #3861's. This PR touches only `trainer/vlm.py`, `rl/train.py` (one call site), `tests/unit/train/test_multimodal.py` and `docs/development.md` (one sentence). <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Dead-code removal and API simplification; the active Qwen3.5 multimodal path is unchanged and Kimi/Qwen3-VL types were already rejected at model load. > > **Overview** > Removes **model-type dispatch** for RL image preprocessing: `materialize_images` now takes only `(refs, processor)` and always runs the **Qwen3.5-style** path (`pixel_values` + `image_grid_thw`, merge-size placeholder length check against vLLM). > > The **Kimi K2.5** branch (`grid_thws`, `preprocess` media dict) and the **`_IMAGE_INPUTS` map** for `qwen3_vl` / `qwen3_vl_moe` are deleted. The RL trainer call site drops `model.config.model_type`. Docs note that new VLM families must **extend** `materialize_images` if their processor outputs differ; unit tests drop the Kimi case and match the slimmer API. > > **No intended change** for currently trainable Qwen3.5 VLMs—the same processor call and length validation as the old Qwen entries. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit ba6ca9b. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
2091ecd to
a7293b2
Compare
4008f0e to
eaf5654
Compare
…stry - Move src/prime_rl/multimodal/, trainer/multimodal.py, utils/vlm.py and setup_processor into trainer/vlm.py. Delete ForwardPolicy, MaterializedMM, the adapter Protocol/registry and the Kimi K2.5 / Qwen3-VL image paths; materialize_images() returns the Qwen3.5 vision kwargs. - One VLM registry: _CUSTOM_VLM_MAPPING. Vision encoder, language model and weight-broadcast layer prefix come from the model layout (model.model.visual, model.model.language_model). - Qwen3.5 VLM builds MRoPE positions whenever image_grid_thw is given; the trainer always passes position_ids, and RL and SFT defer CP to the model for every batch with images. - Drop [model.vlm].vision_encoder_attr / language_model_attr and the unread orchestrator.model.vlm (breaking config change).
… too); one validate_vlm
6342813 to
6113c9a
Compare
# Conflicts: # tests/unit/orchestrator/test_batch.py
| fused_lm_head_token_chunk_size: int | Literal["disabled"] = 8192 | ||
| """Flattened token chunk size for the fused LM head. ``int >= 1`` sets the tokens per LM-head chunk explicitly; ``disabled`` uses the vanilla LM head. In SFT the fused head computes the summed cross-entropy and its gradients chunk by chunk, holding one chunk's full-vocab logits at a time.""" | ||
|
|
||
| @model_validator(mode="after") |
There was a problem hiding this comment.
Is there a reason we still validate it?
| return image.convert("RGB") | ||
|
|
||
|
|
||
| def materialize_images(refs: MMRefs, processor: Any) -> dict[str, torch.Tensor]: |
There was a problem hiding this comment.
Hm I don't really like it - it put's model specific image processing login inside the trainer, e.g. Nemoron-VL will have different logic than Qwen. Can we keep this in model module, and trainer calls the image processing function inside model?
| ) | ||
| if image_grid_thw is not None: | ||
| # Image inputs get MRoPE positions; text-only inputs keep the caller's positions. | ||
| if mm_token_type_ids is None: |
There was a problem hiding this comment.
Do we need this check?
| spatial_merge_size=self.config.vision_config.spatial_merge_size, | ||
| seq_lens=seq_lens, | ||
| ) | ||
| elif position_ids is None: |
There was a problem hiding this comment.
Shoudn't those always be passed rather than reconstructed if not present?
| # CP requires the sequence length to be divisible by cp_size. CatDataset | ||
| # pads every pack to a multiple of it; shard_for_cp raises on violations. | ||
| defer_vlm_cp_to_model = mm_kwargs is not None and "image_grid_thw" in mm_kwargs | ||
| # The VLM shards image inputs itself, after building their positions. |
There was a problem hiding this comment.
Maybe we should have some check to verify if custom cp handling is implemented, so it doesn't fail later with some rank hang?
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit fecaf4e. Configure here.
# Conflicts: # src/prime_rl/utils/vlm.py
# Conflicts: # tests/unit/train/models/test_glm5_next.py
# Conflicts: # src/prime_rl/trainer/model.py
…_vl.py Qwen3.8 Flash Next no longer imports the Qwen3.5 modeling module; both point materialize_images at materialize_qwen_vl_images. The family-agnostic data-URL decoding moves to trainer/vlm.py as load_image. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… vision.py Model packages stay self-contained, like the duplicated vision towers: Qwen3.5 and Qwen3.8 Flash Next each define materialize_qwen_vl_images next to their vision model, and the top-level models/qwen_vl.py goes away. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hubert-marek
left a comment
There was a problem hiding this comment.
LGTM assuming tested e2e
hubert-marek
left a comment
There was a problem hiding this comment.
LGTM assuming tested e2e

Folds the trainer's multimodal code into one module, keeps one VLM registry, and removes VLM config fields that nothing reads.
What changes
src/prime_rl/multimodal/(5 files),trainer/multimodal.py,utils/vlm.pyandsetup_processor(frommodel.py) becomesrc/prime_rl/trainer/vlm.py:get_vision_encoder,get_language_model,get_layer_prefix,load_image,setup_processor.materialize_images(refs, processor); the RL trainer callsmodel.materialize_images, so a new VLM family brings its own. Qwen3.5 and Qwen3.8 Flash Next each definematerialize_qwen_vl_imagesin their ownvision.py(the packages stay self-contained), which runs the Qwen-VL image processor and checks placeholder lengths against vLLM; the family-agnostic data-URL decoding isload_imageintrainer/vlm.py.ForwardPolicy,MaterializedMM, theMultimodalAdapterProtocol and its registry, and the Kimi K2.5 / Qwen3-VL image paths. No Kimi K2.5 or Qwen3-VL trainer model exists, soget_modelalready rejected them._CUSTOM_VLM_MAPPING(trainer/models/__init__.py) is the only one;VLM_REGISTRYandis_vlm_architectureare gone. The vision encoder, language model and NCCL layer prefix come from the model layout:model.model.visualandmodel.model.language_model, withmodel.modelfor text models (Muse Glimmer, Qwen3.8 Flash (qwen4_exp) and GLM-5.3 Flash (glm5_next) resolve the same way; they load no vision tower).image_grid_thwis given;forward()always passesposition_ids, used only for text-only rows. RL and SFT defer CP to the model for every batch with images.setup_context_parallelrefusescp > 1for a VLM withoutcp_context.[model.vlm]keeps onlyfreeze_vision_encoder;vision_encoder_attr/language_model_attrandorchestrator.model.vlmare removed (the orchestrator detects images from message content).vlmmoves to the trainerModelConfigand the shared[model.vlm]propagates only totrainer.model.vlm. The VLM bfloat16optimization_dtype/reduce_dtypevalidator is removed. Old keys fail with "Extra inputs are not permitted".Migration: delete
vision_encoder_attr/language_model_attrand any[orchestrator.model.vlm]block.Behavior
Training is unchanged for every trainable VLM (Qwen3.5 dense and MoE): same MRoPE positions, same CP shards, same component lookup. Configs with fp32 dtypes and
[model.vlm]are no longer rejected.Checks
pytest -m "not gpu" tests/unit/orchestrator tests/unit/test_configs.py tests/unit/train tests/unit/utilsafter merging main (1810d5b): 651 passed.ruff check/ruff format --check: clean. No dependency changes.forward(): embeddings, positions and logits reaching the decoder are bit-identical for an image batch at cp=1 and ulysses cp=2, and for a text-only batch.RL with images is not GPU-validated here: the nightly multimodal RL config stops on main and on this branch with the orchestrator error that #3949 fixes. #3949 and this PR share no files.
Note
Medium Risk
Breaking VLM config and a broad trainer refactor (FSDP, NCCL broadcast, CP, RL micro-batch prep), though trainable Qwen3.5 paths were validated for numerical parity.
Overview
Breaking config:
[model.vlm]now only hasfreeze_vision_encoder. Removevision_encoder_attr/language_model_attrand any[orchestrator.model.vlm]block. Shared[model.vlm]propagates to the trainer only. SFT no longer requiresoptimization_dtype/reduce_dtypebfloat16 for VLM runs.Architecture cleanup: Deletes
prime_rl/multimodal/,utils/vlm.py(VLM_REGISTRY, per-family attr overrides), andForwardPolicy/MultimodalAdapter(including Kimi paths). VLM helpers live intrainer/vlm.py(encoder/LM lookup,load_image,setup_processor). Vision and LM modules are resolved from a fixed layout:model.model.visualandmodel.model.language_model.RL image prep: Each custom VLM exposes
materialize_images(refs, processor)(Qwen3.5 / Qwen3.8 implementmaterialize_qwen_vl_imagesin theirvision.py). The RL data path callsmodel.materialize_imagesinstead of a global adapter registry.Forward / CP: Trainer
forward()always passesposition_ids; Qwen VLMs build MRoPE only whenimage_grid_thwis set. Context parallelism defers sharding to the model for any batch withmm_kwargs, andsetup_context_parallelerrors if a VLM lackscp_contextwithcp > 1.Docs and example TOMLs are updated to match.
Reviewed by Cursor Bugbot for commit bba4004. Bugbot is set up for automated code reviews on this repo. Configure here.