PR #200732 made atomicrmw fadd respect the function's denormal
mode — at the cost of turning most f32 atomic adds into CAS loops. PR #206154
proposes to make the old fast-but-unfaithful lowering the default again.
This report weighs both defaults from the IR-contract, end-user, and
deployment perspectives, and recommends a path.
The correct-by-default lowering is the right destination, but today it
regresses the most common configuration (default IEEE mode → every global f32
atomicrmw fadd becomes a CAS loop), offers no denormal mode that yields
native atomics across address spaces, and its only remedy is a hidden
-mllvm flag that is not a supported interface and is nearly unusable in
JIT pipelines. Meanwhile the correctness it buys has no demonstrated beneficiaries.
The deciding fact: clang already ships the right control surface —
-fatomic-ignore-denormal-mode / [[clang::atomic(ignore_denormal_mode)]],
lowered to per-instruction metadata that AMDGPU honors for exactly this problem.
NVPTX should adopt that mechanism, and flipping the default back to
FTZ-correct should be gated on it, on an announced schedule — not on hope.
PTX's native atom.add.f32 has a fixed, address-space-dependent
flush-to-zero behavior that cannot be changed by the program[2]:
| Operation | Denormal behavior | Configurable? |
|---|---|---|
atom.add.f32, global memory | Always flushes denormals (FTZ) | No |
atom.add.f32, shared memory | Never flushes | No |
atom.add.f16 / .bf16 | Never flushes | No |
atom.add.f64 | No f64 FTZ on NVIDIA GPUs | — |
For years the NVPTX backend lowered atomicrmw fadd to
atom.add unconditionally — fast, single-instruction, and
identical to what nvcc does for CUDA's atomicAdd(), but
oblivious to the function's declared denormal mode. Under LLVM's default
(IEEE) floating-point environment, a global-memory atomic add silently
flushed denormals: a genuine miscompile with respect to the IR contract.
atom.add only when the function's denormal mode matches the
instruction's fixed behavior; otherwise expand to a CAS loop. A hidden
escape hatch, -nvptx-allow-ftz-atomics (default
false), forces the old lowering.
"denormal-fp-math" string attribute, which
is auto-upgraded only by the bitcode reader, not by the C++ API path XLA uses —
but even with correct attributes, the default IEEE mode makes every global
f32 atomic a CAS loop.[5]
nvptx-allow-ftz-atomics to true: restore the fast,
FTZ-ignoring lowering by default; make the FTZ-correct behavior opt-in.
AlexMaclean in favor; jlebar skeptical but not objecting.
LangRef is unambiguous on three points[1]:
atomicrmw fadd: defined as
*ptr = *ptr + val “using floating point arithmetic” — i.e., an
ordinary FP add subject to the same environment rules, plus atomicity. There is
no carve-out granting FP atomics looser denormal semantics.denormal_fpenv is asymmetric: with output mode
preservesign, flushing outputs is permitted but not mandated
(“It is not mandated that flushing to zero occurs”); with input mode
preservesign, treating input denormals as zero is mandated.Mapping the hardware onto that contract:
| Function mode | Global atom.add.f32 | Shared atom.add.f32 | Generic ptr |
|---|---|---|---|
ieee (default) |
violates flushes when it must preserve | conforms | unknowable |
preservesign |
conforms | violates ignores mandated input flush | unknowable |
atom.add.f32 legal in
both address spaces, and a generic pointer can never be proven legal at compile
time. The conflict is not between “right mode” and “wrong mode” — it is between a
per-function attribute and per-instruction, per-address-space
hardware behavior. A function-level knob cannot resolve it; only a per-operation
one can (§7).
So the pre-#200732 lowering was, strictly, a miscompile — and worse in one direction than the other: in IEEE mode it silently introduced flushing, which is never permitted; in FTZ mode failing to flush inputs is at least the kind of slop many FTZ users tolerate. #200732 fixed a real contract violation. The question is not whether it was a bug fix. It was. The question is what the default should be while the ecosystem cannot yet express the alternative.
| Criterion | A · Correct | B · Fast |
|---|---|---|
| IR contract fidelityDoes codegen mean what the IR says? | sound | violated by default |
| Performance of common caseDefault-mode global f32 atomics | CAS loop | native |
| Behavioral continuityExisting deployed code, incl. racy-but-stable patterns | breaks silently | unchanged |
| Ecosystem consistencynvcc / CUDA expectations | diverges | matches |
| Failure-mode qualityHow victims discover the change | delayed perf loss, divergence heisenbugs | rare silent numeric drift |
| Beneficiaries todayUsers demonstrably helped by the default | none identified | most GPU users |
| Long-term IR healthPrecedent for target defaults | good | acceptable only if documented & time-boxed |
The principled position — jlebar's — is straightforward and largely right:
LangRef is a contract, a backend that ignores denormal_fpenv on
atomicrmw fadd miscompiles, and programs broken by CAS-loop
divergence contain data races that LLVM was always entitled to expose
(atomics are not convergent; the compiler may already duplicate or
sink them in ways that break warp-convergence assumptions). Correctness fixes
should not be held hostage to programs that "happened to work."
But LLVM practice tempers the principle in two relevant ways.
First, targets do make pragmatic speed/semantics trade-offs at defaults, and the
project's real norm is that such trade-offs must be visible and controllable — not that
they never happen. The NVPTX backend in this very file defaults
UsePrecSqrtF32 one way and approximate-log2 the other, each with a
rationale about matching CUDA-ecosystem expectations. The sin is not deviation;
it is silent, uncontrollable deviation.
Second, the fix's benefit is asymmetric to its cost. Correctness fixes normally protect someone. Here, nobody has surfaced a workload harmed by denormal flushing in FP atomics across a decade of the old behavior — reductions fed by atomic fadd are already nondeterministically associated, so per-op flushing is typically noise beneath run-to-run variance. The costs, by contrast, are measured (XLA regressions), broad (all default-mode global f32 atomics), and of the worst discovery class: performance loss and divergence bugs found weeks later by bisection, not compile errors found in CI.
From a high-level language, the user's expressible intents are painfully coarse.
A CUDA programmer writes atomicAdd(ptr, x) and controls denormal
behavior only via -ftz=true/false (clang: -fcuda-flush-denormals-to-zero),
which sets a per-function mode. Under Option A, the outcomes are:
| User compiles with | Global atomicAdd | Shared atomicAdd | Generic ptr |
|---|---|---|---|
| default (no FTZ) | CAS loop | native | CAS loop |
-ftz=true | native | CAS loop | CAS loop |
There is no spelling available to the user that yields native atomics
everywhere — and kernels routinely mix shared-memory staging with global
accumulation, so "pick the mode that matches your pointers" doesn't exist as
advice. Worse, whether a given atomicAdd compiles to one instruction
or a divergent loop is now invisible at the source level; the answer requires
reading backend source. That destroys the user's ability to reason about both
performance and synchronization behavior of a primitive they think of as "one
hardware instruction." (Do I need __syncwarp() after this
atomicAdd? Today: it depends on the address space, the denormal
attribute, and the compiler version.)
Also worth naming plainly: nvcc unconditionally compiles atomicAdd(float*)
to atom.add.f32, and CUDA's own documentation describes the global-memory
flush behavior as a property of the operation. The CUDA ecosystem's de facto
contract is "atomicAdd is the hardware instruction, with the hardware's quirks."
A clang/LLVM default that quietly deviates from that puts the two CUDA compilers
at odds on a performance cliff, which users experience as "clang is slow," not as
"clang is correct."
The counterweight: a user who has deliberately requested IEEE semantics — a numerics-careful HPC code, say — is silently denied them under Option B, with no diagnostic. That user exists in principle; the discussion surfaced none in practice. Their remedy under Option B (flip one flag, or soon, per-block attribute §7) is far cheaper than the majority's remedy under Option A (audit every atomic for perf and convergence, in every framework in the stack).
The escape hatch that makes Option A tolerable is
-mllvm -nvptx-allow-ftz-atomics=true. As a deployment interface,
it fails on every axis:
cl::opt flags are hidden,
explicitly not a stable interface, and can be renamed or removed without notice.
Telling every affected user to adopt one is institutionalizing a hack.cl::opt
once at startup, making it effectively immutable.-mllvm flags requires
plumbing through several layers of framework configuration, or is impossible
for end users entirely (serving stacks, managed environments).Option B's deployment story is the mirror image, and much smaller: the rare denormal-sensitive user must discover that FP atomics ignore their FTZ setting and opt into strict lowering. That discovery is also silent today — which is why the recommendation below makes documentation and a supported control mandatory, not optional. But asking few, expert users to opt in beats asking everyone else to opt out through an interface most of them cannot reach.
AMDGPU faced the identical dilemma years earlier — its global FP atomics also have fixed denormal behavior that can conflict with the function's mode — and the resolution that survived review was neither a wrong default nor a hidden flag. It was per-instruction metadata, emitted by the frontend from a supported user-facing control[3]:
; user said "denormal handling on this atomic is insignificant" —
; backend may emit the native instruction despite the mode mismatch
%r = atomicrmw fadd ptr addrspace(1) %p, float %v seq_cst,
!amdgpu.ignore.denormal.mode !0
The full control stack already exists in-tree, today:
| Layer | Mechanism | Status |
|---|---|---|
| Driver flag | -fatomic-ignore-denormal-mode (alias: -munsafe-fp-atomics) | shipping |
| Source, scoped | [[clang::atomic(ignore_denormal_mode)]] on a compound statement | shipping |
| IR | !amdgpu.ignore.denormal.mode on atomicrmw fadd | shipping |
| Backend honor | AMDGPU: yes (SIISelLowering) · SPIR-V AMDGCN: yes · NVPTX: no | the gap |
This dissolves most of the dilemma. The reason #206154 exists is that Option A's
victims have no reachable remedy; the reason jlebar distrusts Option B is that
"temporary" wrong defaults have no exit. A per-operation control that clang's CUDA
path emits (for atomicAdd, or under a flag, or per
[[clang::atomic]] block) gives Option A a usable remedy — and
thereby gives Option B a concrete, testable exit criterion. It also answers
Artem-B's "the escape hatch is too indiscriminate" objection: metadata is
per-instruction, composable across TUs, and JIT-friendly (frameworks control
their own IR; no process-global flag plumbing).
!atomic.ignore.denormal.mode
or a target-neutral spelling blessed in LangRef) rather than having NVPTX match on
an amdgpu.-prefixed string — but that is a bikeshed, not a blocker;
dropped metadata is semantically safe by design, so a transition is low-risk.
Land #206154: default nvptx-allow-ftz-atomics to
true. Between two flawed stopgaps, prefer the one whose
harms are small, opt-out-able by its few affected users, and consistent with
what the ecosystem has always shipped — over the one whose harms are broad,
latent, and unfixable through any supported interface. But land it as a
transition with obligations, not as a resting state:
NVPTXUsage.rst and the release notes must state that by default
atomicrmw fadd may not honor denormal_fpenv on f32,
what the hardware does per address space, and how to get strict behavior. A
known deviation is a policy; an undocumented one is a bug that ferments.
Honor ignore-denormal metadata in
shouldExpandAtomicRMWInIR, and have clang's CUDA/OpenMP paths emit it
via the existing -fatomic-ignore-denormal-mode /
[[clang::atomic]] machinery. This replaces the process-global
chicken switch with a supported, per-instruction, JIT-friendly surface, and
gives XLA/Triton something they can emit directly.
Once frontends can express "fast atomic here, please," the strict default becomes livable and should return, with a release-note deprecation window. Do not gate on future PTX ftz/non-ftz atomic variants: as the discussion established, those arrive behind PTX-version floors and may well be SASS CAS loops themselves — jlebar's skepticism on this point deserves to be ratified as policy.
Unsynchronized read-after-atomic is a data race whichever default wins, and LLVM may break it through unrelated transforms. The right venue is documentation and (if NVIDIA wants stronger guarantees) an explicit convergent atomic intrinsic — not the FTZ default. Neither PR should carry that weight.
In short: #200732 identified the right contract; #206154 identifies the right default for today; the AMDGPU mechanism is the right bridge between them. The failure mode to avoid is landing #206154 and stopping — that converts a correctness fix into a dead flag and validates every "temporary defaults are forever" prior. Conditions (1)–(3) are cheap, mostly-existing work; they should be part of the same conversation, ideally the same patch series.
An honest report says what would change its verdict.
This assessment was produced in response to the following request, reproduced verbatim:
PR https://github.com/llvm/llvm-project/pull/200732 implemented correct denormals folding for atomic FP operations on NVIDIA GPUs, but it also introduced undesirable side effect in the code that happened to work acceptably well for the existing code. Details and the discussion are in https://github.com/llvm/llvm-project/pull/206154. I want an impartial expert opinion on what should be the default compiler behavior and why -- fast atomics that ignore specified denormals handling mode (pre-patch behavior) or correct denormals handling, but with side effects that will need to be addressed by all affected users. Consider this from both the LLVM's point of view, from the end-user's point of view (e.g. what can user do from a high-level language like CUDA), and from the build and deployment point of view where adoption may need build changes, and/or workarounds for the performance regressions. Supplementary material: LLVM IR reference is the canonical IR spec/contract and can be found in llvm/docs/LangRef.rst
Create your report as a HTML document served with a simple python web server and give me its URL.
denormal_fpenv attribute semantics (output flush permitted vs. input flush mandated), atomicrmw fadd semantics. llvm/docs/LangRef.rst (≈ lines 2745–2799, 4110–4122, 12582).llvm/lib/Target/NVPTX/NVPTXISelLowering.cpp:7439–7484; AllowFTZAtomics flag at lines 109–120.!amdgpu.ignore.denormal.mode, llvm/docs/AMDGPUUsage.rst:2243; honored in llvm/lib/Target/AMDGPU/SIISelLowering.cpp:20139; emitted by clang in clang/lib/CodeGen/Targets/AMDGPU.cpp:575 from -fatomic-ignore-denormal-mode (alias -munsafe-fp-atomics, clang/include/clang/Options/Options.td:5926) and [[clang::atomic(...)]] (clang/include/clang/Basic/Attr.td:5424)."denormal-fp-math"/autoupgrade analysis, and the thread-divergence discussion.