Skip to content

Commit 766f710

Browse files
cuichenxkamran-nvidiaadityavavreNVDA
authored
perf(recipe): tune Nemotron Omni H100 fine-tuning (#5524)
Signed-off-by: Chen Cui <chcui@nvidia.com> Signed-off-by: adityavavreNVDA <avavre@nvidia.com> Co-authored-by: Kamran Jafari <kjafarisadeg@nvidia.com> Co-authored-by: adityavavreNVDA <avavre@nvidia.com>
1 parent 5bc1ca7 commit 766f710

14 files changed

Lines changed: 998 additions & 129 deletions

File tree

examples/model_verification_cards/nemotron-3-nano-omni-30b-a3b-reasoning/card.yaml

Lines changed: 91 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,17 @@ summary: >
66
Performance disclaimer: this model has not been performance-tuned; reported
77
timing and throughput metrics are sanity checks, not optimized performance
88
results. Verification uses the immutable public model and CORD v2 revisions.
9-
CPU and distributed GPU import, deterministic Megatron inference, bounded
10-
full-model SFT, and LoRA PEFT runs completed. Strict CPU and GPU round trips
11-
preserved all 7,349 tensors bitwise, but Transformers 5.8.0 cannot natively
12-
reload the local custom-code exports because its dynamic-module cache omits
13-
transitive configuration imports. The one-step HF/Megatron comparison
14-
predicts the same token but remains below the 0.99 cosine gate. The packed
15-
8K long-context recipe completes one optimizer step but does not complete the
16-
second because of H100 memory pressure. Unsupported and incomplete workflows
17-
remain explicitly identified rather than inferred from focused unit coverage.
9+
CPU and distributed GPU import, deterministic Megatron inference, 100-step
10+
one-node H100 full-model SFT, in-batch-packed 8K SFT, and LoRA PEFT runs
11+
completed. The H100 fine-tuning variants preserve real image-text samples and
12+
natural routing while using lower-precision optimizer state; 4K SFT and PEFT
13+
use HybridEP, while packed CP2 uses the standard all-to-all dispatcher. Strict
14+
CPU and GPU round trips preserved all 7,349 tensors bitwise, but Transformers
15+
5.8.0 cannot natively reload the local custom-code exports because its
16+
dynamic-module cache omits transitive configuration imports. The one-step
17+
HF/Megatron comparison predicts the same token but remains below the 0.99
18+
cosine gate. Unsupported and incomplete workflows remain explicitly
19+
identified rather than inferred from focused unit coverage.
1820
verification_index:
1921
model_level:
2022
verified:
@@ -27,8 +29,8 @@ verification_index:
2729
- manual_forward_pass
2830
training:
2931
H100:
30-
verified: [sft, peft]
31-
unverified: [sft_export_inference, sft_long_context]
32+
verified: [sft, sft_long_context, peft]
33+
unverified: [sft_export_inference]
3234
unsupported: [pretrain, checkpoint_resume]
3335
model:
3436
hf_id: nvidia/Nemotron-3-Nano-Omni-30B-A3B-Reasoning-BF16
@@ -180,37 +182,45 @@ items:
180182
H100:
181183
status: verified
182184
precision: bf16
183-
enabled_features: {}
185+
bridge_commit: 9b69b47a321de14b0453c230dd130e18ae0f00f7 # pragma: allowlist secret
186+
enabled_features:
187+
moe_dispatcher: hybridep
184188
command: >
185-
./scripts/training/train.sh --nodes 2 --gpus-per-node 8
186-
--recipe nemotron_omni_cord_v2_sft_4gpu_h100_bf16_config
189+
./scripts/training/train.sh --wait --nodes 1 --gpus-per-node 8
190+
--recipe nemotron_omni_cord_v2_sft_8gpu_h100_bf16_config
187191
--mode sft --step_func nemotron_omni_step
188192
--pretrained_checkpoint
189193
work/model-verification/nemotron-3-nano-omni-30b-a3b-reasoning/gpu-megatron-clean/iter_0000000
190-
--max_steps 10 --tensor_model_parallel_size 2
191-
--pipeline_model_parallel_size 2 --expert_model_parallel_size 4
192-
--expert_tensor_parallel_size 1
194+
--max_steps 100
193195
--save_dir
194-
work/model-verification/nemotron-3-nano-omni-30b-a3b-reasoning/sft-checkpoints-clean
195-
--save_interval 10
196+
work/model-verification/nemotron-3-nano-omni-30b-a3b-reasoning/sft-4k-checkpoints
197+
--save_interval 50
196198
'dataset.source.load_kwargs={revision:"7f0115a4b758a71d6473b8d085751692da2fef98"}'
197199
dataset.do_validation=false dataset.do_test=false
198200
validation.eval_iters=0 validation.eval_interval=0 checkpoint.load=null
199201
logger.log_interval=1 logger.log_throughput=true rng.seed=5678
200-
last_verified: 2026-07-30
202+
last_verified: 2026-08-12
201203
metrics:
202-
initial_loss: 1.123339
203-
final_loss: 0.4893276
204-
last_10_steps_step_time_ms_avg: 24454.11
205-
last_10_steps_model_tflops_per_gpu_avg: 60.73
206-
last_10_steps_tokens_per_second_per_gpu_avg: 669.990
204+
initial_loss: 1.281308
205+
final_loss: 0.03330838
206+
last_10_steps_step_time_ms_avg: 3603.67
207+
last_10_steps_model_tflops_per_gpu_avg: 197.10
208+
last_10_steps_tokens_per_second_per_gpu_avg: 9092.952
207209
expected_result: >
208-
The immutable-revision CORD v2 run completes exactly 10 full-SFT
209-
optimizer steps on 16 H100 GPUs at TP2/PP2/CP1/EP4/ETP1, GBS/MBS
210-
64/1. LM loss is finite from 1.123339 to 0.4893276, all ten recorded
211-
steps average 24,454.11 ms and 60.73 TFLOP/s/GPU including first-step
212-
compilation, no iteration is skipped or NaN, and a complete
213-
iter_0000010 checkpoint is saved.
210+
The immutable-revision, real image-text CORD v2 run completes exactly
211+
100 full-SFT optimizer steps on eight H100 GPUs at
212+
TP2/PP1/CP1/EP8/ETP1 and GBS/MBS 64/4 with natural-routing HybridEP.
213+
Execution uses selective MoE/layernorm recompute, fused attention, TE
214+
cross entropy, fused weighted squared-ReLU, and router fusion without
215+
changing the image-text objective or routing policy. Model parameters,
216+
main gradients, and Adam moments use BF16; optimizer main parameters use
217+
scaled FP16 without parameter remainders. LM loss is finite from
218+
1.281308 to 0.03330838, the final ten steps average 3,603.67 ms and
219+
197.10 TFLOP/s/GPU, sampled peak memory is 76,349 MiB, and no iteration
220+
is skipped or NaN. Complete optimizer and RNG checkpoints are saved at
221+
steps 50 and 100. Direct step-50 resume loads the complete state and
222+
reaches step 100 at 197.07 final-ten TFLOP/s/GPU with finite loss and
223+
zero skipped or NaN iterations.
214224
215225
sft_export_inference:
216226
H100:
@@ -224,10 +234,10 @@ items:
224234
--hf-model nvidia/Nemotron-3-Nano-Omni-30B-A3B-Reasoning-BF16
225235
--hf-revision 24e67ea000b7c2837fc8f9488aa2008524fac8ba
226236
--megatron-path
227-
work/model-verification/nemotron-3-nano-omni-30b-a3b-reasoning/sft-checkpoints-clean/iter_0000010
237+
work/model-verification/nemotron-3-nano-omni-30b-a3b-reasoning/sft-4k-checkpoints/iter_0000100
228238
--hf-path
229239
work/model-verification/nemotron-3-nano-omni-30b-a3b-reasoning/sft-hf-export-clean
230-
--torch-dtype bfloat16 --tp 2 --pp 1 --ep 4 --etp 1
240+
--torch-dtype bfloat16 --tp 2 --pp 1 --ep 8 --etp 1
231241
--trust-remote-code --distributed-save --not-strict
232242
- >
233243
uv run python
@@ -247,72 +257,86 @@ items:
247257
248258
sft_long_context:
249259
H100:
250-
status: unverified
260+
status: verified
251261
precision: bf16
262+
bridge_commit: eb7cf72392ac07e7a095b87d9b1d6bf2b38c5b6e # pragma: allowlist secret
252263
enabled_features:
253264
sequence_packing: in_batch
254265
context_parallel_size: 2
255266
command: >
256-
./scripts/training/train.sh --nodes 1 --gpus-per-node 8
267+
./scripts/training/train.sh --wait --nodes 1 --gpus-per-node 8
257268
--recipe nemotron_omni_cord_v2_long_context_sft_8gpu_h100_bf16_config
258269
--mode sft --step_func nemotron_omni_step
259270
--pretrained_checkpoint
260271
work/model-verification/nemotron-3-nano-omni-30b-a3b-reasoning/gpu-megatron-clean/iter_0000000
261-
--max_steps 10
272+
--max_steps 100
262273
--save_dir
263-
work/model-verification/nemotron-3-nano-omni-30b-a3b-reasoning/long-context-checkpoints-clean
264-
--save_interval 10
274+
work/model-verification/nemotron-3-nano-omni-30b-a3b-reasoning/sft-8k-packed-checkpoints
275+
--save_interval 50
265276
'dataset.source.load_kwargs={revision:"7f0115a4b758a71d6473b8d085751692da2fef98"}'
266277
dataset.do_validation=false dataset.do_test=false
267278
validation.eval_iters=0 validation.eval_interval=0 checkpoint.load=null
268279
logger.log_interval=1 logger.log_throughput=true rng.seed=5678
269-
last_verified: null
280+
last_verified: 2026-08-11
270281
metrics:
271-
initial_loss: null
272-
final_loss: null
273-
last_10_steps_step_time_ms_avg: null
274-
last_10_steps_model_tflops_per_gpu_avg: null
275-
last_10_steps_tokens_per_second_per_gpu_avg: null
282+
initial_loss: 1.251364
283+
final_loss: 0.01020405
284+
last_10_steps_step_time_ms_avg: 15577.9
285+
last_10_steps_model_tflops_per_gpu_avg: 93.73
286+
last_10_steps_tokens_per_second_per_gpu_avg: 4206.986
276287
expected_result: >
277-
The 8K TP4/PP1/CP2/EP1/ETP4, MBS2 in-batch-packing run uses
278-
precision-aware Adam with FP16 main parameters and stored FP32
279-
remainders, BF16 gradients, and BF16 moments. Step 1 completes with
280-
finite LM loss 1.142561 in 170,179.0 ms at 8.6 TFLOP/s/GPU with no
281-
skipped or NaN iteration, but step 2 encounters rank-divergent H100
282-
memory exhaustion and does not produce the required 10-step checkpoint.
288+
The immutable-revision, real image-text CORD v2 run completes exactly
289+
100 full-SFT optimizer steps on eight H100 GPUs at
290+
TP4/PP1/CP2/EP8/ETP1 and GBS/MBS 64/2 with 8K in-batch packing and the
291+
standard all-to-all dispatcher. Model parameters, main gradients, and
292+
Adam moments use BF16; optimizer main parameters use scaled FP16 without
293+
parameter remainders. LM loss is finite from 1.251364 to 0.01020405, the
294+
final ten steps average 15,577.9 ms and 93.73 TFLOP/s/GPU, sampled peak
295+
memory is 75,197 MiB, and no iteration is skipped or NaN. Complete
296+
optimizer and RNG checkpoints are saved at steps 50 and 100. Direct
297+
step-50 resume loads the complete state and reaches step 100 with finite
298+
loss and zero skipped or NaN iterations.
283299
284300
peft:
285301
H100:
286302
status: verified
287303
precision: bf16
288-
enabled_features: {}
304+
bridge_commit: eb7cf72392ac07e7a095b87d9b1d6bf2b38c5b6e # pragma: allowlist secret
305+
enabled_features:
306+
moe_dispatcher: hybridep
289307
command: >
290-
./scripts/training/train.sh --nodes 1 --gpus-per-node 8
291-
--recipe nemotron_omni_cord_v2_peft_4gpu_h100_bf16_config
308+
./scripts/training/train.sh --wait --nodes 1 --gpus-per-node 8
309+
--recipe nemotron_omni_cord_v2_peft_8gpu_h100_bf16_config
292310
--mode lora --step_func nemotron_omni_step
293311
--pretrained_checkpoint
294312
work/model-verification/nemotron-3-nano-omni-30b-a3b-reasoning/gpu-megatron-clean/iter_0000000
295-
--max_steps 10
313+
--max_steps 100
296314
--save_dir
297-
work/model-verification/nemotron-3-nano-omni-30b-a3b-reasoning/peft-checkpoints-clean
298-
--save_interval 10
315+
work/model-verification/nemotron-3-nano-omni-30b-a3b-reasoning/peft-checkpoints
316+
--save_interval 50
299317
'dataset.source.load_kwargs={revision:"7f0115a4b758a71d6473b8d085751692da2fef98"}'
300318
dataset.do_validation=false dataset.do_test=false
301319
validation.eval_iters=0 validation.eval_interval=0 checkpoint.load=null
302320
logger.log_interval=1 logger.log_throughput=true rng.seed=5678
303-
last_verified: 2026-07-30
321+
last_verified: 2026-08-11
304322
metrics:
305-
initial_loss: 1.098394
306-
final_loss: 0.3166811
307-
last_10_steps_step_time_ms_avg: 41897.41
308-
last_10_steps_model_tflops_per_gpu_avg: 22.22
309-
last_10_steps_tokens_per_second_per_gpu_avg: 782.101
323+
initial_loss: 1.282941
324+
final_loss: 0.02634745
325+
last_10_steps_step_time_ms_avg: 11621.42
326+
last_10_steps_model_tflops_per_gpu_avg: 61.25
327+
last_10_steps_tokens_per_second_per_gpu_avg: 2819.621
310328
expected_result: >
311-
The ten-step TP4/PP1/CP1/EP1 LoRA run exits successfully and saves a
312-
complete eight-shard iter_0000010 adapter checkpoint. LM loss is finite
313-
from 1.098394 to 0.3166811, all ten steps average 41,897.41 ms and
314-
22.22 TFLOP/s/GPU including first-step compilation, and no iteration is
315-
skipped or NaN.
329+
The immutable-revision, real image-text CORD v2 run completes exactly
330+
100 LoRA optimizer steps on eight H100 GPUs at
331+
TP2/PP1/CP1/EP8/ETP1 and GBS/MBS 64/1 with natural-routing HybridEP.
332+
Model parameters, main gradients, and Adam moments use BF16; optimizer
333+
main parameters use scaled FP16 without parameter remainders. LM loss is
334+
finite from 1.282941 to 0.02634745, the final ten steps average
335+
11,621.42 ms and 61.25 TFLOP/s/GPU, sampled peak memory is 44,873 MiB,
336+
and no iteration is skipped or NaN. Complete optimizer and RNG
337+
checkpoints are saved at steps 50 and 100. Direct step-50 resume loads
338+
the complete state and reaches step 100 with finite loss and zero
339+
skipped or NaN iterations.
316340
317341
checkpoint_resume:
318342
all:

src/megatron/bridge/models/nemotron_omni/modeling_nemotron_omni.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
import torch
3434
from megatron.core import tensor_parallel
35+
from megatron.core.fp8_utils import get_fp8_align_size
3536
from megatron.core.models.hybrid.hybrid_model import HybridModel
3637
from megatron.core.models.multimodal.llava_model import pixel_shuffle
3738
from megatron.core.models.vision.multimodal_projector import MultimodalProjector
@@ -107,6 +108,28 @@ def _pixel_shuffle_dynamic_resolution(
107108
return shuffled.reshape(batch, (height * width) // 4, hidden * 4)
108109

109110

111+
def _project_multimodal_embeddings(
112+
projection: torch.nn.Module,
113+
embeddings: torch.Tensor,
114+
) -> torch.Tensor:
115+
"""Project media rows, padding only the temporary FP8 compute input."""
116+
input_shape = embeddings.shape[:-1]
117+
flat_embeddings = embeddings.reshape(-1, 1, embeddings.shape[-1])
118+
num_embeddings = flat_embeddings.shape[0]
119+
projection_config = getattr(projection, "config", None)
120+
if getattr(projection_config, "fp8", None):
121+
alignment = get_fp8_align_size(projection_config.fp8_recipe)
122+
padding = -num_embeddings % alignment
123+
if padding:
124+
flat_embeddings = torch.cat(
125+
(flat_embeddings, flat_embeddings.new_zeros((padding, 1, flat_embeddings.shape[-1]))),
126+
dim=0,
127+
)
128+
129+
projected = projection(flat_embeddings)[:num_embeddings]
130+
return projected.reshape(*input_shape, projected.shape[-1])
131+
132+
110133
class NemotronOmniModel(MegatronModule):
111134
"""Nemotron Omni model whose input sequence is already media-expanded.
112135
@@ -389,8 +412,7 @@ def _encode_images(
389412
encoded = encoded[:, class_tokens:, :]
390413
encoded = pixel_shuffle(encoded).reshape(-1, encoded.shape[-1] * 4)
391414

392-
projected = self.vision_projection(encoded.unsqueeze(1))
393-
return projected.squeeze(1).contiguous()
415+
return _project_multimodal_embeddings(self.vision_projection, encoded).contiguous()
394416

395417
def _encode_sound(self, sound_clips: torch.Tensor, sound_length: Optional[torch.Tensor]) -> torch.Tensor:
396418
"""Encode mel features and return valid projected rows in sample order."""
@@ -419,7 +441,10 @@ def _encode_sound(self, sound_clips: torch.Tensor, sound_length: Optional[torch.
419441
projection_parameter = next(self.sound_projection.parameters(), None)
420442
if projection_parameter is not None:
421443
sound_embeddings = sound_embeddings.to(dtype=projection_parameter.dtype)
422-
projected = self.sound_projection(sound_embeddings.permute(1, 0, 2).contiguous()).contiguous()
444+
projected = _project_multimodal_embeddings(
445+
self.sound_projection,
446+
sound_embeddings.permute(1, 0, 2).contiguous(),
447+
).contiguous()
423448
projected_by_sample = projected.permute(1, 0, 2)
424449
if getattr(getattr(self.sound_model, "config", None), "sound_pad_to_clip_duration", False):
425450
return projected_by_sample.reshape(-1, projected.shape[-1]).contiguous()

src/megatron/bridge/models/nemotron_omni/nemotron_omni_provider.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ class NemotronVLModelProvider(HybridModelProvider, ABC):
101101
def _build_vision_config(self, language_cfg):
102102
"""Build RADIO ViT-H vision encoder config from a language config copy."""
103103
vision_cfg = copy.deepcopy(language_cfg)
104+
if not self.use_vision_backbone_fp8_arch:
105+
vision_cfg.fp8 = None
106+
vision_cfg.fp8_param = False
104107
vision_cfg.sequence_parallel = False
105108
vision_cfg.context_parallel_size = 1
106109
vision_cfg.tp_comm_overlap = False

src/megatron/bridge/recipes/nemotron_omni/h100/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
__all__ = [
1919
"nemotron_omni_cord_v2_long_context_sft_8gpu_h100_bf16_config",
2020
"nemotron_omni_cord_v2_peft_4gpu_h100_bf16_config",
21+
"nemotron_omni_cord_v2_peft_8gpu_h100_bf16_config",
2122
"nemotron_omni_cord_v2_sft_4gpu_h100_bf16_config",
23+
"nemotron_omni_cord_v2_sft_8gpu_h100_bf16_config",
2224
"nemotron_omni_valor32k_peft_4gpu_h100_bf16_config",
2325
"nemotron_omni_valor32k_sft_4gpu_h100_bf16_config",
2426
]

0 commit comments

Comments
 (0)