Repository navigation
Conversation
[Medium impact] The change since the previous review appears safe to merge. SummaryThe PR makes delayed gradients add to existing gradients for shared parameters and microbatches.
Since the previous review, only Reviews (11) · Last reviewed commit: "Iterate the non-tied test's module pairs..." · Reviewed by Greptile |
| # module overwrite the contributions of the earlier ones, so | ||
| # accumulate instead: initialise on the first contribution of the | ||
| # step and add on every later one. | ||
| if weight_tensor.grad is None: |
There was a problem hiding this comment.
Is it safe? There's also zero_grad(set_to_none=False)
There was a problem hiding this comment.
Yes. set_to_none=False leaves a zeroed tensor where set_to_none=True leaves None, so the first contribution of the step takes the add path and lands on the same value; nothing in the branch needs grad to be None to start a step. Said so in the comment (7ed69a4).
It also turned out not to be pinned: the test ran both settings but only asserted second != first, which cannot show pollution, because a first-step value carried into the second still differs from the first. It now compares the second step's grad against a fresh model on the same weights and input, bit-exactly.
wujingyue
left a comment
There was a problem hiding this comment.
Core changes look good! Tests need a lot of cleanup in addition to my comments.
| SIZE = 16 | ||
| BATCH = 2 | ||
| DTYPE = torch.bfloat16 | ||
| DEVICE = "cuda" |
There was a problem hiding this comment.
Should go with the test rather than global.
There was a problem hiding this comment.
Moved (0f419b2): the global is gone. _build and _x take device="cuda" as a default, the micro-batch test uses _x(4), and the oracle takes the device from model[0].weight.device.
| # differ by a BF16 ULP. That is not something the fix promises to remove, so the | ||
| # tolerance is declared here and applies to the multi-microbatch case only; | ||
| # every single-microbatch comparison below stays bit-exact. | ||
| MULTI_MICROBATCH_RTOL = 1e-2 |
There was a problem hiding this comment.
Moved into test_multiple_microbatches_accumulate, the only test that needs it.
| MULTI_MICROBATCH_ATOL = 1e-2 | ||
|
|
||
|
|
||
| def _tie(model, bias): |
There was a problem hiding this comment.
| def _tie(model, bias): | |
| def _tie(model, *, bias): |
otherwise it's hard to understand what _tie(model, True) means
There was a problem hiding this comment.
Done: _tie(model, *, bias).
| return model | ||
|
|
||
|
|
||
| def _build(delay, tied, bias=False): |
There was a problem hiding this comment.
Done: _build(delay, *, bias=False).
| fuse_wgrad_accumulation=False, | ||
| ), | ||
| ) | ||
| return _tie(model, bias) if tied else model |
There was a problem hiding this comment.
This should be separated out of _build so SRP
There was a problem hiding this comment.
Done: _build now only builds. Tying happens at the call sites, where each test says whether it wants a tied pair or two independent ones.
| return _tie(model, bias) if tied else model | ||
|
|
||
|
|
||
| def _run(model, x, microbatches=1, delay=False, order=(0, 1)): |
There was a problem hiding this comment.
Renamed to backward_dw_order and documented: it is the module order used to drain the delayed wgrad queues, which decides which tied module pops its delayed wgrad first.
0f419b2 to
6681062
Compare
|
Want your agent to iterate on Greptile's feedback? Try greploops. |
backward_dw() assigned the popped delayed wgrad to Parameter.grad. When two modules share one Parameter, each pops its own contribution and the later one silently replaced the earlier, so the shared gradient came out too small -- on the issue's own 16x16 / batch=2 BF16 repro it was 0.6015x the regular path, with 256/256 elements differing. The bias takes the same pop path and hits the same bug, with an extra twist: the second module's bgrad is numerically ~0, so it wiped out a contribution the first module had already written correctly (1.0984 delivered vs 33.0859 expected). Initialise on the first contribution of the step and add on every later one, for both the weight and the bias. dtype/shape/buffer-alias contracts are unchanged, and the fused wgrad-accumulation branch is untouched. tests/pytorch/test_tied_delayed_wgrad.py covers the matrix: tied and non-tied, both backward_dw() orders, multiple microbatches, two optimizer steps with set_to_none True/False, the bias case, and parity with a plain nn.Linear oracle. It uses only the public te.Linear API so it cannot be satisfied by a shared implementation detail of the fix. Without the fix it fails 5 and passes 3; with it, 8 pass. Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
| return model | ||
|
|
||
|
|
||
| def _build(delay, *, bias=False, device="cuda"): |
There was a problem hiding this comment.
| def _build(delay, *, bias=False, device="cuda"): | |
| def _build(*, delay, bias, device="cuda"): |
There was a problem hiding this comment.
Went with your follow-up suggestion in the next thread instead; see the reply there.
| return model | ||
|
|
||
|
|
||
| def _build(delay, *, bias=False, device="cuda"): |
There was a problem hiding this comment.
| def _build(delay, *, bias=False, device="cuda"): | |
| def _build_two_linears(delay, *, bias=False, device="cuda"): |
There was a problem hiding this comment.
Sorry for the very late replies on this round. Applied in c5cf151: the helper is now _build_two_linears(delay, *, bias=False, device="cuda") and only builds the two layers.
| SIZE = 16 | ||
| BATCH = 2 | ||
| DTYPE = torch.bfloat16 |
There was a problem hiding this comment.
There was a problem hiding this comment.
Removed in c5cf151. The SIZE/BATCH/DTYPE constants are gone, and each test spells out its own sizes and torch.bfloat16.
| ``backward_dw_order`` is the module order used for draining, which decides which tied | ||
| module pops its delayed wgrad first. | ||
| """ | ||
| for xb in torch.chunk(x, microbatches, dim=0): |
There was a problem hiding this comment.
| model[i].backward_dw() | ||
|
|
||
|
|
||
| def _x(batch=BATCH, device="cuda"): |
There was a problem hiding this comment.
There was a problem hiding this comment.
Removed in c5cf151. Each test builds its input with an inline torch.randn(...).
|
|
||
|
|
||
| @pytest.fixture(autouse=True) | ||
| def _seed(): |
There was a problem hiding this comment.
There was a problem hiding this comment.
Removed in c5cf151. Each test calls torch.manual_seed(9) itself.
| xd = _x() | ||
| _run(delayed, xd, delay=True) | ||
| _run(regular, xd.detach().clone().requires_grad_(True)) | ||
| for i in (0, 1): |
There was a problem hiding this comment.
Done in d14c2d2: test_non_tied_linear_gradients now iterates zip(delayed, regular) instead of indexing both models.
6681062 to
a1ac680
Compare
Review question: is the `grad is None` first-contribution test safe when the optimizer was built with `zero_grad(set_to_none=False)`? It is: that setting leaves a zeroed tensor where `set_to_none=True` leaves `None`, so the first contribution takes the add path and lands on the same value, and nothing in the branch depends on `grad` being `None`. Say so where the branch is, and pin it in the test. `test_two_optimizer_steps_do_not_pollute_each_other` already ran both settings, but `second != first` cannot show pollution - a first-step value carried into the second still differs from the first. It now compares the second step's gradient against a fresh model on the same weights and input, bit-exactly. Verified with the new test over an out-of-tree copy of an installed TransformerEngine: 5 failed, 3 passed against the unfixed `backward_dw` (the same five this PR describes), and 8 passed with this PR's `backward_dw` in place. Signed-off-by: 0z5a <192209249+0z5a@users.noreply.github.com> Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
- `_tie(model, *, bias)` and `_build(delay, *, bias=False, device="cuda")`: the boolean arguments are keyword-only, so a call site says what it means. - `_build` only builds. Tying moved out to the call sites, which is where a test decides whether it wants a tied pair or two independent ones. - `order` is now `backward_dw_order`, and the docstring says what it orders. - The module-level `DEVICE` and tolerance globals are gone: the device is a default on the two helpers that need it, and the multi-microbatch tolerance is local to the test that needs it. Signed-off-by: 0z5a <192209249+0z5a@users.noreply.github.com> Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
a1ac680 to
557a35b
Compare
denera
left a comment
There was a problem hiding this comment.
Thank you for the PR!
In addition to @wujingyue's review comments, I would request two additional changes, one trivial but the other a critical omission.
-
[TRIVIAL] The new test will not run in our CI. I would recommend moving the cases into
test_numerics.pyOR addingtest_tied_delayed_wgrad.pyinto theqa/L0_pytorch_unittest/test.shscript (this is what our CI pipeline actually launches). -
[CRITICAL] Your fix covers only one of the four delayed-wgrad implementations.
te.LayerNormMLP.backward_dw,te.GroupedLinear.backward_dwandte.ops.GroupedLinear.backward_dwall remain unchanged and still drop all but the last wgrad contribution. These should be addressed in this PR as well.
Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
There was a problem hiding this comment.
@0z5a I just manually tested this PR and I'm seeing 24 of the 50 test cases added in this PR failing on SM100. Could you please make sure that all the tests pass locally on your end before we launch the CI pipeline? Thanks!
Additionally, there's a CUDA-graph hazard here whenever optimizer.zero_grad() is invoked.
make_graphed_callables()does a few eager warmup passes before capture. After these warmups,param.gradholds a tensorGfrom the normal caching allocator, not the graph's private pool.- Graph capture on
backward_dw()records kernels, not Python assignments. It captures the WGRAD GEMM writing into a fresh bufferSfrom the graph's pool, and theG.add_(S)for the accumulation with the raw address for tensorGbaked in at capture time (NOTE: This only affectsfuse_wgrad_accumulation=False). optimizer.zero_grad()by default setsparam.grad = None. This drops the last reference toGand the normal caching allocator frees the block.- Next time the graph runs, the captured raw address for tensor
Gis no longer valid. If the caching allocator has handed that address block to something else, the WGRAD accumulation corrupts it on graph replay.
Fixing this requires setting the param.grad = None for the parameters of modules that require backward_dw() right before the two bwd_dw_graph captures in lines 809 and 1008 for transformer_engine/pytorch/graph.py.
I would also recommend adding a test for backward_dw() capture under test_cuda_graphs.py. Something simple like te.Linear with delay_wgrad_compute=True graphed and compared against eager would reveal the above graph capture issue in the future.
Capture deferred contributions in the graph pool and accumulate them into caller-owned gradients before delayed hooks. Cover both capture orders, default and explicit gradient resets, tied weights, microbatches, bias, retained gradients, and fused main_grad with independent PyTorch references. Native GPU validation is pending on the coordinated test machine. Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
With delay_wgrad_compute=True and fuse_wgrad_accumulation=False, GroupedLinear returned the still-empty wgrad buffer to autograd from the main backward pass. Autograd stored it as (or added it into) .grad before backward_dw() had written it, so accumulating the delayed result on top read uninitialized memory. Return None instead, as Linear does, and let backward_dw() own .grad. The tied-gradient tests compared multi-term BF16 sums from the delayed and regular paths with a fixed 1e-2 tolerance. The two paths add the same terms in a different order, so allow a few BF16 roundings at the gradient's magnitude. The graphed microbatch test compared against a PyTorch FP32 reference bitwise, but TE runs FP32 GEMMs in TF32; compare graph replay against the same TE modules run eagerly instead. Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
|
@denera Sorry for the slow follow-up, and thank you for running the tests. You were right: the failures are not specific to SM100. Applying this PR's head ( There were three causes:
Results on the new head
The CUDA-graph hazard you described ( I don't have SM100 access right now, so I'd appreciate it if you could rerun there before CI. pylint (3.3.1, repo rc) is clean on the touched files. |
Pair each delayed Linear with its regular counterpart directly instead of indexing both Sequentials, as suggested in review. Signed-off-by: 0z5a <dezhen.lu@student.uni-tuebingen.de>
|
@denera Follow-up on Blackwell. I ran the tests at the current head (
The 24/50 failures you saw on SM100 do not occur on sm_103 with the current head. The three graph tests this PR adds also pass, 26 cases in all:
|
Fixes #3437.
Delayed
backward_dw()calls from modules sharing a parameter must add their contributions to the existing gradient. Assignment drops earlier tied-weight and microbatch contributions. Apply the same None-versus-add contract to all four implementations: the base path used byte.Linear,te.LayerNormMLP,te.GroupedLinear, andte.ops.GroupedLinear.LayerNormMLP accumulates both FC weight gradients and nonempty deferred bias gradients. Biases computed in the main backward, including FP8 block-scaling biases, are preserved without recomputing them from saved activations. Grouped paths cover discrete and packed weights, with deferred discrete bias accumulation in the module path. Fused
main_gradaccumulation and reduction hooks retain their existing behavior.Merge current main (
5e994640) and resolve the LayerNormMLP bias conflict while preserving the accumulation fix and upstream changes.The public regression file is now launched by
qa/L0_pytorch_unittest/test.sh. Its 50 parametrized GPU cases cover tied and non-tied Linear, an independentnn.Linearreference, both drain orders, multiple microbatches, two reset/step iterations with bothset_to_nonesettings, both LayerNormMLP FCs and biases, FP8 block-scaling bias preservation, and discrete/packed grouped weights. Tests keep their sizes local and spell out execution and drain steps; only the two-Linear factory is shared. Packed and FP8 cases check their existing runtime support predicates.CUDA Graph follow-up at
dbe1326e7a2ddfca0ee40b19142f84404eaa9399:The two deferred-weight-gradient capture paths now clear ordinary-allocator
.gradtargets while capturing, retain the fresh graph-pool contributions, and clone or add those contributions into caller-owned gradients before delayed hooks during replay.zero_grad(set_to_none=True)can release the caller's gradients without invalidating a captured accumulation address. Reset releases the retained graph gradients.tests/pytorch/test_cuda_graphs.pyadds 26 parametrized cases covering both capture schedules, both gradient reset modes, FP32/BF16, bias, retained gradients, tied weights, multiple microbatches and fusedmain_grad, compared with independent PyTorch references.Validation for this follow-up:
WeightGradStoreusing real CPU Parameters and independent PyTorch autograd results. All 36 accumulation controls passed; the previous review head fails 32 of them in the three remaining implementations. Another nine controls passed for fused-main-grad preservation and hook invocation.transformer_engine/pytorch/graph.pypassed (10.00/10).Microbatch accumulation can differ in BF16 rounding order; those cases use a 1e-2 tolerance. Single-microbatch Linear comparisons remain exact.