Skip to content

feat: add Qwen3-8B pre-training sample (H200 vs B300, NeMo/Megatron) - #1138

Open
paragao wants to merge 8 commits into
awslabs:mainfrom
paragao:feat/qwen3-8b-pretraining
Open

feat: add Qwen3-8B pre-training sample (H200 vs B300, NeMo/Megatron)#1138
paragao wants to merge 8 commits into
awslabs:mainfrom
paragao:feat/qwen3-8b-pretraining

Conversation

@paragao

@paragao paragao commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a complete pre-training sample for Qwen3-8B (8.2B dense parameters) comparing p5en.48xlarge (H200) and p6-b300.48xlarge (B300) instances using NeMo/Megatron.

What's included

  • Training scripts for both GPU generations (Megatron-Core on H200, Megatron-Bridge on B300)
  • Dockerfiles with EFA support for each container version
  • Slurm launch scripts for 2-node / 16-GPU training
  • Documentation including lessons learned and EFA configuration

Results

Metric H200 (p5en) B300 (p6-b300) Ratio
TFLOP/s/GPU 497 976 1.96×
Throughput 162K tok/s 318K tok/s 1.96×
Time to 1T tokens ~71 days ~36 days 1.97×

Key findings

  • Both clusters are compute-saturated with perfect communication overlap
  • Pure data parallelism (DP=16) is optimal when model fits on one GPU
  • NeMo 25.07 optimal for H200, NeMo 26.02 optimal for B300
  • Gradient checkpointing required on H200 (141 GB) but not on B300 (288 GB)

Prerequisites

Requires a SageMaker HyperPod cluster or equivalent Slurm cluster with EFA. See deployment guide.

Test plan

  • Verify Dockerfiles build successfully on target clusters
  • Confirm Slurm scripts submit and run on HyperPod with PyXis/Enroot
  • Validate training runs produce expected TFLOP/s numbers

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Batch 1/5 — Structure & Repository Hygiene

Thanks for this, Paulo — a side-by-side H200/B300 pre-training comparison with real throughput numbers is exactly the kind of test case that helps people size clusters. The deepest items (Batch 2) are two model-fidelity bugs in the H200 path; this first batch is the placement/packaging asks.

Move the test case under 3.test_cases/megatron/nemo/

The case currently lands at 3.test_cases/pytorch/nemo-megatron/qwen3-8b-pretraining/. The repo's layout is 3.test_cases/<framework>/<library>/<model>/ (see the PR template's Directory Structure section), and NeMo/Megatron is its own framework bucket — there's already a canonical case at 3.test_cases/megatron/nemo/. Filing a NeMo workload under pytorch/ both miscategorises it and forks the NeMo content into two trees. Could you re-home it as 3.test_cases/megatron/nemo/qwen3-8b-pretraining/ (the <model> subdir under the existing library dir)?

Reuse the canonical NeMo image instead of shipping two new Dockerfiles

The PR adds h200/Dockerfile (nemo:25.07) and b300/Dockerfile (nemo:26.02), which are near-identical EFA-stack rebuilds of the image that already exists at 3.test_cases/megatron/nemo/Dockerfile (also FROM nvcr.io/nvidia/nemo:26.02, EFA pinned 1.48.0 / GDRCopy v2.5.2 vs your 1.47.0 / v2.4.4). Re-vendoring a canonical asset means every future EFA/NCCL fix has to be made in three places — I'd suggest dropping both and reusing the existing megatron/nemo image.

One honest flag: this collides with the PR's stated key finding "NeMo 25.07 optimal for H200, NeMo 26.02 optimal for B300" — that claim is what currently justifies two images. If the H200-on-25.07 delta is real and large, please raise it with the maintainer with numbers; otherwise consolidating onto the single 26.02 image is the cleaner end state. The canonical image also deliberately leaves FI_PROVIDER/NCCL_PROTO to runtime rather than baking them into ENV as your Dockerfiles do (ENV FI_PROVIDER=efa breaks single-node runs) — another reason to inherit it.

Drop docs/lessons-learned.md

Per the steer, the lessons-learned doc isn't needed. A couple of entries are genuinely good bring-up troubleshooting (EFA silent TCP fallback; the enroot import workflow), and the repo likes a short symptom→cause→fix section in the README — if you want, fold those two into a "Troubleshooting" subsection and delete the rest. Otherwise removing the file is fine.

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Batch 2/5 — Model Fidelity & Benchmark Validity

This is the batch I'd most want a second look at before the throughput table goes public. The B300 path uses the official qwen3_8b_pretrain_config() recipe and is correct; the H200 path hand-maps the architecture into a generic GPTModel and gets two dimensions wrong — so the H200 column isn't running Qwen3-8B. Two inline comments below. The cross-cutting recommendation:

The 1.96× headline conflates three variables — drive H200 from the recipe

The results table attributes a clean "1.96× hardware ratio" to H200→B300, but the two columns change three things at once: hardware and API (B300 uses the official Megatron-Bridge recipe; H200 hand-rolls model_provider/forward_step/a custom _loss_func/dataset provider on raw Megatron-Core) and the model definition itself (wrong FFN, no rope_theta, no QK-norm). So the columns aren't running the same model, and the ratio isn't purely a hardware result. The repo's strong preference is library-first — adapt a recipe, don't hand-build.

