Skip to content

feat(multimodal): enable Nemotron-H Omni VLM training - #3610

Open
biswapanda wants to merge 4 commits into
PrimeIntellect-ai:mainfrom
biswapanda:feat/nemotron-h-omni-training
Open

biswapanda wants to merge 4 commits into
PrimeIntellect-ai:mainfrom
biswapanda:feat/nemotron-h-omni-training

Conversation

@biswapanda

@biswapanda biswapanda commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Register Nemotron-H Omni with Prime-RL's multimodal batching.
  • Materialize and validate the image-processor tensors required by Nemotron 3.5 Super.
  • Preserve frozen vision weights after their first FSDP forward when packed ranks process different image counts.
  • Complete the three-PR stack for native Nemotron 3.5 Super image-conditioned RL training.

Why

Nemotron-H Omni image preprocessing produces tensors and image-token expansion metadata that Prime-RL must materialize and validate before building training batches. Registering its multimodal adapter lets batching materialize those tensors and validate their token alignment. Packed ranks can process different numbers of images; resharding the frozen vision encoder after every forward can therefore cause mismatched all-gather collectives. Retaining its gathered weights prevents those repeated collectives while preserving the existing behavior for other vision encoders.

Changes

  • Add NemotronHOmniAdapter for image tensor materialization.
  • Validate placeholder expansion lengths and reject mixed processed image shapes.
  • Add the model opt-in used to prevent rank-divergent repeated vision all-gathers.
  • Add focused multimodal adapter and FSDP behavior tests.

Validation

  • After moving architecture metadata into the model layer, the combined stack tree remains identical. Actual text-first and image-second acceptance on the updated heads is pending GPU access.

  • Ruff format: passed for all three reconstructed stack layers.

  • Ruff check: passed for all three reconstructed stack layers.

  • Python compileall: passed for all three reconstructed stack layers.

  • The pre-split equivalent passed 64 focused Nemotron model, conversion, multimodal batching, routed-forwarding, and composite-weight tests.

  • A three-step Nemotron 3.5 Super image RL run passed with Python dynamo.vllm, TP4 inference, four trainer GPUs, inline routed experts, sampling masks, and NCCL policies v0 through v3. Mismatch KL: 0.0021, 0.0006, 0.0030.

  • An eight-step follow-up passed with NCCL policies v0 through v8. Mismatch KL: 0.0023, 0.0022, 0.0022, 0.0095, 0.0005, 0.0107, 0.0035, 0.0030. All 32 rollouts were trainable with zero errors or cancellations.

  • Focused pytest collection for the reconstructed stack is blocked on this host because its local environment lacks flash_attn; GitHub CI is the clean test environment.

Stack

  1. feat(trainer): add Nemotron-H Omni checkpoint conversion #3730 — Nemotron-H Omni checkpoint conversion.
  2. feat(trainer): add Nemotron-H Omni composite model #3731 — Nemotron-H Omni composite trainer model.
  3. This PR — multimodal training integration.

This branch is based on commit b514c4894 from #3731. Because the branches originate from a fork, all three upstream PRs target main; upper diffs shrink as lower PRs merge.

vLLM compatibility

This PR does not include the Prime-RL runtime backport of vLLM #56231. Nemotron LoRA validation should use a vLLM release candidate or later release containing that upstream fix.

@biswapanda
biswapanda force-pushed the feat/nemotron-h-omni-training branch from 271148d to 4c21126 Compare September 30, 2026 05:00
@biswapanda
biswapanda force-pushed the feat/nemotron-h-omni-training branch from a156c67 to 33c5d7f Compare September 30, 2026 15:09
@biswapanda biswapanda changed the title feat(trainer): support Nemotron-H Omni VLM training feat(multimodal): enable Nemotron-H Omni VLM training Sep 30, 2026
@biswapanda
biswapanda force-pushed the feat/nemotron-h-omni-training branch from 33c5d7f to 308e357 Compare September 30, 2026 15:46
samsja added a commit that referenced this pull request Oct 10, 2026
Replaces the three switches `model.fsdp_cpu_offload`,
`model.optim_cpu_offload` and `model.full_offload` with one field:

```toml
[model]
offload = "optimizer"   # default; also "full", "none"
```

This applies to both the RL trainer and SFT. The `fsdp` switch is not
migrated: the mode is removed outright (rationale below). The
`[model.full_offload] numa_bind` knob is dropped: no recipe, deployment,
template or doc sets it. Full offload now always pins each rank's CPUs
to its GPU's NUMA node. That was already the default. Ranks whose GPU
reports no NUMA node still skip binding with a warning. On hosts without
GPU sysfs topology (`/sys/bus/pci/devices/<bus>/numa_node`), binding is
no longer optional: full offload fails there instead of being switched
off.

Why: the three switches could only be used one at a time, enforced by
`cpu_offload_mutual_exclusion`. Because `optim_cpu_offload` defaulted to
`true`, enabling full offload also meant writing `optim_cpu_offload =
false`, otherwise validation failed (see the old
`configs/advanced/deepseek-v4-flash/sft.toml` comment). With one value
the conflicting combinations cannot be expressed, so that validator is
deleted. The default behaviour does not change: optimizer-state offload
stays on.

