Explainer for NVIDIA-NeMo/RL #4512 (SFT processor config and GTP data placement). All permalinks pin the PR head 339e2020; Megatron-LM at b4d72b79.
Three pages on this PR, one question each. This one: which GPUs get work.
Megatron numbers GPUs TP fastest, then CP, then GTP, then DP, then PP
(parallel_state.py:602-618), and its data-parallel rank already counts GTP GPUs as separate lanes by default
(parallel_state.py:2225-2231).
So the old 4-D layout's data_parallel index for GPU (d, g) is d·G + g, which is exactly the lane the SFT loader already asks Megatron for
(sft_worker.py:82) and the number the PR now computes by hand
(topology.py:89).
The PR's fifth axis gives the same lanes for SFT v2 training, but every other call still splits only data_parallel.
A lane here is a group of GPUs that trains on its own batch. With DP = 2 and GTP = 3 there are 6 lanes, and each lane is 4 GPUs (the TP group). Layout A gives each lane one number directly. Layout B gives it two coordinates, and the PR combines them into one number. The figure lines the numbering up per lane.
Why A already matches: Megatron numbers GPUs TP fastest, then GTP, then DP, so GPU r = t + 4·(g + 3·d).
Reshaping 24 GPUs as (pp=1, -1, cp=1, tp=4) leaves r // 4 = 3·d + g on the middle axis, which is the same lane number B computes by hand.
So splitting the producer's batches along A's data_parallel axis already sends batch k to lane k.
get_data_parallel_rank() and get_data_parallel_world_size(), which count GTP by default, so the answers are 0..5 of 6.
Energon reads shard k of 6 from that (sft_worker.py:82-83, unchanged by the PR).data_parallel (6 entries)
(tq_policy.py:801-803, base).
The PR: the same list reshaped into [[0,1,2],[3,4,5]] and split on (dp, gtp)
(tq_policy.py:763-770). Same GPUs, same batches.In layout B the other calls see gtp_weight_remat as an axis they neither split nor copy, so only index 0 runs
(worker_groups.py:1188-1196). The PR's only guard is on SFT v2 training
(tq_policy.py:813-818).
Where the fix lands: RL only. Keep the 4-D layout in Policy.__init__;
delete GTP_WEIGHT_REMAT_AXIS (named_sharding.py), the GTP loop in
topology.py, and the GTP nesting and guard in
tq_policy.py. Keep the GTP setup calls in setup.py.
No Megatron change: Megatron's default data-parallel group already includes GTP.
Nothing below is implemented. With GTP = 1 this is exactly the code before the PR.
@@ lm_policy.py L356-L370 · Policy.__init__ - # MCore rank order is TP, CP, GTP, DP, PP, with TP varying fastest. - shape = [pp_size, -1, cp_size, tp_size] - axes = [ - "pipeline_parallel", - "data_parallel", - "context_parallel", - "tensor_parallel", - ] - if gtp_size > 1: - shape.insert(2, gtp_size) - axes.insert(2, GTP_WEIGHT_REMAT_AXIS) - self.sharding_annotations = NamedSharding( - layout=np.arange(cluster.world_size()).reshape(shape), - names=axes, - ) + # MCore's rank order is TP, CP, GTP, DP, PP (TP fastest), and its + # data-parallel rank includes GTP peers (with_gtp_remat=True by default). + # So GTP folds into data_parallel: index = dp * gtp_size + gtp. + self.sharding_annotations = NamedSharding( + layout=np.arange(cluster.world_size()).reshape( + pp_size, # PP + -1, # DP x GTP + cp_size, # CP + tp_size, # TP + ), + names=["pipeline_parallel", "data_parallel", "context_parallel", "tensor_parallel"], + )
One unit test should pin the folding: build the 4-D layout and check that GPU (d, g) lands at data_parallel index d·G + g.
So what does the author do instead of the GTP axis? Nothing new. Nobody ever writes DP, not 2 and not 6. The only GTP input is one setting, the shard count (page 2). Only the two places that care how weights are split read it. Everything that moves data uses the flat lane number 0..5, which both sides already compute on their own. Same example: 24 GPUs, TP 4, shard count 12.
| Piece | Needs G? | After the fix |
|---|---|---|
| Config (YAML) | input | TP 4 and shard count 12. No DP setting exists. |
| Megatron setup | yes | Bridge: GTP = 12 // 4 = 3 (transformer_config.py), passed to Megatron (initialize.py), which works out DP = 24 // 12 = 2. Already works; no change. |
| Driver world-size check | yes | GTP = shard count // TP, from the same setting (page 2). The only place in NeMo-RL that needs G. |
| Driver layout | no | reshape(PP, -1, CP, TP): the -1 becomes 24 / 4 = 6 lanes. |
| Loader plan | no | Back to the code before the PR: one loader per lane 0..5 (topology.py, base). |
| Energon loader | no | Already asks Megatron: lane 0..5 out of 6 (sft_worker.py). Unchanged. |
| Dispatch | no | Splits on data_parallel only, like every other call (tq_policy.py, base). |
Driver and Megatron count the same 6 lanes without talking to each other. The driver gets 24 / (PP·CP·TP) = 6. Megatron's data-parallel size includes GTP by default, so it gets DP × GTP = 2 × 3 = 6. The figure at the top shows they also number them the same way.
🔴 review comment, lm_policy.py:356-370