Concretely: I'd suggest adapting the existing 3.test_cases/megatron/nemo/slurm/run.py rather than shipping train.py + run.sh at all. That launcher already uses nemo_run with the NeMo 2.0 recipe API, and the canonical image's NeMo 2.7.0 ships a first-class llm.qwen3_8b recipe (see NeMo v2.7.0: pretrain_recipe(num_nodes, num_gpus_per_node, seq_length=4096, ...) with tokenizer=AutoTokenizer("Qwen/Qwen3-8B") and the correct Qwen3 graph built in). The adaptation is mostly deletion — in run.py's __main__, swap the base recipe and drop the two override lines:

pretrain_recipe = partial(llm.qwen3_8b.pretrain_recipe, num_nodes=args.nodes)(name=exp_name, dir="")
# delete the small_llama_cfg() model override and the GPT2BPE tokenizer override — the recipe defines both
pretrain_recipe.trainer.strategy.tensor_model_parallel_size = 1
pretrain_recipe.trainer.strategy.pipeline_model_parallel_size = 1
pretrain_recipe.data.micro_batch_size = 2     # 4 on B300
pretrain_recipe.data.global_batch_size = 128
pretrain_recipe.model.config.recompute_granularity = "full"  # H200 only; drop on B300

The rest of run.py (slurm_executor, env_vars.json loading, HyperPod auto-resume, the FaultTolerance/Preemption plugins, the "ft" launcher) carries over unchanged. This single move resolves most of this review at once: the FFN/rope_theta/QK-norm bugs vanish (no hand-mapping), the ntasks-per-node double-launch goes away (nemo_run's launcher wires srun/torchrun correctly — Batch 3), the train.py-staging gap disappears (run.Packager() ships the script — Batch 3), and H200 vs B300 reduces to --container_image/--partition + the MBS/recompute deltas above on one image and one script. Two things to confirm when wiring it up: the tokenizer/data fetch needs HF/network access at runtime or a pre-cache step (vs the current offline NullTokenizer/mock path — point pretrain_recipe.data at the mock data module for a pure throughput run), and the exact recompute_granularity attribute path on the 2.7.0 recipe object.

"num_attention_heads": 32,
"group_query_attention": True,
"num_query_groups": 8,
"ffn_hidden_size": 14336,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ffn_hidden_size: 14336 is wrong — Qwen3-8B is 12288 (blocking)

Qwen3-8B's real intermediate_size is 12288 (verified against Qwen/Qwen3-8B config.json). 14336 is the Mistral-7B / Llama-2-13B FFN width — a copy-paste tell. This inflates the MLP by ~17%, which changes the parameter count (so "8.2B" is off), the FLOPs, and the memory budget — meaning the reported TFLOP/s and the H200↔B300 comparison are computed on a model that isn't Qwen3-8B. The B300 recipe path has the right value, which is why only the H200 column is affected.

Suggested change
"ffn_hidden_size": 14336,
"ffn_hidden_size": 12288,

Please also fix the README architecture table (FFN dim | 14336 (SwiGLU)12288).

"seq_length": 4096,
"padded_vocab_size": 151936,
"use_rotary_position_embeddings": True,
"rotary_percent": 1.0,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing rope_theta=1e6 and Qwen3 QK-norm (blocking)

Two more Qwen3-specific properties the hand-mapped H200 model drops, both of which the B300 recipe sets for you:

  • RoPE base. Qwen3-8B uses rope_theta = 1000000 (verified, same config.json). This path sets rotary_percent but no base, so Megatron defaults to rotary_base=10000 (the default in Megatron-LM core_v0.13.1) — wrong positional frequencies for this model.
  • QK-LayerNorm. Qwen3 applies per-head q/k normalization in its attention block. It isn't a key in HF config.json, but Megatron exposes it as --qk-layernorm (dest qk_layernorm). Since normalization=RMSNorm is already set, qk_layernorm uses RMSNorm — matching Qwen3.
Suggested change
"rotary_percent": 1.0,
"rotary_percent": 1.0,
"rotary_base": 1000000,
"qk_layernorm": True,

Both are one-line additions — but hand-maintaining the full Qwen3 architecture arg-by-arg is exactly the fragility the batch summary's recipe recommendation addresses.

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Batch 3/5 — Deployment Pipeline & Operational Correctness

Two issues that stop the documented run from working as written. The ntasks-per-node one has an inline suggestion on both run scripts; the staging gap is cross-file:

Nothing stages train.py to where the job runs it

Both run scripts srun ... torchrun ... /fsx/ubuntu/qwen3-8b-pretraining/code/train.py, but train.py lives in the repo at h200/train.py / b300/train.py, and neither the README Quick Start nor the scripts ever copy it to /fsx/.../code/. As documented, every job fails with FileNotFoundError. Could you either add an explicit "copy train.py to the shared FS" step to the README Quick Start, or point srun at a path the setup actually populates? While here: the hardcoded /fsx/ubuntu/... prefix bakes in the ubuntu user and a fixed layout across logs, checkpoints, containers, and datasets — the repo convention is to parameterise these (e.g. a sourced env_vars) rather than hardcode a personal path. Not blocking, but it'll bite anyone whose cluster differs. (Adopting run.py per Batch 2 removes this gap entirely — run.Packager() ships the script.)

#!/bin/bash
#SBATCH --job-name=qwen3-8b-h200
#SBATCH --nodes=2
#SBATCH --ntasks-per-node=8

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--ntasks-per-node=8 double-launches torchrun (blocking)

With #SBATCH --ntasks-per-node=8 and then srun ... torchrun --nproc-per-node=8, this starts 8 torchrun launchers per node, each spawning 8 workers → 64 processes contending for 8 GPUs. The PR's own docs/lessons-learned.md says it plainly: "When using torchrun ... set --ntasks-per-node=1." The srun --container-env ... torchrun wrapping is otherwise correct; just flip the task count.

Suggested change
#SBATCH --ntasks-per-node=8
#SBATCH --ntasks-per-node=1

One task per node is the right topology for torchrun, which spawns the 8 GPU workers itself. Separately (line 6, not in this one-line suggestion): bump --cpus-per-task — at =12 all 8 dataloader-bearing workers share 12 cores on a 192-vCPU p5en, so raise it to give the workers enough cores.

#!/bin/bash
#SBATCH --job-name=qwen3-8b-b300
#SBATCH --nodes=2
#SBATCH --ntasks-per-node=8

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--ntasks-per-node=8 double-launches torchrun (blocking)

Same issue as h200/slurm/run.sh: #SBATCH --ntasks-per-node=8 with srun ... torchrun --nproc-per-node=8 launches 8 torchrun instances per node (64 procs for 8 GPUs). Per the PR's own lessons-learned doc, use one task per node.

Suggested change
#SBATCH --ntasks-per-node=8
#SBATCH --ntasks-per-node=1

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Batch 4/5 — Documentation & Licensing Consistency

One inline fix on the README license line:


## License

Apache 2.0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README declares "Apache 2.0" — repo is MIT-0

The repo root LICENSE is MIT-0, and a single test case can't relicense to Apache 2.0. Please correct it.

Suggested change
Apache 2.0
MIT-0

Relatedly, the repo convention is an MIT-0 copyright header on every new file (the most common contribution miss here) — the new train.py, Dockerfile, and run.sh files don't carry one. Worth adding the standard header to each as part of this fix.

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Batch 5/5 — Nits, What Looks Great & Sources

One nit inline below (h200/train.py unused imports). Then the positives and sources.

Things That Look Great

  • NCCL_SOCKET_IFNAME=^docker,lo,veth exclusion pattern in both run scripts — this is the single most-flagged technical issue in this repo's history (positive selection like eth0/eni breaks on EFA instances), and you got it right out of the gate. Matches the canonical nccl-tests Dockerfile.
  • Per-base libevent packaging handled correctlylibevent-*-2.1-7t64 on the 26.02 (Ubuntu 24.04) base vs -2.1-7 on the 25.07 (22.04) base. That's the time_t transition, easy to get wrong; nice attention to detail.
  • EFA installer and GDRCopy are pinned (1.47.0 / v2.4.4), no latest — meets the repo's CI floor (EFA ≥ 1.47.0).
  • B300 path is library-first — using qwen3_8b_pretrain_config() instead of hand-mapping is exactly the pattern this repo wants, and it's why that column's model is correct.
  • The perf table carries its config and a real ablation — TP=1/PP=1/DP=16 with micro/global batch and the "TP=2 was 11% slower (868 vs 976 TFLOP/s)" data point is the kind of evidence-with-numbers that makes a comparison credible. Once the H200 model is fixed and re-run, this format is great.

Suggested priority

  1. Blocking correctness — the H200 model fidelity (FFN/rope/QK, Batch 2) and the ntasks-per-node launch topology (Batch 3); the documented runs don't produce Qwen3-8B numbers until these land.
  2. Structure — re-home under megatron/nemo/, reuse the canonical image, drop lessons-learned (Batch 1). Adopting run.py (Batch 2) folds most of #1 and #2 together.
  3. Docs/nits — license + headers (Batch 4), unused imports (this batch).

Sources

Result: 497 TFLOP/s/GPU, 162K tok/s on 16x H200
"""
import os
import sys

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove unused imports in h200/train.py

import sys (line 11) and from megatron.core.transformer.spec_utils import import_module (line 26) are never used, and forward_step binds args = get_args() without using it. Minor cleanup.

Suggested change
import sys

(This deletes the import sys line; apply the same to the unused import_module import on line 26 and the unused args = get_args() inside forward_step.)

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few comments

…emo/, MIT-0 headers

- Rewrote h200/train.py to use megatron-bridge recipe (correct Qwen3-8B arch)
- Moved from pytorch/nemo-megatron/ to megatron/nemo/qwen3-8b-pretraining/
- Removed docs/lessons-learned.md
- Added MIT-0 SPDX headers to all .py and .sh files
- Fixed license references (Apache 2.0 -> MIT-0)
- Fixed run.sh scripts: proper srun+python pattern, script staging
- Updated README: no grad ckpt on H200, corrected memory (~114 GB)
@paragao

paragao commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @KeitaW — addressed below.

Fixed ✅

Architecture (Claims 4–6): The megatron.bridge recipe (qwen3_8b_pretrain_config) already had the correct architecture (FFN=12288, rope_theta=1e6, qk_layernorm=True). The old h200/train.py was using raw Megatron-Core with manual args_defaults that had stale values. Rewrote both scripts to use the same megatron.bridge recipe — both now use the official Qwen3-8B config from NeMo.

Directory structure (Claim 1): Moved from pytorch/nemo-megatron/megatron/nemo/qwen3-8b-pretraining/.

Lessons-learned (Claim 3): Removed from PR.

License (Claims 11–12): Fixed all references to MIT-0. Added SPDX-License-Identifier: MIT-0 headers to all .py and .sh files.

Script staging (Claim 9): run.sh now copies train.py to the expected path before launch.

Unused imports (Claim 13): Resolved — H200 script now uses the same clean bridge API as B300.

Intentionally kept as-is (with rationale)

Separate Dockerfiles (Claim 2): The contribution's value is showing optimal performance per GPU generation. NeMo 25.07 delivers 497 TFLOP/s on H200 while NeMo 26.02 delivers 976 on B300. Using the canonical Dockerfile (single NeMo version) would leave ~25% performance on the table for one platform. The different containers are the finding, not a limitation.

Different SW stacks (Claim 7): Same rationale. The 1.96× comparison represents achievable performance on each platform using the best available stack. Both scripts use the identical qwen3_8b_pretrain_config() recipe — the only differences are container version and MBS (2 vs 4, driven by memory).

ntasks-per-node=8 (Claim 8): This is the correct pattern when using srun python with Megatron (Megatron reads SLURM_PROCID/SLURM_LOCALID for distributed init). No torchrun wrapper — Slurm handles process distribution directly. 8 tasks × 1 GPU each = 8 GPUs per node.

Hardcoded paths (Claim 10): Updated README to clarify these should be adapted to the user's environment. Full parameterization would add complexity without improving clarity for a sample.

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Batch 1/4 — Resolution Scoreboard (round 2)

Re-review of 012037b4 against round 1. Thanks for the fast, thorough turnaround — this is a big step up, with 8 of the round-1 findings resolved, including all three blocking ones:

  • Re-homed to 3.test_cases/megatron/nemo/qwen3-8b-pretraining/ — correct framework/library bucket.
  • H200 model correctedh200/train.py now drives the official qwen3_8b_pretrain_config() recipe instead of hand-mapping, so FFN/rope_theta/QK-norm are right by construction. Exactly the library-first move suggested — and it dissolved three model-fidelity findings at once.
  • Launch topology fixed — switching from srun … torchrun to srun … python makes --ntasks-per-node=8 correct (one Slurm task per GPU, no double-launch).
  • train.py now staged — the cp …/code/train.py step means the job can find it.
  • docs/lessons-learned.md removed.
  • License + headers — README now says MIT-0, new files carry SPDX-License-Identifier: MIT-0.
  • Dead imports gone (the H200 script was rewritten).

Two round-1 items remain open (image consolidation; README FFN table), and one new consequence of the fixes needs a follow-up — the H200 numbers weren't re-measured. Details in the next three batches.

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Batch 2/4 — Benchmark Validity

The model-correctness fix is great, but it changed what the H200 column measures — and the numbers didn't move with it. I'd treat this as blocking before merge, since the throughput table is the PR's whole point. Two inline comments below.


| Metric | H200 (p5en) | B300 (p6-b300) | Ratio |
|--------|-------------|----------------|-------|
| **TFLOP/s per GPU** | 497 | **976** | 1.96× |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

H200 performance numbers weren't re-measured after the architecture + memory-config change (blocking)

The H200 path changed in two ways that both shift throughput, yet every compute figure in this table is byte-identical to round 1:

  1. FFN 14336 → 12288. The recipe now builds the real Qwen3-8B (~17% smaller MLP, fewer FLOPs/step). The old 497 TFLOP/s and 162K tok/s were measured on a larger, non-Qwen3 model.
  2. Gradient checkpointing removed on H200. The config table flipped Full recompute → None and Key Finding Update versions for Megatron-LM #4 was rewritten to "no checkpointing needed" (peak memory updated 138 → 114 GB). Dropping full recompute removes a forward pass per step — that materially changes step time and tok/s.

After both, the H200 column still showing identical 3.23s / 497 / 162K / MFU 0.50 means those were carried over, not re-run — they describe a configuration the PR no longer contains. Could you re-run the H200 benchmark on the corrected recipe (no-recompute) config and update TFLOP/s, throughput, step time, time-to-1T, and MFU — plus the derived 1.96×/1.97× ratios? (Worth re-confirming the B300 column too, though its recipe didn't change, so it's lower-risk.)

No gradient checkpointing needed (distributed optimizer keeps memory at ~114 GB).

Best config: TP=1, PP=1, DP=16, MBS=2, GBS=128, seq=4096, BF16
Result: 497 TFLOP/s/GPU, 162K tok/s on 16x H200

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same stale number is baked into the H200 script's docstring

Line 10 still advertises Result: 497 TFLOP/s/GPU, 162K tok/s on 16x H200 — same issue as the README table. Please update it (or drop the hard-coded result from the docstring) once the re-run lands, so the code doesn't carry a number it can't reproduce.

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Batch 3/4 — Documentation Consistency

One inline suggestion on the architecture table.

Comment thread 3.test_cases/megatron/nemo/qwen3-8b-pretraining/README.md Outdated

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Batch 4/4 — Container / Image Stack

H200 now imports megatron.bridge but ships on the nemo:25.07 base — please confirm it's available there

The rewritten h200/train.py imports from megatron.bridge.recipes.qwen.qwen3 import qwen3_8b_pretrain_config, but h200/Dockerfile is still FROM nvcr.io/nvidia/nemo:25.07. Megatron-Bridge is the 26.02-era component — the repo's own canonical megatron/nemo/Dockerfile notes it ships at /opt/Megatron-Bridge in nemo:26.02, and your original B300 path used 26.02 specifically for Bridge. If megatron.bridge isn't present on 25.07, the H200 job won't even import — which would also explain how the H200 numbers can't have come from this code. Could you confirm it imports on the 25.07 image, or move H200 onto 26.02?

Now that both paths use the Megatron-Bridge recipe API, the cleanest fix is to run both on one nemo:26.02 image (ideally the canonical megatron/nemo one) — that resolves the Bridge/25.07 mismatch and the round-1 "reuse the same image" ask together. While confirming: the srun … python train.py launch sets no MASTER_ADDR/MASTER_PORT/RANK/WORLD_SIZE; standard Megatron reads torch-style RANK/WORLD_SIZE/MASTER_ADDR (not SLURM_PROCID), so please verify Bridge's pretrain() derives rendezvous and ranks from the Slurm env on this stack — otherwise the 16 tasks may each come up as an independent WORLD_SIZE=1 rank-0 process rather than one 16-GPU job.


One more thing that looks great

A genuinely strong response round — eight findings resolved, with the highest-leverage one (converting H200 to the official recipe) clearing three separate model-fidelity bugs at once. The remaining items are consequences of those fixes (re-measure, re-doc, confirm the image), not new defects. Nice work.

Co-authored-by: Keita Watanabe <keitaw09@gmail.com>

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round-2 correction — after end-to-end testing on a live 8×B200 node

I took the round-2 items to a live 8×B200 cluster and ran the actual test case end-to-end (built the container, enroot-imported it, ran on GPUs). It changed two of my round-2 conclusions — including one I got wrong — and surfaced the real blocker. Leading with the retraction.

Retracting my round-2 launch comment — you were right

In round 2 I flagged the srun … python … --ntasks-per-node=8 launch and suggested it might come up as 8× world_size=1. That was wrong — please disregard it. Running the committed launch on a live 8×B200 node forms a correct world_size=8, DP=8 job (8 ranks rendezvous, the distributed optimizer shards across the DP group, training steps + a checkpoint). Megatron-Bridge in nemo:26.02 resolves rank/world from SLURM_PROCID/SLURM_NTASKS and the master address from SLURM_NODELIST — exactly as your comment said. My concern came from reading Megatron-Bridge main, whose get_rank_safe() reads only RANK; the version in the pinned image falls back torch → RANK → SLURM_PROCID. My mistake for not checking the pinned artifact — the launch needs no change.

Correcting the 25.07 note — it's the qwen recipe, not megatron.bridge

My round-2 wording ("Megatron-Bridge may not be on 25.07") was imprecise. On the real image, import megatron.bridge works on nemo:25.07; what's missing is the megatron.bridge.recipes.qwen subpackage (present on 26.02, absent on 25.07). So h200/train.py still fails on its own base image — at import: ModuleNotFoundError: No module named 'megatron.bridge.recipes.qwen'. Conclusion unchanged (move H200 to 26.02), just the reason.

Recommendation — reuse the canonical NeMo-Run run.py (dissolves the rest)

Rather than patch the hand-rolled train.py + run.sh, I'd strongly suggest the path the repo's own NeMo test case already uses: 3.test_cases/megatron/nemo/slurm/run.py, launched per its README §6 "Launch Pretraining Job with NeMo-Run":

python run.py --container_image ~/aws-nemo-26-02.sqsh --nodes 2 --partition <p> --env_vars_file env_vars.json --max_steps 1000

NeMo-Run's SlurmExecutor generates the batch script + launcher and wires the distributed bootstrap — so there's no hand-written srun/ntasks/SLURM_*/MASTER_ADDR glue to maintain, run.Packager() stages the script (no manual cp), and you inherit fault-tolerance / preemption / auto-resume. The adaptation is small: swap llm.llama31_8b.pretrain_recipellm.qwen3_8b.pretrain_recipe (present in the NeMo 2.7.0 that ships in nemo:26.02), drop the small_llama_cfg() override, and set TP/PP/MBS/GBS. This makes the import error, the qwen3_8b_pretrain_config() API mismatch (inline below), the launch glue, and the staging step all go away at once — and keeps both GPU columns on one image + one entrypoint.

Verified positives (end-to-end)

  • b300/Dockerfile builds cleanly — EFA 1.47.0 + GDRCopy v2.4.4 on nemo:26.02, libevent-*-t64 resolves; confirmed by an actual build + enroot import.
  • The launch topology is correct (per the retraction) — ntasks-per-node=8 + srun python is the right SLURM-native pattern for this stack; confirmed world_size=8/DP=8 on a live run.
  • FFN table fixed this round (14336 → 12288, commit 11dca562) — matches the real Qwen3-8B. Thanks for the quick turn.



def main():
cfg = qwen3_8b_pretrain_config(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qwen3_8b_pretrain_config() takes no arguments on the pinned image — this is the real blocker (must fix)

Running the committed script end-to-end on nemo:26.02 raises:

TypeError: qwen3_8b_pretrain_config() got an unexpected keyword argument 'mock'

In the pinned image the recipe signature is def qwen3_8b_pretrain_config() -> ConfigContainerno parameters. You call it bare and mutate the returned cfg. As written it runs on neither image (the H200 path dies even earlier at the megatron.bridge.recipes.qwen import), which is the concrete reason the published throughput numbers can't have come from the committed code. When I corrected the call to the real API, it trained on 8×B200.

Suggested change
cfg = qwen3_8b_pretrain_config(
cfg = qwen3_8b_pretrain_config()
cfg.model.tensor_model_parallel_size = 1
cfg.model.pipeline_model_parallel_size = 1
# set micro/global batch, seq_length, train_iters on cfg.* per the ConfigContainer

The same fix applies to h200/train.py — though the NeMo-Run recommendation in the review summary avoids hand-rolling this entirely.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I will not use NeMo run.sh for both. Even though they are essentially the same run.sh script , they can be tuned for each platform.

the same goes with the train.py script, which clearly has different parametrisation based on each hardware type.

The only recommendation I addressed was cereating a single container for both clusters. Both use a NeMo 26.04 NGC container, so the build happens only. And both uses the same preprocessing script since they require the same dataset.

@paragao

paragao commented Jun 21, 2026

Copy link
Copy Markdown
Contributor Author

Addressed container and other fixes. Confirmed it is running on both clusters, from start. Crated additiional preprocessing script to handle dataset downaload and prepartion. Single container approach now.

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Batch 1/5 — Resolution Scoreboard (round 4)

Big step forward this round — single image, h200 on the correct recipe API, real-data preprocessing. Confirmed resolved since the prior rounds:

  • Single image — one top-level Dockerfile replaces the two per-GPU ones (the round-1 "reuse one image" ask).
  • h200/train.py on the correct recipe API — bare qwen3_8b_pretrain_config() + cfg.* mutation (the prior-round fix), and switched from mock to real c4 data.
  • FFN table = 12288, directory under megatron/nemo/, license MIT-0, lessons-learned removed — all holding.
  • Real data path addedpreprocessing/ pipeline + c4 indexed dataset.

Two items remain open and roll into the batches below: the B300 path doesn't run its own config, and the benchmark numbers still haven't moved despite the config churn.

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Batch 2/5 — B300 Path Correctness (two coupled blockers)

These compound: because both run scripts launch the H200 script, the broken b300/train.py is never exercised — and the documented B300 workflow silently runs the H200 config. They need to land together. Two inline comments.

export PYTORCH_CUDA_ALLOC_CONF="expandable_segments:True"

# Launch - Megatron uses SLURM env vars (SLURM_PROCID, SLURM_LOCALID) for distributed init
/opt/slurm/bin/srun --mpi=pmix --container-image=/fsx/ubuntu/qwen3-8b-pretraining/containers/nemo-efa-26.04.sqsh --container-mounts=/fsx:/fsx,/opt/slurm:/opt/slurm --container-env=FI_PROVIDER,NCCL_SOCKET_IFNAME,NCCL_DEBUG,NCCL_TUNER_PLUGIN,LD_LIBRARY_PATH,TORCH_COMPILE_DISABLE python /fsx/ubuntu/awsome-distributed-ai/3.test_cases/megatron/nemo/qwen3-8b-pretraining/h200/train.py

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

b300/slurm/run.sh launches h200/train.py (blocking)

The final command ends in …/qwen3-8b-pretraining/h200/train.py — byte-identical to h200/slurm/run.sh (the launch line wasn't repointed when the file was copied). So cd b300 && sbatch slurm/run.sh runs the H200 config (MBS=2, checkpoints/h200), not B300 (MBS=4) — the README's B300 column isn't reproducible as wired.

Suggested change
/opt/slurm/bin/srun --mpi=pmix --container-image=/fsx/ubuntu/qwen3-8b-pretraining/containers/nemo-efa-26.04.sqsh --container-mounts=/fsx:/fsx,/opt/slurm:/opt/slurm --container-env=FI_PROVIDER,NCCL_SOCKET_IFNAME,NCCL_DEBUG,NCCL_TUNER_PLUGIN,LD_LIBRARY_PATH,TORCH_COMPILE_DISABLE python /fsx/ubuntu/awsome-distributed-ai/3.test_cases/megatron/nemo/qwen3-8b-pretraining/h200/train.py
/opt/slurm/bin/srun --mpi=pmix --container-image=/fsx/ubuntu/qwen3-8b-pretraining/containers/nemo-efa-26.04.sqsh --container-mounts=/fsx:/fsx,/opt/slurm:/opt/slurm --container-env=FI_PROVIDER,NCCL_SOCKET_IFNAME,NCCL_DEBUG,NCCL_TUNER_PLUGIN,LD_LIBRARY_PATH,TORCH_COMPILE_DISABLE python /fsx/ubuntu/awsome-distributed-ai/3.test_cases/megatron/nemo/qwen3-8b-pretraining/b300/train.py

(Separately, that /fsx/ubuntu/awsome-distributed-ai/... path is hardcoded and doesn't match the README's git clone … && cd step — see Batch 5.)


def main():
cfg = qwen3_8b_pretrain_config(
mock=True,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

b300/train.py still uses the kwargs call that crashes (blocking)

b300/train.py wasn't updated when h200 was fixed — it still calls the recipe with arguments:

cfg = qwen3_8b_pretrain_config(mock=True, tensor_model_parallel_size=1, micro_batch_size=4, …)

The recipe takes no arguments (returns a ConfigContainer you mutate). One-line repro against this PR's pinned image:

docker run --rm --entrypoint python3 nvcr.io/nvidia/nemo:26.04 -c "from megatron.bridge.recipes.qwen.qwen3 import qwen3_8b_pretrain_config; qwen3_8b_pretrain_config(mock=True)"
# TypeError: qwen3_8b_pretrain_config() got an unexpected keyword argument 'mock'

(verified on nemo:26.04; the bare qwen3_8b_pretrain_config() returns a ConfigContainer.) So once run.sh is repointed (above), the B300 job fails at the constructor. Please mirror h200/train.py: call it bare, then set cfg.model.tensor_model_parallel_size, cfg.train.micro_batch_size = 4, etc. While doing so, reconcile attribute names with h200 — b300 uses cfg.train.eval_interval/cfg.train.dir where h200 uses cfg.validation.eval_interval/cfg.checkpoint.save; at most one set is correct (h200's is the path I validated live).

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Batch 3/5 — Benchmark Validity

One inline comment on the results table.


| Metric | H200 (p5en) | B300 (p6-b300) | Ratio |
|--------|-------------|----------------|-------|
| **TFLOP/s per GPU** | 497 | **976** | 1.96× |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The results table hasn't been re-measured, and now contradicts the code

Every compute figure (497/976 TFLOP/s, 162K/318K tok/s, 3.23s/1.65s, MFU 0.50, ~114 GB) is byte-identical to the original submission, across four rounds of materially different configs (FFN 14336→12288, recompute strategy described three different ways, mock→real data, two images→one, NeMo 26.02→26.04). Two consequences:

  • The numbers can't be from the current code: the B300 path doesn't run (Batch 2), and the H200 config has changed since the figures were taken.
  • The recompute claim contradicts the code: the README ("Best Configuration" + Key Finding Update versions for Megatron-LM #4) attributes the ~114/~173 GB peaks and 0.50 MFU to "selective (core_attn) gradient checkpointing on both clusters," but both train.py docstrings say "no gradient checkpointing needed" and neither script sets any recompute_* config — so the headline memory/MFU is attributed to a setting the code doesn't enable.

Could you re-run both columns on the current committed config and update the table (and the 1.96×/1.97× ratios), and either enable the recompute config you describe or drop the claim so the doc matches the code? Stating the scale actually run (and distinguishing smoke-test from research-scale) would help too.

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Batch 4/5 — Data Preprocessing

The new preprocessing/ pipeline is a real improvement (real c4 data, fail-loud HF_TOKEN check, parallelized). Two related items below — they converge on one fix: run Megatron's upstream tool inside the training container.

"""Tokenize a chunk of documents into fixed-length sequences."""
chunk_id, texts, tokenizer_name, output_dir, seq_length = args_tuple

tokenizer = AutoTokenizer.from_pretrained(tokenizer_name, trust_remote_code=True)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documents are concatenated with no end-of-document token

tokenize_chunk encodes each doc with add_special_tokens=False, concatenates into a shared buffer, and chops into fixed 4096-token sequences — so unrelated documents bleed across sequence boundaries, and every chunk is recorded as its own "document" (doc_idx = [0,1,2,…]), making the boundaries synthetic. Megatron's own tools/preprocess_data.py preserves real per-document boundaries and offers --append-eod. For meaningful pre-training data you generally want an EOD token between documents (and real boundaries). Note: the .idx/.bin binary format you wrote is correct (magic/version/dtype/pointer math all match upstream) — the issue is the document/EOD semantics, not the format.

This is the strongest argument for library-first, and it's free: the image you already build ships the upstream tool at /opt/Megatron-Bridge/3rdparty/Megatron-LM/tools/preprocess_data.py (verified in nemo:26.04), which gives correct boundaries + --append-eod and won't rot if the on-disk format version bumps (the commit log shows this writer already had to be "fixed to match megatron requirements" once). I'd retire the hand-rolled tokenizer/indexer in favor of it — see the next comment for how to run it (which also resolves the runtime-pip issue).

SCRIPT_DIR=$SLURM_SUBMIT_DIR/preprocessing/
SCRIPT="${SCRIPT_DIR}/preprocess.py"

pip install -r $SCRIPT_DIR/requirements.txt

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Run preprocessing in the same container — drop the runtime venv + pip

preprocess.sh builds a venv and pip install -r requirements.txt at job time, with >= lower bounds only — a non-reproducible runtime install (breaks air-gapped, and a fresh resolve can differ run-to-run). But the image you already build has everything preprocess.py imports, so there's no need to install anything:

docker run --rm --entrypoint python3 nvcr.io/nvidia/nemo:26.04 -c "import importlib.metadata as m; print(m.version('numpy'), m.version('transformers'), m.version('datasets'))"
# 1.26.4 5.3.0 3.1.0

Tied together with the EOD comment above, the whole step becomes two in-container commands and retires both requirements.txt and the hand-rolled tokenizer/indexer (preprocess.py shrinks to just the c4 download):

IMG=/fsx/ubuntu/qwen3-8b-pretraining/containers/nemo-efa-26.04.sqsh
# 1) download c4 -> JSONL (datasets is already in the image; or reuse kubernetes/data-processing/load_dataset.py)
srun --container-image=$IMG --container-mounts=/fsx:/fsx python preprocessing/preprocess.py  # download only -> c4.jsonl
# 2) tokenize + index with the upstream tool already in the image (flags verified against its --help)
srun --container-image=$IMG --container-mounts=/fsx:/fsx \
  python /opt/Megatron-Bridge/3rdparty/Megatron-LM/tools/preprocess_data.py \
    --input /fsx/.../c4.jsonl --json-keys text \
    --tokenizer-type HuggingFaceTokenizer --tokenizer-model Qwen/Qwen3-8B \
    --append-eod --workers $(nproc) \
    --output-prefix /fsx/ubuntu/qwen3-8b-pretraining/datasets/c4_qwen3_8b

Reproducible by construction (no install step) and fixes the EOD/boundaries issue in the same move. Aside: transformers>=4.40.0 permits 4.4x, which predates Qwen3 tokenizer support — a fresh install could grab a version that can't load Qwen/Qwen3-8B, whereas the container's transformers 5.3.0 does. (If you keep a host venv anyway, pin ==.)

@KeitaW KeitaW left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review Batch 5/5 — Documentation & Config Consistency, + what looks great

Several path/version mismatches that break copy-paste or disagree across files (one inline suggestion on the clone typo):

  • Checkpoint path drift: train.py writes /fsx/ubuntu/qwen3-8b/checkpoints/… (note qwen3-8b), while README + logs use /fsx/ubuntu/qwen3-8b-pretraining/…. Unify the prefix.
  • Hardcoded repo path: both run.sh exec python /fsx/ubuntu/awsome-distributed-ai/3.test_cases/…/train.py, but the README says git clone … && cd (into CWD, not /fsx/ubuntu/). The job FileNotFoundErrors unless the user clones to exactly that path — derive it (e.g. $SLURM_SUBMIT_DIR) or document the exact location.
  • Version docstring drift: b300/train.py docstring says "NeMo 26.02"; everything else says 26.04.

Scope note (not blocking): this PR also adds MIT-0 headers to 9 files in the canonical nemo/kubernetes/ + nemo/slurm/run.py tree, unrelated to the Qwen3 feature. Adding the headers is good hygiene (the repo's most-recurring miss), but folding edits to an unrelated test case into a feature PR mixes concerns — ideally a separate PR, or a line in the description.

The bigger lever (carried from prior rounds): most of Batches 2/4/5 are the cost of hand-rolling train.py + run.sh + a Megatron indexer. The repo's canonical megatron/nemo/slurm/run.py (NeMo-Run, README §6) plus Megatron's tools/preprocess_data.py would remove the launch glue, the b300/h200 divergence, and the indexer entirely. Not a blocker, but worth weighing.

Things That Look Great

  • Single image — one Dockerfile for both GPU generations; clean EFA stack (EFA 1.47.0, GDRCopy v2.5.1 pinned). I confirmed the equivalent build compiles and imports the recipe.
  • h200/train.py uses the correct recipe API (bare call + cfg.* mutation) — exactly right, verified runnable on a live B200.
  • NCCL_SOCKET_IFNAME=^docker,lo,veth exclusion — the repo's most-flagged technical issue, correct here.
  • preprocess.py fail-loud HF_TOKEN check with a clear remediation message — good ergonomics.
  • The .idx/.bin writer is format-correct — magic, version, dtype code, field order, and the cumsum(sizes[:-1])*itemsize pointer math all match Megatron's indexed_dataset.py.

Sources

  • Recipe API (qwen3_8b_pretrain_config(mock=True)TypeError; bare call → ConfigContainer): reproduced on the pinned nvcr.io/nvidia/nemo:26.04 (verified live, 2026-06-22). Launch correctness confirmed end-to-end — a real world_size=8 training run via srun on a live 8×B200 (nemo:26.02).
  • Container deps (numpy 1.26.4, transformers 5.3.0, datasets 3.1.0) and the upstream tool path (/opt/Megatron-Bridge/3rdparty/Megatron-LM/tools/preprocess_data.py, flags per its --help): verified in nemo:26.04 (2026-06-22).
  • Megatron indexed-dataset + EOD convention: indexed_dataset.py, tools/preprocess_data.py.
  • Qwen3-8B dims: Qwen/Qwen3-8B config.jsonintermediate_size: 12288.
  • Canonical NeMo-Run launcher: megatron/nemo/slurm/run.py + README §6.

### Clone this repo and change it its directory
```bash
git clone https://github.com/awslabs/awsome-distributed-ai.git
cd awsome-distribued-ai

@KeitaW KeitaW Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README clone typo

The clone above produces awsome-distributed-ai; this cd (missing a "t") won't match.

Suggested change
cd awsome-distribued-ai
cd awsome-distributed-ai

libevent-core-2.1-7 libevent-pthreads-2.1-7 \
ethtool iproute2 pciutils curl \
&& cd /tmp \
&& curl -O https://efa-installer.amazonaws.com/aws-efa-installer-1.47.0.tar.gz \

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update to 1.48.0

&& cd / && rm -rf /tmp/* /var/lib/apt/lists/*

# Environment
ENV LD_LIBRARY_PATH="/opt/amazon/ofi-nccl/lib:/opt/amazon/efa/lib:${LD_LIBRARY_PATH}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to set LD_LIBRARY_PATH or NCCL_TUNER_PLUGIN, EFA Installer 1.4.8.0 detect which NGC containers it is and update accordingly.
This NGC /etc/shinit_v2 detect it runs on EFA and set the TUNER and NET plugin accordingly.

We should stop using LD_LIBRARY_PATH,NCCL_TUNER_PLUGIN and NCCL_NET_PLUGIN unless we need the OMPI provided by the EFA installer and we should only set LD_LIBRARY_PATH to there.

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.

3 participants