Repository navigation
Run GPT-OSS-120B on 2 nodes, and let the deployment set the perf knobs - #271
Conversation
The micro_batch_size / recompute_num_layers overrides were keyed on DEVICE alone, so MI355X always ran the shipped config (mbs 8, recompute 4). That config does not fit at 16 GPUs: TP1 x PP2 fixes model parallelism at 2, so DP is 8 there against 16 at 32 GPUs, and the distributed optimizer's 12/DP bytes/param grow by ~6 GB/GPU. Job 56918 OOM'd in the MoE grouped GEMM with 428 MiB free on a 288GB card, and expandable_segments only moved the failure one iteration later. Trigger the same overrides at NUM_GPUS <= 16 regardless of HBM size. This restores the configuration AICOMNET-141 measured on 2 x 8 GPUs on both gfx942 and gfx955; there it came from a baked-in MI300X config directory selected by PRIMUS_CONFIG_DEVICE, which this branch instead expresses as CLI overrides on top of the MI355X config. The 32-GPU MI355X path is unchanged, so the existing 4-node baseline stands. GBS 2048 still divides evenly at mbs 2 and 1, so scaleout_gbs_override remains a no-op. Co-authored-by: Cursor <cursoragent@cursor.com>
…er off run_primus() is the single launch point for every Primus model here, but nothing outside the script could reach train_iters or the profiler: they come from the shipped config and no env var was consulted. For a perf gate both matter. The GPT-OSS-120B config ships profile: true with the window on iterations 6-7, which costs 3.2x the baseline iteration on BF16 and 3.7x on FP8 at 2 nodes, and leaves 8-9 elevated while the trace is written -- with train_iters 10 that pollutes 3 of the 8 measured iterations. PRIMUS_TRAIN_ITERS and PRIMUS_DISABLE_PROFILE are read from the environment, which madengine injects from context.docker_env_vars. Both are optional and unset leaves the command byte-identical, so no other model changes. Verified inside rocm/primus:v26.4 that Primus coerces these overrides rather than assigning strings: --profile false yields profile=False of type bool, --train_iters 15 yields int 15, and an unrecognised key is reported as an unknown override instead of being silently dropped. Co-authored-by: Cursor <cursoragent@cursor.com>
Both blocks changed in this branch decide what does *not* get added to the command, which is the part a later edit silently breaks. Follows the convention of scripts/primus_train/tests: the blocks are extracted from the script and evaluated in isolation, so no GPU and no Primus are needed. run_primus() is exercised by shadowing the `bash` command with a shell function, which captures the assembled argv without reaching the launcher. The checks pin the two properties worth keeping: unset knobs leave the command byte-identical (an explicit PRIMUS_DISABLE_PROFILE=0 included), and the knobs come after the caller's arguments so the last occurrence of a key wins. The MEM_OVERRIDE checks pin that MI355X is untouched at 32 GPUs, picks the overrides up at 16, and that 192GB parts keep them at every scale. Verified by mutation: reverting the world-size clause, flipping the knob order, or disabling the profiler unconditionally each fails a check. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The new environment knobs bypass numerous direct Primus launch paths despite being described as applying to every model.
Review effort: Balanced
Findings: 1
What changed in this PR
Enables GPT-OSS-120B training on 16 GPUs and exposes deployment-controlled Primus performance settings.
Changes:
- Adds 16-GPU memory overrides.
- Adds optional iteration and profiler controls.
- Adds shell regression tests.
| File | Description |
|---|---|
primus_megatron-lm_benchmark_report.sh |
Adds runtime knobs and topology-based overrides. |
tests/test_primus_overrides.sh |
Tests argument assembly and memory overrides. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
PRIMUS_TRAIN_ITERS and PRIMUS_DISABLE_PROFILE only take effect through run_primus(). Most models in this script still invoke `primus-cli direct` themselves, and there the two variables are ignored without a word: the run is full-length where a short one was requested, and profiled where the profiler was meant to be off. Either way it looks like an ordinary successful run, and the perf number it produces is not the one the caller thinks they asked for. Routing all ~40 remaining launch sites through run_primus() is the real fix, but it is a large change to a script many models share and does not belong in a PR that adds one model. Scope the knobs explicitly instead and warn on exit when they were set but never applied. Reported by Copilot on ROCm#271. Co-authored-by: Cursor <cursoragent@cursor.com>
i-kosarev
left a comment
There was a problem hiding this comment.
LGTM, just maybe replace "run_primus() is the single launch point for every Primus model here" in description with "run_primus() is the launch point for the six models this CI measures, GPT-OSS-120B among them; the other branches call runner/primus-cli direct themselves and the knobs do not reach them. But nothing outside the script could reach either setting: …"
## What this changes Adds `gpt-oss-120b-training` at 2 nodes (16 GPUs) to the MADEngine gate as a second training workload. It runs through the same same-node A/B as llama-3.1-70b. Stacked on #12899 and based on its branch, so the diff shows only the commits below. Merge after #12899. - **Workload matrix.** The scheduled caller hardcoded `llama-3.1-70b-training`, so it becomes a matrix with `fail-fast: false`. `WORK_DIR` now includes the workload name, because matrix jobs share `run_id` and `run_attempt`. The legs run one at a time (`max-parallel: 1`): each wants an exclusive 2-node `salloc` on `meta64`, and queue time counts against its `--timeout-minutes`. The `workflow_dispatch` input becomes a `choice`. - **GPT-OSS-120B entry.** The `mad-rccl` pin from #12899 (`7a9a25e`) contains ROCm/MAD#271 (`07ecef61`), which makes GPT-OSS-120B fit at 16 GPUs and adds `PRIMUS_TRAIN_ITERS` and `PRIMUS_DISABLE_PROFILE`. The workload sets them through a new per-workload `env_vars` key, merged into the container env: 15 iterations with the profiler off. It expects BF16 and FP8, so a leg that stops running fails the gate. A workload's `env_vars` now also wins for `NCCL_SOCKET_IFNAME` and `GLOO_SOCKET_IFNAME`; neither workload sets them. - **Weekly, not nightly.** The nightly gate keeps only `llama-3.1-70b-training`. GPT-OSS-120B moves to a new `therock-test-madengine-weekly` job that runs only when `test_scope` is `all`, which the Saturday cron sets and a dispatch can request. It is a matrix of `include` entries with `max-parallel: 1`, so later weekly workloads queue behind each other rather than compete for exclusive nodes. It `needs` the nightly job for ordering only, and `!cancelled()` keeps a failed nightly from skipping it. Nodes are pinned per entry, because each workload was validated at one scale. - **Log size.** RCCL prints `ncclAllToAllv is deprecated` at WARN on every call. GPT-OSS's MoE all-to-all produced 777600 such lines per node per phase, 97% of a 180 MB node log; run 38238634668 alone left 2.1 GB under the runner account's disk quota. After the allocation, before anything reads or uploads the logs, `squash_log_spam()` keeps the first occurrence of each such line and replaces the repeats with a count: 1.1 GB to 24 MB on that run's baseline phase, with identical RCCL banners and parsed results. Real warnings are untouched. Tested in `tests/test_log_squash.py`. - **llama-4-scout dropped.** The entry predates this work and has never run on ruby, in CI or by hand. It pins `rocm/primus:v26.4`, while the MAD template keeps Scout on v26.3. It reached the dispatch choices only because that list mirrors `WORKLOAD_CONFIGS`. The gate now has exactly the two workloads that have run end to end. ## Why the profiler has to be off The shipped config profiles iterations 6-7. At 2 nodes those cost 3.2x (BF16) and 3.7x (FP8) a steady iteration, and iterations 8-9 stay elevated while the trace is written. Primus averages from iteration 3, so the profiler window would sit inside the measured window. With 15 iterations, 13 are measured, at about 65 s (BF16) and 116 s (FP8) each. ## Risk to the existing workloads - The two knobs are unset for llama-3.1-70b. I rendered its manifest to confirm: no `PRIMUS_*` variables appear, so its command is byte-identical. ## Testing and cost [Run 38238634668](https://github.com/ROCm/rocm-systems/actions/runs/38238634668) on ruby, 2x8 MI355X (job 70409): PASSED, BF16 ratio 0.9993, FP8 0.9924, with results read from `perf_primus-megatron-GPT-OSS-120B.csv` rather than the live-log fallback. Phases took 53 and 61 min, so about 2 h per A/B. `time_limit` is 4 h, which fits inside the existing `--timeout-minutes` 330. On the weekly schedule it adds about 2 h plus runner wait to the Saturday run. Peak HBM at 2 nodes was 191.6 GB, about 60 GB under the smaller nodes in `meta64`, so no exclude list is needed. That margin is arithmetic: the workload has not yet run on a small node. ## Not in this PR - A recorded baseline. The A/B gate is a ratio and needs no absolute reference. - Changing the llama iteration count. JIRA ID AICOMNET-427
(#13180) ## What this changes Adds `gpt-oss-120b-training` at 2 nodes (16 GPUs) to the MADEngine gate as a second training workload. It runs through the same same-node A/B as llama-3.1-70b. Stacked on #12899 and based on its branch, so the diff shows only the commits below. Merge after #12899. - **Workload matrix.** The scheduled caller hardcoded `llama-3.1-70b-training`, so it becomes a matrix with `fail-fast: false`. `WORK_DIR` now includes the workload name, because matrix jobs share `run_id` and `run_attempt`. The legs run one at a time (`max-parallel: 1`): each wants an exclusive 2-node `salloc` on `meta64`, and queue time counts against its `--timeout-minutes`. The `workflow_dispatch` input becomes a `choice`. - **GPT-OSS-120B entry.** The `mad-rccl` pin from #12899 (`7a9a25e`) contains ROCm/MAD#271 (`07ecef61`), which makes GPT-OSS-120B fit at 16 GPUs and adds `PRIMUS_TRAIN_ITERS` and `PRIMUS_DISABLE_PROFILE`. The workload sets them through a new per-workload `env_vars` key, merged into the container env: 15 iterations with the profiler off. It expects BF16 and FP8, so a leg that stops running fails the gate. A workload's `env_vars` now also wins for `NCCL_SOCKET_IFNAME` and `GLOO_SOCKET_IFNAME`; neither workload sets them. - **Weekly, not nightly.** The nightly gate keeps only `llama-3.1-70b-training`. GPT-OSS-120B moves to a new `therock-test-madengine-weekly` job that runs only when `test_scope` is `all`, which the Saturday cron sets and a dispatch can request. It is a matrix of `include` entries with `max-parallel: 1`, so later weekly workloads queue behind each other rather than compete for exclusive nodes. It `needs` the nightly job for ordering only, and `!cancelled()` keeps a failed nightly from skipping it. Nodes are pinned per entry, because each workload was validated at one scale. - **Log size.** RCCL prints `ncclAllToAllv is deprecated` at WARN on every call. GPT-OSS's MoE all-to-all produced 777600 such lines per node per phase, 97% of a 180 MB node log; run 38238634668 alone left 2.1 GB under the runner account's disk quota. After the allocation, before anything reads or uploads the logs, `squash_log_spam()` keeps the first occurrence of each such line and replaces the repeats with a count: 1.1 GB to 24 MB on that run's baseline phase, with identical RCCL banners and parsed results. Real warnings are untouched. Tested in `tests/test_log_squash.py`. - **llama-4-scout dropped.** The entry predates this work and has never run on ruby, in CI or by hand. It pins `rocm/primus:v26.4`, while the MAD template keeps Scout on v26.3. It reached the dispatch choices only because that list mirrors `WORKLOAD_CONFIGS`. The gate now has exactly the two workloads that have run end to end. ## Why the profiler has to be off The shipped config profiles iterations 6-7. At 2 nodes those cost 3.2x (BF16) and 3.7x (FP8) a steady iteration, and iterations 8-9 stay elevated while the trace is written. Primus averages from iteration 3, so the profiler window would sit inside the measured window. With 15 iterations, 13 are measured, at about 65 s (BF16) and 116 s (FP8) each. ## Risk to the existing workloads - The two knobs are unset for llama-3.1-70b. I rendered its manifest to confirm: no `PRIMUS_*` variables appear, so its command is byte-identical. ## Testing and cost [Run 38238634668](https://github.com/ROCm/rocm-systems/actions/runs/38238634668) on ruby, 2x8 MI355X (job 70409): PASSED, BF16 ratio 0.9993, FP8 0.9924, with results read from `perf_primus-megatron-GPT-OSS-120B.csv` rather than the live-log fallback. Phases took 53 and 61 min, so about 2 h per A/B. `time_limit` is 4 h, which fits inside the existing `--timeout-minutes` 330. On the weekly schedule it adds about 2 h plus runner wait to the Saturday run. Peak HBM at 2 nodes was 191.6 GB, about 60 GB under the smaller nodes in `meta64`, so no exclude list is needed. That margin is arithmetic: the workload has not yet run on a small node. ## Not in this PR - A recorded baseline. The A/B gate is a ratio and needs no absolute reference. - Changing the llama iteration count. JIRA ID AICOMNET-427 [rocm-systems] ROCm/rocm-systems#13180 (commit f5c8b3a)

What this is for
Widening the RCCL CI to a second training workload. GPT-OSS-120B is the
candidate, and two things in
primus_megatron-lm_benchmark_report.shstoodbetween it and a usable perf gate.
1. GPT-OSS-120B could not run at 16 GPUs
The
micro_batch_size/recompute_num_layersoverrides were keyed onDEVICEalone, so MI355X always ran the shipped config (mbs 8,
recompute_num_layers4).That config does not fit at the 16-GPU topology minimum.
TP1 x PP2fixes modelparallelism at 2, so DP is 8 at 16 GPUs against 16 at 32, and the distributed
optimizer's
12/DPbytes/param cost about 6 GB/GPU more. Measured, not inferred:Megatron's own theoretical report gives 377448 MB of weight+optimizer state at
DP16 against 419386 MB at DP8, with the activation term byte-identical, and the
ratio is exactly
(6 + 12/8) / (6 + 12/16).Two jobs confirm the failure mode. One OOM'd in the MoE grouped GEMM
(
grouped_linear.py:206viaexperts.py:386, inside_checkpointed_forward)with 428 MiB free on a 288GB card; a retry with
PYTORCH_CUDA_ALLOC_CONF=expandable_segments:Truebought exactly one iterationand then died in the same place.
The fix triggers the existing overrides at
NUM_GPUS <= 16regardless of HBMsize. That restores the configuration that AICOMNET-141 measured on 2 x 8 GPUs
on both gfx942 and gfx955, where it came from a baked-in MI300X config directory
selected by
PRIMUS_CONFIG_DEVICE; this branch expresses the same end state asCLI overrides on top of the MI355X config.
Verified end to end on 2 x 8 MI355X with stock
rocm/primus:v26.4, bothprecisions, 10/10 iterations, loss decreasing:
BF16 lands within 3.3% of the independent AICOMNET-141 measurement on another
cluster of the same architecture.
The 32-GPU MI355X path is unchanged, so existing 4-node baselines stand.
global_batch_size2048 still divides evenly at mbs 2 and 1, soscaleout_gbs_overrideremains a no-op.2.
train_itersand the profiler were unreachable from outsiderun_primus()is the launch point for six models: GPT-OSS-120B,Llama-3.1-8B/70B/405B, Llama-4-Scout-17B-16E and Kimi-K2-Thinking, which
covers every Primus workload the RCCL CI measures. The other branches call
runner/primus-cli directthemselves, the knobs do not reach them, and thescript prints a
[WARN]when they are set for such a model. But nothingoutside the script could reach either setting: they come from the shipped
config, no env var was consulted, and
run.shparses only--model_repo.This matters for a perf gate. The GPT-OSS-120B config ships
profile: truewiththe window on iterations 6-7. At 2 nodes that costs 3.2x the baseline iteration
on BF16 and 3.7x on FP8, and 8-9 stay elevated while the trace is written — with
train_iters10 that is 3 of the 8 measured iterations:PRIMUS_TRAIN_ITERSandPRIMUS_DISABLE_PROFILEare now read from theenvironment, which madengine injects from
context.docker_env_vars. Both areoptional, and unset leaves the command byte-identical, so no other model changes
behaviour.
Checked inside
rocm/primus:v26.4before relying on it: Primus coerces theseoverrides rather than assigning strings, so
--profile falseyieldsprofile=Falseof typebooland not the truthy string"false";--train_iters 15yieldsint; and an unrecognised key is reported as anunknown override instead of being silently dropped.
3. Tests
scripts/primus_megatron-lm/tests/test_primus_overrides.sh, following theconvention of
scripts/primus_train/tests: the two blocks are extracted fromthe script and evaluated in isolation, so the test needs neither a GPU nor
Primus.
run_primus()is exercised by shadowing thebashcommand with a shellfunction, which captures the assembled argv without reaching the launcher.
It pins the properties that a later edit would silently break — unset knobs
leave the command untouched, the knobs come last so the final occurrence of a
key wins, MI355X is untouched at 32 GPUs and picks the overrides up at 16, and
192GB parts keep them at every scale. Confirmed to actually fail on regression
by mutating the script three ways: reverting the world-size clause, flipping the
knob order, and disabling the profiler unconditionally.
Not in this PR
The numbers above were measured with the profiler still on, so they are proof
that the run works, not a CI baseline. A baseline wants a run with
PRIMUS_DISABLE_PROFILE=1andPRIMUS_TRAIN_ITERSaround 15.