Add multimodule for multimodal models - #1258
Open
wenqingqian wants to merge 19 commits into
Open
Conversation
…zer state loading
Freezing the vision tower under MIMO crashed at four points: 1. Training: the scheduler's delayed ViT backward called torch.autograd.backward on vision outputs carrying no autograd graph (frozen params -> forward builds no graph). Fix: forward the frozen ViT under torch.no_grad and stop marking exchanged tensors requires_grad (exchange_macro_outputs gains mark_requires_grad), so no grad hooks are registered, exhausted macro batches are dropped silently, and the delayed backward never triggers. _vision_backward_fn also early-returns defensively when no graph exists. 2. save/load_parameter_state: an all-frozen module gets Megatron's official stub optimizer (DistributedOptimizer(optimizer=None), is_stub_optimizer=True, attributes such as data_parallel_group uninitialized). ChainedOptimizer's aggregate is_stub_optimizer is all(...), so the mixed vision-stub + language-non-stub chain passed outer guards and crashed inside the loop. Skip stubs per member. 3. state_dict/load_state_dict: Megatron's ChainedOptimizer.state_dict has no stub guard and crashes on stub.state_dict() (None inner optimizer). MIMO ChainedOptimizer.state_dict now stores a None placeholder for stub members and load_state_dict skips them symmetrically. Non-frozen behavior is unchanged: default arguments and the frozen=False paths are code-level equivalent to before. Verified on 8xA100 (Qwen3.5-4B MIMO): freeze-on 20-iter run with complete exit checkpoint, freeze-off regression with bitwise-identical iter-1 loss, and resume from a freeze-on checkpoint (iter 20 -> 40). Known limitations (non-blocking, mirror upstream gaps): stub handling in sharded_state_dict / load_state_dict_from_file (MIMO + non-legacy ckpt formats were already unsupported); fp16 loss-scale delegation when the first member is a stub; cross-freeze-config resume reuses slots silently.
After training completed at high steady-state GPU memory (e.g. ~38.6/39.5 GiB at seq8192), the exit checkpoint save OOMed allocating multi-GiB GPU buffers. Two causes, three fixes: 1. The optimizer parameter-state gather ran on GPU: ChainedOptimizer hardcoded get_parameter_state_dp_zero(use_gloo_comm=False), so the gather recv/send buffers (several GiB fp32) allocated on GPU. The gather now defaults to gloo/CPU (Megatron's own default), with --no-mimo-save-gather-use-gloo to restore the NCCL/GPU path; falls back to NCCL with a warning when gloo process groups are disabled. 2. The MIMO scheduler pinned the last macro batch forever: _serving was never cleared, keeping batch tensors, ViT outputs, gradients and the ctx graph alive through the save (~0.3-2+ GiB). _collect_grad now clears _serving when its macro completes; un-backwarded macros drop ctx; drop_completed_macros() (shared by advance() and the periodic pre-save cleanup) drops exhausted hook-free macros; and the exit save now calls release_training_state() + empty_cache() beforehand. Also rejects --rampup-batch-size under MIMO at validate time (it breaks num_microbatches % vit_batch_factor == 0 during training). Verified on 8xA100 (Qwen3.5-4B MIMO, gbs=64, dense seq): seq8192 60-iter exit-save OOM -> PASS with training loss bitwise identical to pre-fix; seq2048/4096 regressions within ~2% iteration time and bitwise loss; gloo-saved checkpoint resumes cleanly (2048 5->10 iters). Multi-reviewer code review approved (1 required implemented: gloo-group fallback).
Remove --no-mimo-save-gather-use-gloo and the save_gather_use_gloo plumbing (kwarg, NCCL fallback, build-time threading). The parameter state gather at checkpoint save now always uses gloo/CPU — Megatron's own default — so there is no way to move multi-GiB gather buffers back onto the GPU. Smoke-verified: 5-iter MIMO run, exit save OK.
wenqingqian
requested review from
aoyulong,
heavyrain-lzy and
zhaoyinglia
as code owners
August 3, 2026 06:05
Keep the training-loop edits to single-line calls in FlagScale Begin/End markers, matching the existing setup_mimo_ddp hook pattern: the model-list duck-typed dispatch moves to mimo_utils.release_mimo_training_state (exit save) and mimo_utils.drop_mimo_completed_macros (periodic save), exported through the package __init__. Also documents why the periodic-save cleanup is safe while training continues: only exhausted, hook-free macros are dropped and the next advance() re-establishes serving state. Verified with periodic saves mid-training (save_interval=5, 12 iters, 3 checkpoints written): non-freeze loss is bitwise identical to a no-periodic-save control; freeze loss delta (0.01-0.05%) is within the run-to-run self-noise of two identical control runs.
Both FlagScale hook blocks in training.py now run only when args.use_mimo is set, so the non-MIMO path is byte-identical to upstream (in particular the exit save no longer flushes the allocator for non-MIMO runs). Smoke-verified: 7-iter runs with save_interval=3 (2 periodic saves + exit save) pass for both use_mimo=true and false.
save/load_parameter_state previously unwrapped only chained_optimizers[0], silently dropping the expert fp32 master params of a MoE module optimizer (an FL ChainedOptimizer over dense/expert partitions). - _unwrap_distributed_optimizer -> _unwrap_distributed_optimizers: return ALL inner DistributedOptimizers; save/load now branch by inner count: 0 -> delegate to the original method, 1 -> keep the historical single-state file format (backward compatible with dense MIMO ckpts), >1 (MoE) -> FL-style list-of-states with per-inner DP-rank-0 filtering, gloo/CPU gather retained (iteration_0003 OOM fix), stub None placeholders aligned. Mirrors Megatron-LM-FL ChainedOptimizer.save/load_parameter_state. - load: isinstance guard with an actionable error for pre-fix MoE checkpoints that predate this fix. - validate_mimo_config: reject expert_tensor_parallel_size != language TP; the pg builder aliases the expert-TP group to the module TP group, so an explicit mismatch would otherwise be silently ignored. Verified (2026-08-04/05): MoE mimo save->resume roundtrip of all fp32 master params (dense + expert) max_abs_diff <= 2.03e-5 vs bug signature bf16 ulp ~5e-4; dense 4b mimo regression 20/20; code-review Approve (pipeline + independent re-review).
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.
Colocated MIMO Training for Multimodal Models
1. What's MIMO
MIMO is an existing Megatron concept for multimodal training: the model is treated as multiple modules (vision encoder + LLM) whose parallel configurations are decoupled — the ViT and the LLM no longer have to share one TP/DP/* grid.
Upstream Megatron implements two layouts: UNIFIED (both modules share one grid, i.e. traditional training) and NON_COLOCATED (modules on disjoint GPU ranks). The COLOCATED layout — same GPUs, different grids per module — is still
NotImplementedError, and no upstream layout batches ViT work across microbatches.2. This PR: colocated MIMO with microbatch-level scheduling
We implement the colocated mode: ViT and LLM run on the same GPUs with heterogeneous parallel configurations (e.g., on 8 GPUs: ViT TP=1/DP=8, LLM TP=2/DP=4), each module with its own DDP and distributed optimizer (
ChainedOptimizer).On top of the heterogeneous layout, a microbatch scheduler (
MIMOMicrobatchScheduler) batches vision work across microbatches: one ViT macro forward servesvit_batch_factorLLM microbatches, and ViT backward is delayed until the whole macro batch has produced gradients (detach + grad cache). The net effect: fewer, larger ViT forwards per iteration.3. Difference from Megatron's MIMO
vit_batch_factor+ scheduler4. How to Use It (Qwen3.5 example)
Enable MIMO in the training YAML:
With the layout above (world = 8, LLM TP=2 → LLM DP=4),
vit_batch_factor = (8 × 4) / (4 × 2) = 4: one ViT forward with 8 samples per rank feeds 4 LLM microbatches.5. Performance
Setup: Qwen3.5 4B (hybrid GDN + attention, MTP), 8 × A800, seq_len 2048, global batch 64, LLM TP=2 (DP=4), micro batch 2, 200 iterations.
vision_micro_batch_size=N→vit_batch_factor=Nfor N = 2 / 4 / 8 (vbf=8 needs gbs=64 so thatnum_microbatches=8; gbs=32 caps at vbf=4)Steady-state (iter 11–200) per-GPU TFLOPs, estimated from per-iteration time with identical iteration FLOPs across configs (same model, same global batch — the ratios are exact):
Equivalently, iteration time drops by 19.6% / 23.7% / 25.8%. Gains saturate as vbf grows (vbf=4→8 adds only ~2.1pp), since the ViT share of iteration time is already small.
Loss curves stay aligned with the baseline over the full 200 iterations (left two panels). Point-wise relative difference vs baseline (iter 3–200) across vbf=2/4/8: LM loss mean 0.76–0.96%, max ≤ 4.54%; MTP-1 loss mean 0.82–1.08%, max ≤ 4.30%. The drift does not grow with
vit_batch_factor(vbf=8 ≈ vbf=4). Residual differences come from per-module gradient clipping (vs one global clip in the baseline) and macro-batch scheduling order.6. Profile