Repository navigation
[PyTorch] Type the grouped wgrad from main_grad for distributed weights - #3397
fanshiqing wants to merge 4 commits into
Conversation
c9a1f5a routed distributed weights into the no-fusion branch, which allocates at the compute dtype -- so under fp32 grad accumulation the reduce-scatter rounded to BF16 on every rank: wgrad alloc fp32 ---> reduce fp32 ---> accum fp32 <- this fix (was compute dtype ---> compute dtype ---> fp32) Two dispatchers beside finalize_weight_grads: weight_grad_buffers() GEMM writes into the implementer's buffer (no per-backward wgrad allocation) weight_grad_dtype() fallback for the single-grouped branches Signed-off-by: Shiqing Fan <shiqingf@nvidia.com>
[Medium impact] The PR appears safe to merge; no new actionable issue remains. SummaryThis PR makes grouped weight gradients use
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Original weight group] --> B{Distributed leader with main_grad?}
B -->|Yes| C[Allocate wgrad at main_grad dtype]
B -->|No| D[Allocate wgrad at compute dtype]
C --> E[GEMM writes weight gradients]
D --> E
E --> F[Finalize distributed gradients]
Reviews (3) · Last reviewed commit: "[PyTorch] Drop weight_grad_buffers; type..." · Reviewed by Greptile |
|
/te-ci pytorch L1 |
| @@ -656,7 +575,7 @@ def _compute_grad_params( | |||
| shapes=[weight_shape] * num_groups, | |||
| quantizer=None, | |||
| device=device, | |||
| dtype=dtype, | |||
| dtype=weight_grad_dtype(weights, dtype), | |||
There was a problem hiding this comment.
I dont understand how this will help, since we will never enter this else condition of allocating using torch.empty, if main_grad is attached to the parameter.
There was a problem hiding this comment.
Actually we will, if accumulate_into_main_grad is False, but why are we attaching main_grad to parameter in that case in Megatron?
There was a problem hiding this comment.
Hi @vthumbe1503 , for you first question why this line's change is required: we need to follow main_grad's dtype instead of the compute_dtype for this scratch_buffer allocation.
Details:
- The bug: for a GTP-sharded (distributed) weight whose scratch wgrad buffer isn't fused into main_grad, the buffer was allocated in the forward/backward compute dtype (e.g. bf16) — not in main_grad's dtype. That buffer is what gets reduced across the weight's GTP group before being accumulated into main_grad.
- Result: whenever main_grad was fp32 (fp32 grad accumulation enabled by set
--grad-reduce-in-fp32from MLM for a best accuracy loss baseline), the reduce-scatter still ran in bf16 — every rank's contribution got rounded to bf16 before it ever reached the fp32 accumulator. fp32 grad accumulation was silently defeated for GTP-sharded weights, with no error or warning. - The fix: weight_grad_dtype(weights, dtype) returns main_grad.dtype instead of the plain compute dtype whenever the weight is distributed and main_grad is already attached, so the scratch buffer — and therefore the reduction — happens at the same precision main_grad actually uses. Falls back to compute_dtype for plain weights or when main_grad isn't attached yet.
There was a problem hiding this comment.
if accumulate_into_main_grad is False, but why are we attaching main_grad to parameter in that case in Megatron
for MLM:
.main_gradattachment is unconditional.accumulate_into_main_gradis a separate, opt-in performance flag.
There was a problem hiding this comment.
Thanks for the explaination @fanshiqing! Why is this problem not present for discrete weights? Is it because in case discrete weights, we always write directly into the main_grad buffer?
There was a problem hiding this comment.
Hi @vthumbe1503 , Mostly yes — what matters is whether the reduction runs before or after the wgrad lands in main_grad:
- Regular weights: the GEMM writes into the fp32 main_grad (or DDP adds .grad into it), and DDP reduces later, from main_grad, so the sum is in fp32.
- Distributed weights: main_grad is only the local shard, but the GEMM produces the full unsharded wgrad. It must be reduce-scattered first, from a scratch buffer.
- The bug: that scratch buffer used the compute dtype, so the RS summed in BF16 even under fp32 grad accumulation.
- The fix: the scratch buffer now takes main_grad's dtype, so the RS runs in fp32.
Signed-off-by: Shiqing Fan <shiqingf@nvidia.com>
| @@ -101,6 +108,47 @@ def materialize_weight_for_backward(weights: Any) -> List[Any]: | |||
| return list(weights) | |||
|
|
|||
|
|
|||
| def weight_grad_buffers( | |||
There was a problem hiding this comment.
can we get rid of this helper function? doesn't feel necessary
| A distributed weight brings its own, which skips this allocation and carries ``main_grad``'s | ||
| dtype by construction; anything else gets fresh scratch in the compute dtype. | ||
| """ | ||
| if not isinstance(weights, (list, tuple)): |
There was a problem hiding this comment.
this doesn't seem to support single weight? TE as a computing library supports single weight for all, so it's orthogonal to if a certain feature in Mcore doesn't consider single weight yet
There was a problem hiding this comment.
It does handle a single weight: a bare tensor is wrapped, and single_grouped_weight passes [self.weight], whose main_grad dtype is used.
| return list(packed) | ||
|
|
||
|
|
||
| def weight_grad_dtype(weights: Any, compute_dtype: torch.dtype) -> torch.dtype: |
There was a problem hiding this comment.
we can also get rid of this helper?
There was a problem hiding this comment.
No, I'd keep this one. It's the actual fix, and without it the same condition gets copied into 8 places
- It's the whole fix now that weight_grad_buffers is gone.
- Inlining it would copy the same condition into 8 call sites.
- It keeps Megatron's main_grad convention in distributed_weight.py, not spread across TE's ops code.
- It handles a missing main_grad by falling back to the compute dtype. Inlined, each site would need that guard or could raise AttributeError.
…_reduce_in_fp32 Signed-off-by: Shiqing Fan <shiqingf@nvidia.com>
…a weight_grad_dtype only Signed-off-by: Shiqing Fan <shiqingf@nvidia.com>
Description
c9a1f5a routed distributed weights into the no-fusion branch, which allocates at the compute dtype -- so under fp32 grad accumulation the reduce-scatter rounded to BF16 on every rank:
Two dispatchers beside finalize_weight_grads:
weight_grad_buffers()GEMM writes into the implementer's buffer (no per-backward wgrad allocation)weight_grad_dtype()fallback for the single-grouped branchesc9a1f5a9 routed distributed weights into the no-fusion branch, which allocates at the compute dtype -- so under fp32 grad accumulation the reduce-scatter rounded to BF16 on every rank:Two dispatchers beside finalize_weight_grads:
weight_grad_buffers()GEMM writes into the implementer's buffer (no per-backward wgrad allocation)weight_grad_dtype()fallback for the single-grouped branchesType of change
Changes
Please list the changes introduced in this PR:
Checklist: