Possible NVCC Warp-Aggregated Atomic Issue on Ampere (CUDA 11.4) — atomicAdd(+2) Produces Unexpected Results
Environment
- OS: Windows
- CUDA Toolkit: 11.4
- GPU architecture: sm_86 (Ampere)
- Build system: CMake / Visual Studio
- Kernel launch: 691,200 blocks × 64 threads (1D grid)
- Kernel contains substantial branching, nested loops, and divergent control flow
- Counter type: uint32_t
Problem description
I am investigating unexpected results from atomicAdd() in a large CUDA kernel.
The kernel processes 20 subframes, each having its own global-memory counter.
The counter is initialized to zero before launching the kernel, with explicit synchronization before and after execution.
The relevant code is:
uint32_t idx = atomicAdd(
&NumOfDetSpots\[sfInx\],
2u
);
For my test data, only one thread per subframe is expected to execute this operation.
Therefore, the expected result for each subframe is:
- idx = 0
- NumOfDetSpots[sfInx] = 2
However, with the optimized kernel, I consistently observe counters equal to 4, and in earlier tests sometimes 6. The returned idx can also be unexpectedly nonzero.
The behavior is highly sensitive to seemingly unrelated source-code modifications, but is generally reproducible for a given compiled binary.
Debugging performed
1. Initialization and synchronization
I explicitly reset the counters using cudaMemsetAsync() and synchronize before launching the kernel.
After the kernel, I synchronize and copy the counters to host memory.
I also tested directly copying the counter values rather than copying the entire containing structure. The unexpected results persisted.
2. Separate debug counter
I added an additional atomic counter immediately before the original atomic:
atomicAdd(&debugCount[sfInx], 1u);
idx = atomicAdd(&NumOfDetSpots[sfInx], 2u);
Both arrays are initialized to zero.
Observed results for all 20 subframes:
debugCount[sfInx] = 1
NumOfDetSpots[sfInx] = 4
This suggests that only one execution of the debug atomic is being counted, while the second atomic produces an unexpected total.
3. Thread identity and warp participation
I printed the subframe index, global thread index, lane number, and active mask at the atomic location.
The output contains 20 lines, one per subframe.
Example:
sf=0 tid=9599 lane=31 mask=80000000
sf=1 tid=42367 lane=31 mask=80000000
sf=2 tid=75135 lane=31 mask=80000000
sf=3 tid=107903 lane=31 mask=80000000
The global thread indices differ by 32,768 between consecutive subframes.
All observed threads are in separate warps and report only lane 31 active at the diagnostic point.
I understand that __activemask() does not necessarily represent the exact participation mask used internally by compiler-generated warp aggregation.
4. Changing the atomic increment
I found that:
Implementation
Result
Inline atomicAdd(…, 1)
Correct
Inline atomicAdd(…, 2)
Incorrect
Inline atomicAdd(…, increment) with runtime parameter equal to 2
Correct in tested builds
Non-inlined device wrapper with increment 2
Correct
Inline atomic with CUDA device debugging (-G)
Correct
5. Non-inlined wrapper fixes the issue
I implemented:
_device_ _noinline_
uint32_t MyAtomicAdd(uint32_t* ptr, uint32_t value)
{
return atomicAdd(ptr, value);
}
And changed the original call to:
idx = MyAtomicAdd(&NumOfDetSpots[sfInx], 2u);
With this version, all 20 counters consistently produce the expected values:
NumOfDetSpots[sfInx] = 2
idx = 0
6. SASS comparison
I compared the generated SASS for the working and failing versions using cuobjdump.
The optimized inline atomic uses instructions consistent with compiler-generated warp aggregation:
VOTEU.ALL UR4, UPT, PT;
FLO.U32 R4, UR4;
POPC R0, UR4;
ISETP.EQ.U32.AND P0, PT, R4, R3, PT;
IMAD.SHL.U32 R3, R0, 0x2, RZ;
@P0 ATOMG.E.ADD.STRONG.GPU PT, R3, [R62.64], R3;
The population count is scaled by 2 before the atomic.
The working _noinline_ implementation instead generates a direct atomic instruction without the surrounding warp-aggregation sequence:
ATOMG.E.ADD.STRONG.GPU PT, R4, [R4.64], R6;
The larger kernel also contains instructions such as SHFL.IDX and POPC following the aggregated atomic, suggesting reconstruction of per-thread values.
The compiler generates substantially different SASS when changing the increment from 1 to 2.!!!
7. Sensitivity to control flow
The original kernel contains nested loops and divergent branching.
I found a particular loop where changing:
if (condition)
{
RiseLine = i;
break;
}
affected the final atomic result.
Replacing break with i = -1 still reproduced the failure, whereas removing early termination made the result correct in the tested build.
Other small changes, such as replacing:
BBInfo[sfInx][idx].SpotID = 1;
with:
BBInfo[sfInx][0].SpotID = 1;
also caused the failure to disappear in certain builds.
I recognize that these changes alter both program behavior and compiler code generation, so they are not by themselves proof of a compiler issue.
8. Compute Sanitizer
I ran Compute Sanitizer Memcheck, which reported zero errors in the tested configuration.
I understand that this does not exclude data races, uninitialized local variables, or other forms of undefined behavior.
9. Kernel reduction
I removed most of the original kernel logic while preserving the atomic operation.
The simplified kernel worked correctly.
When I progressively restored the original logic, the failure reappeared. The result was particularly sensitive to loop termination and branches.
Current hypothesis
The evidence suggests that the issue is associated with optimization of atomic operations in CUDA 11.4, potentially involving warp aggregation and divergent control flow.
The compiler appears to aggregate atomic additions using VOTEU.ALL, POPC, and related instructions.
However, I cannot rule out undefined behavior elsewhere in the large kernel or a race that becomes visible only under certain optimization conditions.
The fact that _noinline_ and -G both eliminate the failure makes the code-generation differences particularly interesting.
Questions
- Is this pattern of warp-aggregated atomic generation expected in CUDA 11.4 for atomicAdd(…, 2) when the returned old value is used?
- Are there any known CUDA 11.4 / sm_86 issues involving warp aggregation, divergent loops, or incorrect reconstruction of atomic return values?
- Is there a compiler option or supported technique to disable automatic warp aggregation for one atomic operation without disabling optimization for the entire kernel?
- Is using a _noinline_ device wrapper a reasonable temporary workaround?
- What additional diagnostics would help distinguish an NVCC optimization issue from undefined behavior elsewhere in the kernel?
Next planned experiment
I plan to test the same code on an NVIDIA L40S GPU running Linux with CUDA 12.8, using equivalent inputs and optimization settings.
I can provide SASS comparisons, compiler output, and additional diagnostics if useful.
Any guidance from NVIDIA engineers would be appreciated.