Skip to content

Add multimodule for multimodal models - #1258

Open
wenqingqian wants to merge 19 commits into
flagos-ai:mainfrom
wenqingqian:cmimo
Open

Add multimodule for multimodal models#1258
wenqingqian wants to merge 19 commits into
flagos-ai:mainfrom
wenqingqian:cmimo

Conversation

@wenqingqian

Copy link
Copy Markdown
Contributor

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 serves vit_batch_factor LLM 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

Capability Megatron UNIFIED Megatron NON_COLOCATED Ours (colocated MIMO)
ViT/LLM on the same ranks ✅ (shared grid) ❌ (disjoint ranks)
Heterogeneous TP/DP per module ✅ (but rank-disjoint) ✅ (e.g., ViT TP=1/DP=8, LLM TP=2/DP=4)
Microbatch-level ViT batching (one ViT forward feeds N LLM forwards) vit_batch_factor + scheduler
Delayed ViT backward ✅ (detach + grad cache, backward after macro batch completes)

4. How to Use It (Qwen3.5 example)

Enable MIMO in the training YAML:

experiment:
  task:
    entrypoint: ./flagscale/train/megatron/train_qwen35.py

model:
  use_mimo: true
  vision_tensor_model_parallel_size: 1   # ViT TP (currently must be 1)
  vision_micro_batch_size: 4             # ViT micro batch per forward
  micro_batch_size: 2                    # LLM micro batch
  global_batch_size: 32
  tensor_model_parallel_size: 2          # LLM TP

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.

  • baseline: ViT and LLM both TP=2/DP=4
  • MIMO vbf=N: ViT TP=1/DP=8, vision_micro_batch_size=Nvit_batch_factor=N for N = 2 / 4 / 8 (vbf=8 needs gbs=64 so that num_microbatches=8; gbs=32 caps at vbf=4)
performance_overview_2

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):

Config Iteration time per-GPU TFLOPs (est.) vs baseline
baseline 5001.26 ms 77.5
MIMO vbf=2 4022.61 ms 96.3 +24.3%
MIMO vbf=4 3817.43 ms 101.5 +31.0%
MIMO vbf=8 3712.44 ms 104.4 +34.7%

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

Screenshot 2026-07-20 at 18 41 15

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.
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).
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.

1 participant