Repository navigation
Conversation
079beac to
8514faa
Compare
utsav1033
left a comment
There was a problem hiding this comment.
the script showed that it computed gradient thrice and printed its size, correct being 34.352, which i got right everytime. and that wasnt the case on main as it was 0.0 at first and 6218.577 later.
the new test proved through a test added by this pr, the bug test failed on main and passed on this pr.
nothing else broke when i ran the related test files(test_fast, test_losses, test_autograd, test_vmap)
my setup is apple m4 on macOS 27.0.1
8514faa to
46087d0
Compare
ArupKantiDas
left a comment
There was a problem hiding this comment.
Thanks for the fix. I read it against current main and tested it on an M5 Pro, and I think it's right.
The root cause checks out in mlx/backend/metal/device.cpp. end_encoding erases each temporary's buffer from all_outputs_ before recording prev_ce_outputs_[out] = fence_. The gradient lives in the temporary copy's buffer, so it never gets a fence entry, and the next command buffer reads it without a waitForFence. You only see this when needs_commit() closes the buffer right after the VJP, hence the large input.
Dropping the add_temporary is the same thing RMSNormVJP and LayerNormVJP do (their check_input hands back the copy without registering it), and the CUDA CrossEntropyVJP does it too. in_copied always implies donate_in, so out always ends up owning the copy and nothing leaks. I also looked at the forward CrossEntropy::eval_gpu: it still registers its copy as a temporary, but never donates it, so it doesn't need the same change.
What I ran (M5 Pro, macOS 26.6):
| Build | Issue repro, grad norms (expected 34.352) | New test body, repeated |
|---|---|---|
| 0.32.4 from PyPI | 34.352, 6218.577, 6218.577 | 10/10 runs fail |
| This PR (46087d0) | 34.352, 34.352, 34.352 | 0/20 runs fail |
This PR with only cross_entropy.cpp reverted |
not run | 10/10 runs fail (30/40 gradient calls wrong) |
Full suite on the PR build: 994 tests pass, 56 skipped. pre-commit is clean on both files.
A small nit on the test comment, which says "the 50 Mi element command buffer limit". That limit is max_mb_per_buffer_. Device::Device() picks it from the last letter of the GPU architecture name (50 for s and d, 40 for everything else; my M5 Pro reports applegpu_g17s, so it gets 50), and it gets compared against buffer_sizes_, which counts elements, not MB. The slice plus its copy is at least 64 Mi elements, so the test is over the limit on every GPU either way. Maybe "over the per-buffer limit (40 or 50 Mi elements depending on the GPU)"?
I used Claude Code to help trace the encoder code and run the builds and tests above; I read through the results myself before posting.
fixes #4686
On Metal, the gradient of
mx.fast.cross_entropycan be wrong for a non-row-contiguous view likelogits[:, :-1]once the input is big enough that the command buffer gets committed right after the VJP. It's then possible for the next buffer to read the gradient before it's written, so calls sometimes fail.As was mentioned in the issue,
ensure_row_contiguousregisters the input copy as a temporary, and the VJP then donates that copy to the output.end_encodingdrops temporaries from the encoder's outputs, so the gradient buffer isn't tracked by the fence and the next command buffer doesn't wait for it to be written.This fix no longer registers the input copy as a temporary, since the output shares its buffer, matching the CUDA VJP and
LayerNormVJP.I added a regression test with an input big enough so that the command buffer always splits after the VJP, and I built and tested on an M3 Pro.