Repository navigation
Compute the new value of an atomic CAS loop from the compared value - #9700
Open
alexreinking wants to merge 1 commit into
Open
alexreinking wants to merge 1 commit into
alexreinking wants to merge 1 commit into
Conversation
An atomic() update that isn't a single atomic read-modify-write is lowered to a compare-and-swap loop. The loop compared against a phi of the initial atomic load and the cmpxchg result, but computed the new value with codegen(op->value), whose self-load is a plain reload of the location. The cmpxchg then succeeds whenever memory still holds the compared value, even though the stored value was computed from a different load: a data race in the LLVM IR. On CUDA the reload is a weak ld.global that can read a stale L1 line, and updates are lost. Loads of the stored buffer in the value are now replaced with the compared value when their index is equal to the store index. When that isn't known at compile time, the load is kept and the compared value is selected instead when the indices are equal at run time. This matters for a += whose self-load index differs syntactically from the store index after lowering, which is also why such an update misses the atomic-add path. Add a test that checks the CAS loops in the generated LLVM IR contain no non-atomic loads, make test_parallel_cas_update in atomics.cpp stay below its cap so lost updates change the result, and add a staggered multi-block GPU stress test of a CAS update. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PWSVRwgKGuCHpMzU4aSRWi
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #9700 +/- ##
==========================================
- Coverage 72.89% 72.89% -0.01%
==========================================
Files 266 266
Lines 83478 83508 +30
Branches 20497 20503 +6
==========================================
+ Hits 60855 60872 +17
- Misses 16173 16197 +24
+ Partials 6450 6439 -11 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CodeGen_LLVM::codegen_atomic_rmwlowers anatomic()update that isn't asingle atomic read-modify-write to a compare-and-swap loop. The loop compared
against a phi of the atomic load and cmpxchg result, but computed the new value
with
codegen(op->value). In that value, the self-load was a plain reload ofthe location. The cmpxchg could therefore succeed with a value computed from a
different (stale) value of the location. That is a data race in the LLVM IR,
and on CUDA it loses updates: the weak
ld.globalcan read a stale L1 line.Fix
Inside the existing loop, each
Loadof the stored buffer in the value isreplaced with the phi (reinterpreted or cast when the buffer is stored as a
different type, see
upgrade_type_for_storage) before codegen:simplify(load_index == store_index)is true, the load is replaced withthe phi.
select(load_index == store_index, phi, load). Thiscase matters in practice. After lowering, the self-load index and the store
index of a
+=can be different expressions with the same value, e.g.out[t1832] = out[t2214 + 2] + awhere both are lets of separately CSE'dexpressions. That is also why such a
+=misses the atomic-add path. LLVMusually proves the comparison true and folds the select and the load away. In
the case above, the PTX loop has no
ld.globalleft.Vector stores are still scalarized one lane at a time. For each lane the value
and the store index are both reduced with
extract_lane, so a vector self-loadat the store's index becomes a scalar load at that lane's index, the same
expression as that lane's store index, and is replaced exactly. A vector load
of the buffer that remains, e.g. one at a broadcast index, is compared
lane-wise.
AddAtomicMutexdoesn't rule out a self-load at a different index for storesit leaves to the CAS path. With
atomic(true)such a load is the user'sresponsibility, so it is left alone rather than asserted against.
The atomic-add path is unchanged.
Tests
correctness_atomics_cas_loop: compiles CAS-path updates (int32min,float
max, uint32*, a vectorized int32min, and float16min, whichgoes through the storage-type reinterpret) to LLVM assembly. It checks that no
basic block containing a
cmpxchgalso contains a non-atomicload, and thatat least one such block exists. It fails on main for all five cases and passes
with this change.
correctness_atomics:test_parallel_cas_updatenow uses few enough updatesthat no bin reaches the
mincap, so lost updates change the result. Before,6 of 7 bins saturated at 100. A new GPU test,
test_gpu_cas_tiles, runs onCUDA/OpenCL: 200 blocks each
min-update 16 tiles in a staggered order withexact integer deltas. It is a best-effort stress test. On an RTX 3090 it fails
on main in all 3 of 3 runs and passes with this change.
Results on an RTX 3090 (
HL_JIT_TARGET=host-cuda):correctness_atomics,correctness_atomic_tuples,correctness_atomics_gpu_8_bit,correctness_atomics_gpu_mutexandcorrectness_atomics_cas_loopall pass. Onthe original GEMM-shaped reproducer, main gives wrong results in 16 of 18 runs
and this branch in 0 of 27.
Performance: in the CAS loop, the reload is replaced with a value already held
in a register. When a select is needed and LLVM can't fold it, there is one
extra compare and select per iteration. The atomic-add path is unchanged.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PWSVRwgKGuCHpMzU4aSRWi