Why drop `fsdp`: the mode was the stock torch FSDP2 `CPUOffloadPolicy`
with no in-tree config, example, or test user; Muon rejected it and it
broke NCCL weight broadcast. It was kept only for long-context runs
whose fp32 masters/moments no longer fit on GPU (the reason it was
restored in #3052); the `full` mode added in #3234 keeps FP32 masters,
moments, and gradients in host RAM, covering that memory pressure
instead. Gone with it: the `CPUOffloadPolicy` selection in `setup_fsdp`,
CPU-side model init in `setup_model`/`load_dcp_from_hf`,
`_move_buffers_to_cuda`, and the fsdp leg of the Gloo special case.

Changes:
- `ModelConfig.offload: Literal["none", "optimizer", "full"] =
"optimizer"` replaces the three fields.
`OptimizerInBackwardOffloadConfig` (and with it `numa_bind`) and its
bool-normalizing `BeforeValidator` are gone.
- Validators in `TrainerConfig` and `SFTConfig`:
- full offload accepts only AdamW or SignSGD and disables clipping, now
reading `offload == "full"`;
- the `Muon optimizer does not support FSDP CPU offload` validators are
deleted with the mode.
- Runtime:
- `setup_optimizer(..., offload=...)` replaces `cpu_offload` +
`full_offload_config`, and its "both enabled" check is gone.
- `FullCPUOffloadOptimizer` drops its unused `offload_config` argument.
- `setup_full_cpu_optimizer_offload()` takes no arguments and always
binds.
- `model.py`: FSDP always uses the default `OffloadPolicy()`; CPU-side
model init in `setup_model`/`load_dcp_from_hf` and
`_move_buffers_to_cuda` are gone.
- Tomls: `glm-4.5-air/swe-budget.toml` and
`ci/integration/reverse-text-sft/full-offload-resume.toml` → `offload =
"full"`; `glm-5.3/sft/h200/16k-12-node.toml` → `offload = "none"`;
`glm-5.3/sft/h200/base.toml` → `offload = "optimizer"`. Also updated
`docs/scaling.md`, the `deepseek-v4-flash/sft.toml` comment, and
`skills/training/start-run/SKILL.md`, which documented the removed
fields.

Breaking: the old keys now fail with `extra_forbidden`, and `offload =
"fsdp"` fails the `Literal`. There is no shim.

Interaction with #3849 (one trainer-common config base): #3849 moves the
full-offload and Muon/FSDP validators from `TrainerConfig`/`SFTConfig`
onto `BaseTrainerConfig`. This PR edits the full-offload validator's
condition and deletes the Muon/FSDP one, so the conflict is textual
only: keep #3849's single copy of the full-offload validators with
`self.model.offload == "full"` and drop its Muon/FSDP copy. Whichever
lands second takes a small rebase.

Other open PRs that set or read the old keys need a rebase:
- tomls: #3883, #3879, #3766, #3765, #3729, #3629, #3627, #3616, #3544
- code: #3885 (`optim_cpu_offload`), #3739 (`full_offload` validators),
#3724, #3719, #3699, #3610, #3487 (`fsdp_cpu_offload` / `enable_gloo`)

Test plan
- `pytest tests/unit/orchestrator tests/unit/test_configs.py
tests/unit/train/test_cpu_adam.py tests/unit/train/test_state_offload.py
-m "not gpu"`: 336 passed, 1 skipped (Qwen3-VL snapshot not cached on
this host), 4 deselected (GPU; this host has no GPU). The existing
offload config tests are updated to the new field. `test_load_configs`
covers the 4 edited tomls.
- `cli(TrainerConfig|SFTConfig, ...)`: `--model.offload full|none` and
the default (`optimizer`) resolve as expected; `optim_cpu_offload` and
`offload = "fsdp"` are rejected.
- `import prime_rl.trainer.rl.train, prime_rl.trainer.sft.train` works.
- `ruff check` and `ruff format --check` on the changed Python files:
clean.
- Not run: GPU or multi-node training with `offload = "full"`.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Breaking TOML/API surface touches distributed training memory paths
(FSDP offload, optimizer-in-backward, Gloo); behavior should match prior
modes when mapped correctly, but mis-migrated configs will fail at parse
time.
> 
> **Overview**
> **Breaking config change:** `model.fsdp_cpu_offload`,
`model.optim_cpu_offload`, and `model.full_offload` are replaced by a
single **`model.offload`** setting (`"optimizer"` default, or `"full"`,
`"fsdp"`, `"none"`). RL and SFT share the same field; example TOMLs and
scaling docs are updated accordingly.
> 
> The mutual-exclusion validator between the old flags is removed
because only one mode can be expressed. **`[model.full_offload]` /
`numa_bind` is dropped** — `"full"` always runs NUMA binding and CPU
optimizer thread setup via parameterless
`setup_full_cpu_optimizer_offload()`. Runtime paths (`setup_optimizer`,
FSDP CPU offload in `model.py`, Gloo when offload is `"full"` or
`"fsdp"`) key off the new enum; `FullCPUOffloadOptimizer` no longer
takes an offload config object.
> 
> Default training behavior is unchanged: optimizer-state CPU offload
remains the default (`"optimizer"`). Old TOML keys are not aliased and
should fail validation.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
35da73a. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: sami jaghouar <sami.jaghouar@gmail.com>

This branch has not been deployed

No deployments
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