Skip to content

feat(rl): orchestrator.pad_to_seq_len, and pad with real tokens to avoid MoE hot spots - #4184

Merged
garrett361 merged 6 commits into
mainfrom
feat/rl-pad-to-seq-len
Oct 20, 2026
Merged

garrett361 merged 6 commits into
mainfrom
feat/rl-pad-to-seq-len

Conversation

@garrett361

@garrett361 garrett361 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

This PR gives RL the same pad_to_seq_len flag that SFT supports (#4160). The flag pads every packed row to seq_len, so torch.compile and DeepGEMM stop recompiling on new lengths. Full-length padding makes an existing MoE problem worse (identical pad tokens all route to the same experts), so this PR also changes RL and SFT to pad with the row's own tokens instead of a constant id.

We ran FP8 e2e checks on one H200 node per run, each with an empty DeepGEMM cache (harness in a31524e):

Run Dynamo recompiles per rank, total (after step 1) DeepGEMM compiles, total (after step 1)
SFT GLM-4.5-Air 4L --data.pad-to-seq-len 1 (0) 22 (0)
SFT GLM-4.5-Air 4L 3 (0) 34 (7)
RL Qwen3-30B-A3B + replay --orchestrator.pad-to-seq-len 0 (0) 17 (0)
RL Qwen3-30B-A3B + replay 1 (0) 54 (28)

The flag only makes the row length static: the number of micro batches per step, max_seqlen, and multimodal shapes still vary. The throughput and MFU metrics count padding, so they are not comparable with and without the flag.

🤖 Generated with Claude Code

garrett361 and others added 6 commits October 20, 2026 20:56
Mirrors SFT's data.pad_to_seq_len (#4160): pad every packed micro batch to
seq_len so rows have one static shape and torch.compile / DeepGEMM JIT stop
recompiling on new lengths.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Identical pad tokens route to the same experts, so the EP rank hosting them
takes the whole pad run, and router replay padded with zero bytes sent every
pad slot to expert 0. Text rows in RL and SFT now pad by repeating their own
tokens, and RL's routed-experts payload repeats its real routes. Multimodal
rows keep a constant pad id.

Co-authored-by: Mika Senghaas <mika@primeintellect.ai>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The first README misreported the flag-off SFT median step time. Report
compile counts only, since short runs inflate step-time differences.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@samsja samsja left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@garrett361
garrett361 merged commit 8f8fc98 into main Oct 20, 2026
19 checks passed
@garrett361
garrett361 deleted the feat/rl-pad-to-seq-len branch October 20, 2026 21:55

EazyReal commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Thanks for getting this landed. I appreciate the broader coverage here—SFT, pad_to_seq_len, and H200 end-to-end validation—and recognize that repeating real tokens and replay routes is a different implementation from the balanced assignments in #3350.

I’ve closed #3350 as superseded for the original zero-filled replay-padding hotspot. I’d still like us to resolve attribution for the earlier contribution: that PR identified the expert-0 hotspot and provided a proposed fix before this PR was opened.

Could we work out an appropriate way to give that work coauthor credit? That is the recognition I’m asking for; the current “Related: #3350” reference doesn’t fully convey the original diagnosis and implementation. I’m not claiming authorship of the distinct implementation here.

I understand this has already merged and that changing the existing commit’s trailers would involve rewriting history. I’m happy to work with you on a practical way to make the attribution right. If coauthor credit isn’t appropriate under the project’s contribution policy, could you explain how that policy applies here so we can resolve this with a shared understanding?

Thanks—I’d like to get this sorted out constructively and keep contributing.

nxphi47 added a commit that referenced this pull request Nov 2, 2026
`max_vio` measures expert-level imbalance: the busiest expert's tokens
over the mean, with the top_k busiest experts dropped so that padding
experts don't dominate it (#1332). With expert parallelism an MoE layer
waits for the EP rank that receives the most routes, and `max_vio`
tracks that poorly: hot experts that share a rank add up, hot experts on
different ranks don't, and the dropped experts are often the ones that
make a rank busy. This PR logs the rank-level imbalance next to
`max_vio`.

## What it computes

`get_global_moe_stats` already sums each micro step's per-layer expert
counts over the EP group. From those counts, per micro step and layer:

- **rank imbalance**: routes received by the busiest EP rank over the
mean rank. EP rank r holds experts `[r*E/ep, (r+1)*E/ep)`, so the counts
are viewed as `[..., ep, E/ep]` and summed. Padding routes count, since
they run through the expert GEMMs like any other token.

Every rank of an EP group holds the same values. A layer waits for the
busiest rank of its EP group and, through its FSDP collectives, for the
slowest EP group. So the logged value is the max over DP groups at each
(micro step, layer), and the DP mean is logged next to it. Both ride in
the existing all-reduces: the DP max is stacked into the `max_vio` MAX
all-reduce and the DP mean into the `routing_confidence` SUM all-reduce.
There is no new buffer, recording path or collective, and no host sync
per micro step. Per-DP-group values are not logged, since that would
need a gather.

Logged in SFT and RL:

- `ep_imbalance/rank/{mean,max}`: max over DP groups, then the mean and
max over the step's layers and micro steps.
- `ep_imbalance/rank_dp_mean/{mean,max}`: the same with the DP mean.
- The console step line shows `EP Imbalance` (`ep_imbalance/rank/mean`).

## Measurement: SFT on DeepSeek-V4-Base

The run is DeepSeek-V4-Flash-Base in bf16: 43 MoE layers (the first 3
hash-routed), 256 experts, top-6. It trains on long-context chat data
packed into 524288-token rows. With cp = 8 and ep = 8, each EP group is
one CP group and trains one row per micro step, 64K tokens per rank. It
ran on 48 H200s (6 EP groups) with 4 micro steps per rank for 60 steps,
which gives 61,920 (EP group, micro step, layer) values.

| | mean | p50 | p90 | p99 | max |
|---|---|---|---|---|---|
| rank imbalance per EP group, micro step and layer | 1.30 | 1.27 | 1.50
| 1.72 | 3.38 |
| … hash-routed layers 0-2 | 1.15 | 1.15 | 1.18 | 1.28 | 2.04 |
| … learned layers 3-42 | 1.31 | 1.28 | 1.51 | 1.73 | 3.38 |
| `max_vio` per layer, same counts | 2.94 | 2.87 | 4.05 | 5.36 | 22.8 |
| `ep_imbalance/rank/mean` per step | 1.39 | 1.39 | 1.41 | 1.48 | 1.54 |
| `ep_imbalance/rank/max` per step | 1.90 | 1.87 | 2.01 | 2.71 | 3.38 |
| `ep_imbalance/rank_dp_mean/mean` per step | 1.30 | 1.30 | 1.31 | 1.32
| 1.34 |
| `max_vio/mean` per step | 5.79 | 5.58 | 6.47 | 8.79 | 9.73 |

- **Step time tracks the new metric more closely than `max_vio`
(suggestive, not causal).** Over steps 2-60, forward plus backward time
ranged from 185 to 208 s. It correlates r = 0.83 with
`ep_imbalance/rank/mean` and r = 0.65 with `max_vio/mean`. On the 32
steps with more than 99.8% of tokens real (no padding spikes), the
correlations are 0.73 and 0.59.
- **`max_vio` and rank imbalance are loosely related.** Per (layer,
micro step, EP group) r = 0.35. The learned-layer values with `max_vio`
between 2 and 5 (94% of them) span 1.10 to 1.72 in rank imbalance (p1 to
p99). The layer with the highest mean `max_vio` (4.5) has rank imbalance
1.23. The most imbalanced layer (1.65) has `max_vio` 3.4.
- **Per layer:** the hash-routed layers sit at 1.12-1.16 and the learned
layers at 1.16-1.65. In the hottest learned layers (1.45-1.65), the same
rank is the busiest in 97-100% of micro steps. Most of the imbalance is
static: a fixed per-layer permutation of experts over ranks, fit on
steps 1-30, brings steps 31-60 from 1.30 to 1.11.
- **The max over DP groups matters:** 1.39 against 1.30 for the DP mean,
with only 6 groups.
- **The tail comes from the run's older packing, not from the metric.**
That trainer revision padded rows with token 0, and the maxima (rank
imbalance 3.38, `max_vio` 22.8, and the slowest step) come from a single
row that was 57% padding. At head, packs pad by repeating their own
tokens (#4184), so that tail should not reproduce; the mean-level
numbers (1.30 per value, 1.39 per-step DP max) are properties of the
routing and carry over.

The counts were recorded with this change applied to the earlier trainer
revision that the run's packing needs. Every (row, layer) counts exactly
row length × 6 routes, so activation-checkpoint recompute does not
double count.

## Testing

- A manual 4-rank gloo run of `get_global_moe_stats` at head (two EP
groups of two, random counts; not committed) matches a brute-force
reference for `ep_imbalance/rank` (max over DP groups) and
`ep_imbalance/rank_dp_mean`, and `max_vio` and `routing_confidence`
match the previous formulas.
- SFT on 8 H200s with a small GLM MoE (8 experts, top-4, fake data, ep =
4, so two EP groups), on the previous revision: every key is logged, and
the DP mean comes out below the DP max (1.38 vs 1.48).
- The measurement above, with this change applied to an earlier trainer
revision that the run's packing needed.
- `ruff check`, `ruff format --check`.
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