Skip to content

[NVPTX] Set default value of nvptx-allow-ftz-atomics to true - #206154

Merged
Artem-B merged 1 commit into
llvm:mainfrom
Artem-B:nvptx-default-allow-ftz-atomics
Jul 17, 2026
Merged

Artem-B merged 1 commit into
llvm:mainfrom
Artem-B:nvptx-default-allow-ftz-atomics

Conversation

@Artem-B

@Artem-B Artem-B commented Jun 26, 2026

Copy link
Copy Markdown
Member

This is a follow-up to #200732.

Setting the default value of nvptx-allow-ftz-atomics to true preserves the status quo. It allows the NVPTX backend to emit native floating-point atomic add instructions by default, rather than falling back to the CAS loop required for strict FTZ compliance.

Correct FTZ handling forces a lot of existing code to emit CAS loops. This introduces thread divergence, breaking code that relies on atomic operations being non-divergent (for example, warp-wide atomic additions followed by a load of the reduced value without explicit synchronization).

While FTZ-correct atomic behavior is desirable as a long-term default, introducing thread divergence into a common operation that previously "happened to work" without synchronization is highly disruptive. Defaulting to true makes the FTZ-correct behavior opt-in for now, allowing users time to audit and adapt their code.

Test cases are updated to verify both enabled and disabled states of the option, alongside the new default behavior.

@llvmorg-github-actions

Copy link
Copy Markdown

@llvm/pr-subscribers-backend-nvptx

Author: Artem Belevich (Artem-B)

Changes

This is a follow-up to #200732.

Setting the default value of nvptx-allow-ftz-atomics to true preserves the status quo. It allows the NVPTX backend to emit native floating-point atomic add instructions by default, rather than falling back to the CAS loop required for strict FTZ compliance.

Correct FTZ handling forces a lot of existing code to emit CAS loops. This introduces thread divergence, breaking code that relies on atomic operations being non-divergent (for example, warp-wide atomic additions followed by a load of the reduced value without explicit synchronization).

While FTZ-correct atomic behavior is desirable as a long-term default, introducing thread divergence into a common operation that previously "happened to work" without synchronization is highly disruptive. Defaulting to true makes the FTZ-correct behavior opt-in for now, allowing users time to audit and adapt their code.

Test cases are updated to verify both enabled and disabled states of the option, alongside the new default behavior.


Patch is 615.98 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/206154.diff

7 Files Affected:

  • (modified) llvm/lib/Target/NVPTX/NVPTXISelLowering.cpp (+4-4)
  • (modified) llvm/test/CodeGen/NVPTX/atomicrmw-allow-ftz-atomics.ll (+53-49)
  • (modified) llvm/test/CodeGen/NVPTX/atomicrmw-sm60.ll (+3021-1523)
  • (modified) llvm/test/CodeGen/NVPTX/atomicrmw-sm70.ll (+2943-1497)
  • (modified) llvm/test/CodeGen/NVPTX/atomicrmw-sm90.ll (+1635-849)
  • (modified) llvm/test/CodeGen/NVPTX/atomicrmw.py (+10-2)
  • (modified) llvm/test/CodeGen/NVPTX/atomics.ll (+71-41)
diff --git a/llvm/lib/Target/NVPTX/NVPTXISelLowering.cpp b/llvm/lib/Target/NVPTX/NVPTXISelLowering.cpp
index 1c5caf124c17e..315bf5b9f58b0 100644
--- a/llvm/lib/Target/NVPTX/NVPTXISelLowering.cpp
+++ b/llvm/lib/Target/NVPTX/NVPTXISelLowering.cpp
@@ -109,14 +109,14 @@ static cl::opt<bool> UsePrecSqrtF32(
     cl::init(true));
 
 // PTX atom.add.f32 has fixed FTZ behavior that may not match the function's
-// (see shouldExpandAtomicRMWInIR), so by default we fall back to a CAS loop
-// when they disagree. This flag is an escape hatch to use atom.add anyway,
-// trading correct denormal handling for the speed of the native instruction.
+// (see shouldExpandAtomicRMWInIR), so we'd normally fall back to a CAS loop
+// when they disagree. This option (enabled by default) allows using atom.add
+// anyway, trading correct denormal handling for the speed of the native instruction.
 static cl::opt<bool> AllowFTZAtomics(
     "nvptx-allow-ftz-atomics", cl::Hidden,
     cl::desc("NVPTX Specific: Lower atomicrmw fadd to atom.add even when its "
              "FTZ behavior does not match the function's denormal mode."),
-    cl::init(false));
+    cl::init(true));
 
 /// Whereas CUDA's implementation (see libdevice) uses ex2.approx for exp2(), it
 /// does NOT use lg2.approx for log2, so this is disabled by default.
diff --git a/llvm/test/CodeGen/NVPTX/atomicrmw-allow-ftz-atomics.ll b/llvm/test/CodeGen/NVPTX/atomicrmw-allow-ftz-atomics.ll
index 727c13bb4e47e..c1d223082eb30 100644
--- a/llvm/test/CodeGen/NVPTX/atomicrmw-allow-ftz-atomics.ll
+++ b/llvm/test/CodeGen/NVPTX/atomicrmw-allow-ftz-atomics.ll
@@ -1,36 +1,39 @@
 ; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 6
 ; -nvptx-allow-ftz-atomics forces atomicrmw fadd to lower to atom.add even when
 ; the hardware's FTZ behavior doesn't match the function's denormal mode, where
-; we'd otherwise emit a CAS loop. Compare the default lowering (DEFAULT) against
-; the flag (ALLOW).
-; RUN: llc < %s -march=nvptx64 -mcpu=sm_90 -mattr=+ptx87 | FileCheck %s --check-prefixes=CHECK,DEFAULT
-; RUN: llc < %s -march=nvptx64 -mcpu=sm_90 -mattr=+ptx87 -nvptx-allow-ftz-atomics | FileCheck %s --check-prefixes=CHECK,ALLOW
+; we'd otherwise emit a CAS loop. Compare the lowering with the option disabled
+; (DISALLOW) against the option enabled or default (ALLOW).
+; RUN: llc < %s -march=nvptx64 -mcpu=sm_90 -mattr=+ptx87 -nvptx-allow-ftz-atomics=false | FileCheck %s --check-prefixes=CHECK,DISALLOW
+; RUN: llc < %s -march=nvptx64 -mcpu=sm_90 -mattr=+ptx87 -nvptx-allow-ftz-atomics=true | FileCheck %s --check-prefixes=CHECK,ALLOW
+; RUN: llc < %s -march=nvptx64 -mcpu=sm_90 -mattr=+ptx87 | FileCheck %s --check-prefixes=CHECK,ALLOW
+; RUN: %if ptxas-sm_90 && ptxas-isa-8.7 %{ llc < %s -march=nvptx64 -mcpu=sm_90 -mattr=+ptx87 -nvptx-allow-ftz-atomics=false | %ptxas-verify -arch=sm_90 %}
+; RUN: %if ptxas-sm_90 && ptxas-isa-8.7 %{ llc < %s -march=nvptx64 -mcpu=sm_90 -mattr=+ptx87 -nvptx-allow-ftz-atomics=true | %ptxas-verify -arch=sm_90 %}
 ; RUN: %if ptxas-sm_90 && ptxas-isa-8.7 %{ llc < %s -march=nvptx64 -mcpu=sm_90 -mattr=+ptx87 | %ptxas-verify -arch=sm_90 %}
-; RUN: %if ptxas-sm_90 && ptxas-isa-8.7 %{ llc < %s -march=nvptx64 -mcpu=sm_90 -mattr=+ptx87 -nvptx-allow-ftz-atomics | %ptxas-verify -arch=sm_90 %}
 
-; global f32: atom.add flushes, so default uses a CAS loop (function is not FTZ);
-; the flag forces atom.add.
+; global f32: atom.add flushes, so we'd normally use a CAS loop (function is not FTZ)
+; when the option is disabled; when enabled (default), it lowers to atom.add.
 define float @fadd_f32_global(ptr addrspace(1) %addr, float %val) {
-; DEFAULT-LABEL: fadd_f32_global(
-; DEFAULT:       {
-; DEFAULT-NEXT:    .reg .pred %p<2>;
-; DEFAULT-NEXT:    .reg .b32 %r<5>;
-; DEFAULT-NEXT:    .reg .b64 %rd<2>;
-; DEFAULT-EMPTY:
-; DEFAULT-NEXT:  // %bb.0:
-; DEFAULT-NEXT:    ld.param.b32 %r2, [fadd_f32_global_param_1];
-; DEFAULT-NEXT:    ld.param.b64 %rd1, [fadd_f32_global_param_0];
-; DEFAULT-NEXT:    ld.relaxed.sys.global.b32 %r4, [%rd1];
-; DEFAULT-NEXT:  $L__BB0_1: // %atomicrmw.start
-; DEFAULT-NEXT:    // =>This Inner Loop Header: Depth=1
-; DEFAULT-NEXT:    add.rn.f32 %r3, %r4, %r2;
-; DEFAULT-NEXT:    atom.relaxed.sys.global.cas.b32 %r1, [%rd1], %r4, %r3;
-; DEFAULT-NEXT:    setp.ne.b32 %p1, %r1, %r4;
-; DEFAULT-NEXT:    mov.b32 %r4, %r1;
-; DEFAULT-NEXT:    @%p1 bra $L__BB0_1;
-; DEFAULT-NEXT:  // %bb.2: // %atomicrmw.end
-; DEFAULT-NEXT:    st.param.b32 [func_retval0], %r1;
-; DEFAULT-NEXT:    ret;
+;
+; DISALLOW-LABEL: fadd_f32_global(
+; DISALLOW:       {
+; DISALLOW-NEXT:    .reg .pred %p<2>;
+; DISALLOW-NEXT:    .reg .b32 %r<5>;
+; DISALLOW-NEXT:    .reg .b64 %rd<2>;
+; DISALLOW-EMPTY:
+; DISALLOW-NEXT:  // %bb.0:
+; DISALLOW-NEXT:    ld.param.b32 %r2, [fadd_f32_global_param_1];
+; DISALLOW-NEXT:    ld.param.b64 %rd1, [fadd_f32_global_param_0];
+; DISALLOW-NEXT:    ld.relaxed.sys.global.b32 %r4, [%rd1];
+; DISALLOW-NEXT:  $L__BB0_1: // %atomicrmw.start
+; DISALLOW-NEXT:    // =>This Inner Loop Header: Depth=1
+; DISALLOW-NEXT:    add.rn.f32 %r3, %r4, %r2;
+; DISALLOW-NEXT:    atom.relaxed.sys.global.cas.b32 %r1, [%rd1], %r4, %r3;
+; DISALLOW-NEXT:    setp.ne.b32 %p1, %r1, %r4;
+; DISALLOW-NEXT:    mov.b32 %r4, %r1;
+; DISALLOW-NEXT:    @%p1 bra $L__BB0_1;
+; DISALLOW-NEXT:  // %bb.2: // %atomicrmw.end
+; DISALLOW-NEXT:    st.param.b32 [func_retval0], %r1;
+; DISALLOW-NEXT:    ret;
 ;
 ; ALLOW-LABEL: fadd_f32_global(
 ; ALLOW:       {
@@ -47,29 +50,30 @@ define float @fadd_f32_global(ptr addrspace(1) %addr, float %val) {
   ret float %r
 }
 
-; generic f32: we can't know the memory space, so default always uses a CAS loop;
-; the flag forces atom.add.
+; generic f32: we can't know the memory space, so we'd normally use a CAS loop
+; when the option is disabled; when enabled (default), it lowers to atom.add.
 define float @fadd_f32_generic(ptr %addr, float %val) {
-; DEFAULT-LABEL: fadd_f32_generic(
-; DEFAULT:       {
-; DEFAULT-NEXT:    .reg .pred %p<2>;
-; DEFAULT-NEXT:    .reg .b32 %r<5>;
-; DEFAULT-NEXT:    .reg .b64 %rd<2>;
-; DEFAULT-EMPTY:
-; DEFAULT-NEXT:  // %bb.0:
-; DEFAULT-NEXT:    ld.param.b32 %r2, [fadd_f32_generic_param_1];
-; DEFAULT-NEXT:    ld.param.b64 %rd1, [fadd_f32_generic_param_0];
-; DEFAULT-NEXT:    ld.relaxed.sys.b32 %r4, [%rd1];
-; DEFAULT-NEXT:  $L__BB1_1: // %atomicrmw.start
-; DEFAULT-NEXT:    // =>This Inner Loop Header: Depth=1
-; DEFAULT-NEXT:    add.rn.f32 %r3, %r4, %r2;
-; DEFAULT-NEXT:    atom.relaxed.sys.cas.b32 %r1, [%rd1], %r4, %r3;
-; DEFAULT-NEXT:    setp.ne.b32 %p1, %r1, %r4;
-; DEFAULT-NEXT:    mov.b32 %r4, %r1;
-; DEFAULT-NEXT:    @%p1 bra $L__BB1_1;
-; DEFAULT-NEXT:  // %bb.2: // %atomicrmw.end
-; DEFAULT-NEXT:    st.param.b32 [func_retval0], %r1;
-; DEFAULT-NEXT:    ret;
+;
+; DISALLOW-LABEL: fadd_f32_generic(
+; DISALLOW:       {
+; DISALLOW-NEXT:    .reg .pred %p<2>;
+; DISALLOW-NEXT:    .reg .b32 %r<5>;
+; DISALLOW-NEXT:    .reg .b64 %rd<2>;
+; DISALLOW-EMPTY:
+; DISALLOW-NEXT:  // %bb.0:
+; DISALLOW-NEXT:    ld.param.b32 %r2, [fadd_f32_generic_param_1];
+; DISALLOW-NEXT:    ld.param.b64 %rd1, [fadd_f32_generic_param_0];
+; DISALLOW-NEXT:    ld.relaxed.sys.b32 %r4, [%rd1];
+; DISALLOW-NEXT:  $L__BB1_1: // %atomicrmw.start
+; DISALLOW-NEXT:    // =>This Inner Loop Header: Depth=1
+; DISALLOW-NEXT:    add.rn.f32 %r3, %r4, %r2;
+; DISALLOW-NEXT:    atom.relaxed.sys.cas.b32 %r1, [%rd1], %r4, %r3;
+; DISALLOW-NEXT:    setp.ne.b32 %p1, %r1, %r4;
+; DISALLOW-NEXT:    mov.b32 %r4, %r1;
+; DISALLOW-NEXT:    @%p1 bra $L__BB1_1;
+; DISALLOW-NEXT:  // %bb.2: // %atomicrmw.end
+; DISALLOW-NEXT:    st.param.b32 [func_retval0], %r1;
+; DISALLOW-NEXT:    ret;
 ;
 ; ALLOW-LABEL: fadd_f32_generic(
 ; ALLOW:       {
diff --git a/llvm/test/CodeGen/NVPTX/atomicrmw-sm60.ll b/llvm/test/CodeGen/NVPTX/atomicrmw-sm60.ll
index 5eb508eb12e17..c85a1eaa9c982 100644
--- a/llvm/test/CodeGen/NVPTX/atomicrmw-sm60.ll
+++ b/llvm/test/CodeGen/NVPTX/atomicrmw-sm60.ll
@@ -1,7 +1,15 @@
 ; NOTE: Assertions have been autogenerated by utils/update_llc_test_checks.py UTC_ARGS: --version 6
-; RUN: llc < %s -march=nvptx64 -mcpu=sm_60 -mattr=+ptx50 | FileCheck %s --check-prefixes=SM60,SM60-NOFTZ
-; RUN: llc < %s -march=nvptx64 -mcpu=sm_60 -mattr=+ptx50 -denormal-fp-math-f32=preserve-sign | FileCheck %s --check-prefixes=SM60,SM60-FTZ
+; RUN: llc < %s -march=nvptx64 -mcpu=sm_60 -mattr=+ptx50 -nvptx-allow-ftz-atomics=false | FileCheck %s --check-prefixes=SM60,SM60-NOFTZ-DISALLOW
+; RUN: llc < %s -march=nvptx64 -mcpu=sm_60 -mattr=+ptx50 -nvptx-allow-ftz-atomics=true | FileCheck %s --check-prefixes=SM60,SM60-NOFTZ-ALLOW
+; RUN: llc < %s -march=nvptx64 -mcpu=sm_60 -mattr=+ptx50 | FileCheck %s --check-prefixes=SM60,SM60-NOFTZ-ALLOW
+; RUN: llc < %s -march=nvptx64 -mcpu=sm_60 -mattr=+ptx50 -denormal-fp-math-f32=preserve-sign -nvptx-allow-ftz-atomics=false | FileCheck %s --check-prefixes=SM60,SM60-FTZ-DISALLOW
+; RUN: llc < %s -march=nvptx64 -mcpu=sm_60 -mattr=+ptx50 -denormal-fp-math-f32=preserve-sign -nvptx-allow-ftz-atomics=true | FileCheck %s --check-prefixes=SM60,SM60-FTZ-ALLOW
+; RUN: llc < %s -march=nvptx64 -mcpu=sm_60 -mattr=+ptx50 -denormal-fp-math-f32=preserve-sign | FileCheck %s --check-prefixes=SM60,SM60-FTZ-ALLOW
+; RUN: %if ptxas-sm_60 && ptxas-isa-5.0 %{ llc < %s -march=nvptx64 -mcpu=sm_60 -mattr=+ptx50 -nvptx-allow-ftz-atomics=false | %ptxas-verify -arch=sm_60 %}
+; RUN: %if ptxas-sm_60 && ptxas-isa-5.0 %{ llc < %s -march=nvptx64 -mcpu=sm_60 -mattr=+ptx50 -nvptx-allow-ftz-atomics=true | %ptxas-verify -arch=sm_60 %}
 ; RUN: %if ptxas-sm_60 && ptxas-isa-5.0 %{ llc < %s -march=nvptx64 -mcpu=sm_60 -mattr=+ptx50 | %ptxas-verify -arch=sm_60 %}
+; RUN: %if ptxas-sm_60 && ptxas-isa-5.0 %{ llc < %s -march=nvptx64 -mcpu=sm_60 -mattr=+ptx50 -denormal-fp-math-f32=preserve-sign -nvptx-allow-ftz-atomics=false | %ptxas-verify -arch=sm_60 %}
+; RUN: %if ptxas-sm_60 && ptxas-isa-5.0 %{ llc < %s -march=nvptx64 -mcpu=sm_60 -mattr=+ptx50 -denormal-fp-math-f32=preserve-sign -nvptx-allow-ftz-atomics=true | %ptxas-verify -arch=sm_60 %}
 ; RUN: %if ptxas-sm_60 && ptxas-isa-5.0 %{ llc < %s -march=nvptx64 -mcpu=sm_60 -mattr=+ptx50 -denormal-fp-math-f32=preserve-sign | %ptxas-verify -arch=sm_60 %}
 
 define i8 @xchg_acq_rel_i8_global_cta(ptr addrspace(1) %addr, i8 %val) {
@@ -1739,322 +1747,608 @@ define i64 @usub_sat_acq_rel_i64_global_cta(ptr addrspace(1) %addr, i64 %val) {
 }
 
 define float @fadd_acq_rel_float_global_cta(ptr addrspace(1) %addr, float %val) {
-; SM60-NOFTZ-LABEL: fadd_acq_rel_float_global_cta(
-; SM60-NOFTZ:       {
-; SM60-NOFTZ-NEXT:    .reg .pred %p<2>;
-; SM60-NOFTZ-NEXT:    .reg .b32 %r<5>;
-; SM60-NOFTZ-NEXT:    .reg .b64 %rd<2>;
-; SM60-NOFTZ-EMPTY:
-; SM60-NOFTZ-NEXT:  // %bb.0:
-; SM60-NOFTZ-NEXT:    ld.param.b32 %r2, [fadd_acq_rel_float_global_cta_param_1];
-; SM60-NOFTZ-NEXT:    ld.param.b64 %rd1, [fadd_acq_rel_float_global_cta_param_0];
-; SM60-NOFTZ-NEXT:    membar.cta;
-; SM60-NOFTZ-NEXT:    ld.volatile.global.b32 %r4, [%rd1];
-; SM60-NOFTZ-NEXT:  $L__BB60_1: // %atomicrmw.start
-; SM60-NOFTZ-NEXT:    // =>This Inner Loop Header: Depth=1
-; SM60-NOFTZ-NEXT:    add.rn.f32 %r3, %r4, %r2;
-; SM60-NOFTZ-NEXT:    atom.cta.global.cas.b32 %r1, [%rd1], %r4, %r3;
-; SM60-NOFTZ-NEXT:    setp.ne.b32 %p1, %r1, %r4;
-; SM60-NOFTZ-NEXT:    mov.b32 %r4, %r1;
-; SM60-NOFTZ-NEXT:    @%p1 bra $L__BB60_1;
-; SM60-NOFTZ-NEXT:  // %bb.2: // %atomicrmw.end
-; SM60-NOFTZ-NEXT:    membar.cta;
-; SM60-NOFTZ-NEXT:    st.param.b32 [func_retval0], %r1;
-; SM60-NOFTZ-NEXT:    ret;
+; SM60-NOFTZ-DISALLOW-LABEL: fadd_acq_rel_float_global_cta(
+; SM60-NOFTZ-DISALLOW:       {
+; SM60-NOFTZ-DISALLOW-NEXT:    .reg .pred %p<2>;
+; SM60-NOFTZ-DISALLOW-NEXT:    .reg .b32 %r<5>;
+; SM60-NOFTZ-DISALLOW-NEXT:    .reg .b64 %rd<2>;
+; SM60-NOFTZ-DISALLOW-EMPTY:
+; SM60-NOFTZ-DISALLOW-NEXT:  // %bb.0:
+; SM60-NOFTZ-DISALLOW-NEXT:    ld.param.b32 %r2, [fadd_acq_rel_float_global_cta_param_1];
+; SM60-NOFTZ-DISALLOW-NEXT:    ld.param.b64 %rd1, [fadd_acq_rel_float_global_cta_param_0];
+; SM60-NOFTZ-DISALLOW-NEXT:    membar.cta;
+; SM60-NOFTZ-DISALLOW-NEXT:    ld.volatile.global.b32 %r4, [%rd1];
+; SM60-NOFTZ-DISALLOW-NEXT:  $L__BB60_1: // %atomicrmw.start
+; SM60-NOFTZ-DISALLOW-NEXT:    // =>This Inner Loop Header: Depth=1
+; SM60-NOFTZ-DISALLOW-NEXT:    add.rn.f32 %r3, %r4, %r2;
+; SM60-NOFTZ-DISALLOW-NEXT:    atom.cta.global.cas.b32 %r1, [%rd1], %r4, %r3;
+; SM60-NOFTZ-DISALLOW-NEXT:    setp.ne.b32 %p1, %r1, %r4;
+; SM60-NOFTZ-DISALLOW-NEXT:    mov.b32 %r4, %r1;
+; SM60-NOFTZ-DISALLOW-NEXT:    @%p1 bra $L__BB60_1;
+; SM60-NOFTZ-DISALLOW-NEXT:  // %bb.2: // %atomicrmw.end
+; SM60-NOFTZ-DISALLOW-NEXT:    membar.cta;
+; SM60-NOFTZ-DISALLOW-NEXT:    st.param.b32 [func_retval0], %r1;
+; SM60-NOFTZ-DISALLOW-NEXT:    ret;
 ;
-; SM60-FTZ-LABEL: fadd_acq_rel_float_global_cta(
-; SM60-FTZ:       {
-; SM60-FTZ-NEXT:    .reg .b32 %r<3>;
-; SM60-FTZ-NEXT:    .reg .b64 %rd<2>;
-; SM60-FTZ-EMPTY:
-; SM60-FTZ-NEXT:  // %bb.0:
-; SM60-FTZ-NEXT:    ld.param.b64 %rd1, [fadd_acq_rel_float_global_cta_param_0];
-; SM60-FTZ-NEXT:    ld.param.b32 %r1, [fadd_acq_rel_float_global_cta_param_1];
-; SM60-FTZ-NEXT:    atom.cta.global.add.f32 %r2, [%rd1], %r1;
-; SM60-FTZ-NEXT:    st.param.b32 [func_retval0], %r2;
-; SM60-FTZ-NEXT:    ret;
+; SM60-NOFTZ-ALLOW-LABEL: fadd_acq_rel_float_global_cta(
+; SM60-NOFTZ-ALLOW:       {
+; SM60-NOFTZ-ALLOW-NEXT:    .reg .b32 %r<3>;
+; SM60-NOFTZ-ALLOW-NEXT:    .reg .b64 %rd<2>;
+; SM60-NOFTZ-ALLOW-EMPTY:
+; SM60-NOFTZ-ALLOW-NEXT:  // %bb.0:
+; SM60-NOFTZ-ALLOW-NEXT:    ld.param.b64 %rd1, [fadd_acq_rel_float_global_cta_param_0];
+; SM60-NOFTZ-ALLOW-NEXT:    ld.param.b32 %r1, [fadd_acq_rel_float_global_cta_param_1];
+; SM60-NOFTZ-ALLOW-NEXT:    atom.cta.global.add.f32 %r2, [%rd1], %r1;
+; SM60-NOFTZ-ALLOW-NEXT:    st.param.b32 [func_retval0], %r2;
+; SM60-NOFTZ-ALLOW-NEXT:    ret;
+;
+; SM60-FTZ-DISALLOW-LABEL: fadd_acq_rel_float_global_cta(
+; SM60-FTZ-DISALLOW:       {
+; SM60-FTZ-DISALLOW-NEXT:    .reg .b32 %r<3>;
+; SM60-FTZ-DISALLOW-NEXT:    .reg .b64 %rd<2>;
+; SM60-FTZ-DISALLOW-EMPTY:
+; SM60-FTZ-DISALLOW-NEXT:  // %bb.0:
+; SM60-FTZ-DISALLOW-NEXT:    ld.param.b64 %rd1, [fadd_acq_rel_float_global_cta_param_0];
+; SM60-FTZ-DISALLOW-NEXT:    ld.param.b32 %r1, [fadd_acq_rel_float_global_cta_param_1];
+; SM60-FTZ-DISALLOW-NEXT:    atom.cta.global.add.f32 %r2, [%rd1], %r1;
+; SM60-FTZ-DISALLOW-NEXT:    st.param.b32 [func_retval0], %r2;
+; SM60-FTZ-DISALLOW-NEXT:    ret;
+;
+; SM60-FTZ-ALLOW-LABEL: fadd_acq_rel_float_global_cta(
+; SM60-FTZ-ALLOW:       {
+; SM60-FTZ-ALLOW-NEXT:    .reg .b32 %r<3>;
+; SM60-FTZ-ALLOW-NEXT:    .reg .b64 %rd<2>;
+; SM60-FTZ-ALLOW-EMPTY:
+; SM60-FTZ-ALLOW-NEXT:  // %bb.0:
+; SM60-FTZ-ALLOW-NEXT:    ld.param.b64 %rd1, [fadd_acq_rel_float_global_cta_param_0];
+; SM60-FTZ-ALLOW-NEXT:    ld.param.b32 %r1, [fadd_acq_rel_float_global_cta_param_1];
+; SM60-FTZ-ALLOW-NEXT:    atom.cta.global.add.f32 %r2, [%rd1], %r1;
+; SM60-FTZ-ALLOW-NEXT:    st.param.b32 [func_retval0], %r2;
+; SM60-FTZ-ALLOW-NEXT:    ret;
         %retval = atomicrmw fadd ptr  addrspace(1) %addr, float %val syncscope("block") acq_rel
         ret float %retval
 }
 
 define float @fsub_acq_rel_float_global_cta(ptr addrspace(1) %addr, float %val) {
-; SM60-NOFTZ-LABEL: fsub_acq_rel_float_global_cta(
-; SM60-NOFTZ:       {
-; SM60-NOFTZ-NEXT:    .reg .pred %p<2>;
-; SM60-NOFTZ-NEXT:    .reg .b32 %r<5>;
-; SM60-NOFTZ-NEXT:    .reg .b64 %rd<2>;
-; SM60-NOFTZ-EMPTY:
-; SM60-NOFTZ-NEXT:  // %bb.0:
-; SM60-NOFTZ-NEXT:    ld.param.b32 %r2, [fsub_acq_rel_float_global_cta_param_1];
-; SM60-NOFTZ-NEXT:    ld.param.b64 %rd1, [fsub_acq_rel_float_global_cta_param_0];
-; SM60-NOFTZ-NEXT:    membar.cta;
-; SM60-NOFTZ-NEXT:    ld.volatile.global.b32 %r4, [%rd1];
-; SM60-NOFTZ-NEXT:  $L__BB61_1: // %atomicrmw.start
-; SM60-NOFTZ-NEXT:    // =>This Inner Loop Header: Depth=1
-; SM60-NOFTZ-NEXT:    sub.rn.f32 %r3, %r4, %r2;
-; SM60-NOFTZ-NEXT:    atom.cta.global.cas.b32 %r1, [%rd1], %r4, %r3;
-; SM60-NOFTZ-NEXT:    setp.ne.b32 %p1, %r1, %r4;
-; SM60-NOFTZ-NEXT:    mov.b32 %r4, %r1;
-; SM60-NOFTZ-NEXT:    @%p1 bra $L__BB61_1;
-; SM60-NOFTZ-NEXT:  // %bb.2: // %atomicrmw.end
-; SM60-NOFTZ-NEXT:    membar.cta;
-; SM60-NOFTZ-NEXT:    st.param.b32 [func_retval0], %r1;
-; SM60-NOFTZ-NEXT:    ret;
+; SM60-NOFTZ-DISALLOW-LABEL: fsub_acq_rel_float_global_cta(
+; SM60-NOFTZ-DISALLOW:       {
+; SM60-NOFTZ-DISALLOW-NEXT:    .reg .pred %p<2>;
+; SM60-NOFTZ-DISALLOW-NEXT:    .reg .b32 %r<5>;
+; SM60-NOFTZ-DISALLOW-NEXT:    .reg .b64 %rd<2>;
+; SM60-NOFTZ-DISALLOW-EMPTY:
+; SM60-NOFTZ-DISALLOW-NEXT:  // %bb.0:
+; SM60-NOFTZ-DISALLOW-NEXT:    ld.param.b32 %r2, [fsub_acq_rel_float_global_cta_param_1];
+; SM60-NOFTZ-DISALLOW-NEXT:    ld.param.b64 %rd1, [fsub_acq_rel_float_global_cta_param_0];
+; SM60-NOFTZ-DISALLOW-NEXT:    membar.cta;
+; SM60-NOFTZ-DISALLOW-NEXT:    ld.volatile.global.b32 %r4, [%rd1];
+; SM60-NOFTZ-DISALLOW-NEXT:  $L__BB61_1: // %atomicrmw.start
+; SM60-NOFTZ-DISALLOW-NEXT:    // =>This Inner Loop Header: Depth=1
+; SM60-NOFTZ-DISALLOW-NEXT:    sub.rn.f32 %r3, %r4, %r2;
+; SM60-NOFTZ-DISALLOW-NEXT:    atom.cta.global.cas.b32 %r1, [%rd1], %r4, %r3;
+; SM60-NOFTZ-DISALLOW-NEXT:    setp.ne.b32 %p1, %r1, %r4;
+; SM60-NOFTZ-DISALLOW-NEXT:    mov.b32 %r4, %r1;
+; SM60-NOFTZ-DISALLOW-NEXT:    @%p1 bra $L__BB61_1;
+; SM60-NOFTZ-DISALLOW-NEXT:  // %bb.2: // %atomicrmw.end
+; SM60-NOFTZ-DISALLOW-NEXT:    membar.cta;
+; SM60-NOFTZ-DISALLOW-NEXT:    st.param.b32 [func_retval0], %r1;
+; SM60-NOFTZ-DISALLOW-NEXT:    ret;
+;
+; SM60-NOFTZ-ALLOW-LABEL: fsub_acq_rel_float_global_cta(
+; SM60-NOFTZ-ALLOW:       {
+; SM60-NOFTZ-ALLOW-NEXT:    .reg .pred %p<2>;
+; SM60-NOFTZ-ALLOW-NEXT:    .reg .b32 %r<5>;
+; SM60-NOFTZ-ALLOW-NEXT:    .reg .b64 %rd<2>;
+; SM60-NOFTZ-ALLOW-EMPTY:
+; SM60-NOFTZ-ALLOW-NEXT:  // %bb.0:
+; SM60-NOFTZ-ALLOW-NEXT:    ld.param.b32 %r2, [fsub_acq_rel_float_global_cta_param_1];
+; SM60-NOFTZ-ALLOW-NEXT:    ld.param.b64 %rd1, [fsub_acq_rel_float_global_cta_param_0];
+; SM60-NOFTZ-ALLOW-NEXT:    membar.cta;
+; SM60-NOFTZ-ALLOW-NEXT:    ld.volatile.global.b32 %r4, [%rd1];
+; SM60-NOFTZ-ALLOW-NEXT:  $L__BB61_1: // %atomicrmw.start
+; SM60-NOFTZ-ALLOW-NEXT:    // =>This Inner Loop Header: Depth=1
+; SM60-NOFTZ-ALLOW-NEXT:    sub.rn.f32 %r3, %r4, %r2;
+; SM60-NOFTZ-ALLOW-NEXT:    atom.cta.global.cas.b32 %r1, [%rd1], %r4, %r3;
+; SM60-NOFTZ-ALLOW-NEXT:    setp.ne.b32 %p1, %r1, %r4;
+; SM60-NOFTZ-ALLOW-NEXT:    mov.b32 %r4, %r1;
+; SM60-NOFTZ-ALLOW-NEXT:    @%p1 bra $L__BB61_1;
+; SM60-NOFTZ-ALLOW-NEXT:  // %bb.2: // %atomicrmw.end
+; SM60-NOFTZ-ALLOW-NEXT:    membar.cta;
+; SM60-NOFTZ-ALLOW-NEXT:    st.param.b32 [func_retval0], %r1;
+; SM60-NOFTZ-ALLOW-NEXT:    ret;
 ;
-; SM60-FTZ-LABEL: fsub_acq_rel_float_global_cta(
-; SM60-FTZ:       {
-; SM60-FTZ-NEXT:    .reg .pred %p<2>;
-; SM60-FTZ-NEXT:    .reg .b32 %r<5>;
-; SM60-FTZ-NEXT:    .reg .b64 %rd<2>;
-; SM60-FTZ-EMPTY:
-; SM60-FTZ-NEXT:  // %bb.0:
-; SM60-FTZ-NEXT:    ld.param.b32 %r2, [fsub_acq_rel_float_global_cta_param_1];
-; SM60-FTZ-NEXT:    ld.param.b64 %rd1, [fsub_acq_rel_float_global_cta_param_0];
-; SM60-FTZ-NEXT:    membar.cta;
-; SM60-FTZ-NEXT:    ld.volatile.global.b32 %r4, [%rd1];
-; SM60-FTZ-NEXT:  $L__BB61_1: // %atomicrmw.start
-; SM60-FTZ-NEXT:    // =>This Inner Loop Header: Depth=1
-; SM60-FTZ-NEXT:    sub.rn.ftz.f32 %r3, %r4, %r2;
-; SM60-FTZ-NEXT:    atom.cta.global.cas.b32 %r1, [%rd1], %r4, %r3;
-; SM60-FTZ-NEXT:    setp.ne.b32 %p1, %r1, %r4;
-; SM60-FTZ-NEXT:    mov.b32 %r4, %r1;
-; SM60-FTZ-NEXT:    @%p1 bra $L__BB61_1;
-; SM60-FTZ-NEXT:  // %bb.2: // %atomicrmw.end
-; SM60-FTZ-NEXT:    membar.cta;
-; SM60-FTZ-NEXT:    st.param.b32 [func_retval0], %r1;
-; SM60-FTZ-NEXT:    ret;
+; SM60-FTZ-DISALLOW-LABEL: fsub_acq_rel_float_global_cta(
+; SM60-FTZ-DISALLOW:       {
+; SM60-FTZ-DISALLOW-NEXT:    .reg .pred %p<2>;
+; SM60-FTZ-DISALLOW-NEXT:    .reg .b32 %r<5>;
+; SM60-FTZ-DISALLOW-NEXT:    .reg .b64 %rd<2>;
+; SM60-FTZ-DISALLOW-EMPTY:
+; SM60-FTZ-DISALLOW-NEXT:  // %bb.0:
+; SM60-FTZ-DISALLOW-NEXT:    ld.param.b32 %r2, [fsub_acq_rel_float_global_cta_param_1];
+; SM60-FTZ-DISALLOW-NEXT:    ld.param.b64 %rd1, [fsub_acq_rel_float_global_cta_param_0];
+; SM60-FTZ-DISALLOW-NEXT:    membar.cta;
+; SM60-FTZ-DISALLOW-NEXT:    ld.volatile.global.b32 %r4, [%rd1];
+; SM60-FTZ-DISALLOW-NEXT:  $L__BB61_1: // %atomicrmw.start
+; SM60-FTZ-DISALLOW-NEXT:    // =>This Inner Loop Header: Depth=1
+; SM60-FTZ...
[truncated]

@github-actions

github-actions Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

✅ With the latest revision this PR passed the C/C++ code formatter.

@AlexMaclean

Copy link
Copy Markdown
Member

FWIW I'm in favor of this. The associativity of the float operations is already non-deterministic since it is dependent on race conditions anyway so it seems like users are probably also going to be fine with a little FTZ flushing here and there too.

@jlebar

jlebar commented Jun 26, 2026 •

Copy link
Copy Markdown
Member

To add some clarity that I don't think is obvious from the PR description:

The issue here is with code that does something like this:

addr = ...;  // all threads have same address
atomic_add(addr);
return *addr;

If atomic_add is issued in a convergent manner (i.e. all threads run it at the same time), then all threads will read the same value for *addr. If we change atomic_add into a CAS loop, then different threads may read different values for *addr.

Any code which relies on getting the same value for *addr has a race condition. Moreover because atomics are not convergent operations in LLVM, the compiler is free to expose this race even in the case where the atomic is not lowered to a CAS loop. For example the compiler is free to do the following transformation

fn orig() {
  if (foo) { ... }
  else { ... }
  atomic_add(addr);
  return *addr;
}

fn transformed() {
  if (foo) { ...; atomic_add(addr); }
  else { ...; atomic_add(addr); }
  return *addr;
}

This would expose the same problem in the user code.

If we want not to expose this problem in user code, we'd need to mark atomic_add as convergent. If we don't mark atomic_add as convergent, and someone in LLVM checks in a change that makes us more likely to make a transformation like above, are we going to revert that change too? If not, why not? What is the principle here?

Personally I don't think that we should disable a correctness fix (or any other fix) in order to preserve the behavior of programs that have race conditions. You have a clearly buggy program, the compiler change exposes the bug, that is not our problem.

Disabling this correctness fix for performance reasons is a different story and I think that's kind of what #206154 (comment) is getting at. (The theory being, the correctness issue is not that big and in many cases not even noticeable. I'm not 100% sure that's true, but I think it could be correct.)

Art says that he wants to flip it back once there's a PTX version that supports native ftz and non-ftz PTX atomic ops. Apparently this is coming soon. But I am skeptical that will actually solve his problem, for two reasons.

  1. You can only flip the default once you drop support for the old PTX versions. (Unless you're willing to have different values for this flag for old vs new ptx, but that seems extremely weird.) So the flip will actually take a while.

  2. The idea that we can flip this once we have the new PTX atomics seems to be assuming that the PTX atomics will not introduce thread divergence. I have not heard someone guarantee that. (Indeed, after Volta, most PTX instructions are allowed to introduce divergence.) Many PTX atomics already get lowered into SASS CAS loops; my expectation is that these new-to-PTX ops will also be SASS CAS loops and not native hardware instructions. So either (a) the SASS CAS loops will have the same problem as the LLVM CAS loops, or (b) the SASS CAS loops will explicitly re-sync the thread after the atomic, in which case we can get the same behavior in LLVM by emitting code to do this too, if that's the behavior we want. (But we'd eat a performance penalty.)

Having said all this, I don't object to this flag flip. It just doesn't make a lot of sense to me.

@Artem-B

Artem-B commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

I can't say that I'm happy to find myself in the position where I have to support preserving a somewhat broken status quo, but for me it boils down to logistical trade-off based on risk/reward balance, and how it affects the users' ability to reason about their code.

  • FTZ on atomics as implemented before [NVPTX] Respect FTZ flag when lowering atomicrmw fadd. #200732 is clearly broken.
  • This behavior has been around for a pretty long time and, AFAICT, there are no complaints from the users. Either they do not care much about FTZ on atomics, or the impact on numerics is limited and the users affected may have worked around the issue well enough by now. As far as the users are concerned we always generate native atomic ops for f32 additions, and that's exactly what they want.
  • Ergo, fixing FTZ correctness has a rather small chance of improving situation for the users, and a small change to disrupt/change the numeric results and performance expected by the existing code. So, even a win comes with a cost for the end user.
  • The fix effectively changes native atomic ops into CAS loops on a lot of the atomic ops in the user code, regardless of whether or which denormals handling mode is specified by the user. This serializes execution of the atomic ops and will have measurable impact on performance. In most of the cases, it will be a net-negative for the users. They likely do not care about denormals during reductions, and do care about performance. They do not gain anything practically valuable with the fix, and they do have a very tangible hit on performance.
  • [neutral] The divergence itself, while it will be a royal pain to debug for the affected users, will affect mostly the code with pre-existing undefined behavior (atomic reduction w/o sync before loading the result). We can chalk it as a user problem, and it's not part of my balance calculations.

In the end, the correctness improvement is real, but its impact is negligible in practice, and the escape hatch is still there to enable the correct behavior in the infrequent cases where it matters for the user. On the other hand, the performance impact will be widespread, and will affect most of the users who likely want the native atomic ops. It's just whole lot of extra work for everyone (even if all they have to do is add -mllvm -nvptx-allow-ftz-atomics=true) just to restore to the state before the patch.

Another argument for making it an opt-in is that the change exposed a number of higher level questions that we may need to figure out first, before making the new behavior the default. It's mostly about the users being able to reason about the FP atomics. E.g.

  • there's a real need for the user to be able to specify "I want a native atomic op here" and figure out how it's expected to interact with per-function denormal_fpenv settings.
  • if/when someone generates atomic ops in IR, should it be user's or compiler's job to do a warp sync to maintain consistent behavior of the code regardless of the pointer AS and denormal handling settings? Right now the user has no visibility into whether we generate a native op or a CAS loop. E.g. if I do warp-wide add-reduction into shared memory. Do I need to do __syncwarp after atomicAdd() or not? Without looking at compiler sources it's impossible to tell.

@Artem-B

Artem-B commented Jul 3, 2026 •

Copy link
Copy Markdown
Member Author

For what it's worth, we may not be the first ones discussing this. Clang appears to have -fatomic_ignore_denormal_mode/-fno_atomic_ignore_denormal_mode:

defm atomic_ignore_denormal_mode : BoolFOption<"atomic-ignore-denormal-mode",
LangOpts<"AtomicIgnoreDenormalMode">, DefaultFalse,
PosFlag<SetTrue, [], [ClangOption, CC1Option, FlangOption, FC1Option], "Allow">,
NegFlag<SetFalse, [], [ClangOption, FlangOption], "Disallow">,
BothFlags<[], [ClangOption, FlangOption], " atomic operations to ignore denormal mode">>;

It may be interesting to dig up their motivation for the option and the rationale for choosing the default.

Also, here's what Fable produced when I asked it's opinion on the choice of the default: https://artem-b.github.io/nvptx-ftz-atomics-default-assessment.html
I think it did a pretty decent job outlining the moving parts of the issue, and existing precedents.

The key point it produced is that:

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

@Artem-B
Artem-B force-pushed the nvptx-default-allow-ftz-atomics branch from c363b47 to 689a0cb Compare July 13, 2026 21:14
@Artem-B

Artem-B commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

Any objections to committing this patch?

I will follow up with per-instruction atomic_ignore_denormal_mode and generalizing amdgpu.ignore.denormal.mode. Once we have them in place, affected users will have precise canonical way to express what they need, and we can flip the default back to correct-by-default.

This is a follow-up to llvm#200732.

Setting the default value of nvptx-allow-ftz-atomics to true preserves
the status quo. It allows the NVPTX backend to emit native
floating-point atomic add instructions by default, rather than falling
back to the CAS loop required for strict FTZ compliance.

Correct FTZ handling forces a lot of existing code to emit CAS loops.
This introduces thread divergence, breaking code that relies on atomic
operations being non-divergent (for example, warp-wide atomic additions
followed by a load of the reduced value without explicit
synchronization).

While FTZ-correct atomic behavior is desirable as a long-term default,
introducing thread divergence into a common operation that previously
"happened to work" without synchronization is highly disruptive.
Defaulting to true makes the FTZ-correct behavior opt-in for now,
allowing users time to audit and adapt their code.

Test cases are updated to verify both enabled and disabled states of the
option, alongside the new default behavior.
@Artem-B
Artem-B force-pushed the nvptx-default-allow-ftz-atomics branch from 689a0cb to 8e13bdf Compare July 13, 2026 21:21
@rnk

rnk commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

To check my understanding: there's a bug in the user code sample. Technically speaking, programmers should insert __syncwarp / __syncthreads between the add and the load to ensure the load reads the updated value, but this is a common code pattern for programmers thinking in the SIMT model. If they don't write explicit branches, they expect converged execution, and this breaks when a branch-less operation expands to a loop or conditional.

On the other hand, we have the issue of floating point accuracy and correctness of not flushing to zero when the programmer didn't explicitly ask for it, so we're weighing FP precision against breaking technically incorrect user code.

I think the LLVM way is to worry less about breaking users relying on UB, but to offer migration paths for downstream toolchain vendors, which is what this flag does. It stands to reason that upstream would take the stance of being more technically correct, allow downstreams to complete this migration to align with the upstream default at some point in the future, and then we'd move on.

I'm not an FP expert, but there's a bell going off in my head from all the graphics programmers I've met over the years who have insisted to me that denormals are a performance trap, so at least for real-time applications, you always want FTZ.

Casual research shows that CUDA, Triton, and XLA all default to FTZ, so it seems like the blast radius here should be pretty small, and I'm surprised we're considering this workaround. It sounds like noftz is a build configuration you maintain for testing purposes to measure the impact of FTZ on the output.

@Artem-B

Artem-B commented Jul 15, 2026

Copy link
Copy Markdown
Member Author

To check my understanding: there's a bug in the user code sample. Technically speaking, programmers should insert __syncwarp / __syncthreads between the add and the load to ensure the load reads the updated value, but this is a common code pattern for programmers thinking in the SIMT model. If they don't write explicit branches, they expect converged execution, and this breaks when a branch-less operation expands to a loop or conditional.

On the other hand, we have the issue of floating point accuracy and correctness of not flushing to zero when the programmer didn't explicitly ask for it, so we're weighing FP precision against breaking technically incorrect user code.

UB code is how we found the issue, but since then, I've got a few more users that were affected by performance regression on a valid code. We do have the escape hatch option, so it's not a showstopper, but I'm willing to bet that there are more affected users why didn't notice the problem yet.

The more important issue is that we currently have no way to let users specify exactly what they want. I.e. if a function uses generic pointers (but the user knows the address space) LLVM will always use CAS. Or if the user mixes global and shared atomics in the same functions, only one of them will be fast, and the other will use CAS, regardless of FTZ setting.

I'm not an FP expert, but there's a bell going off in my head from all the graphics programmers I've met over the years who have insisted to me that denormals are a performance trap, so at least for real-time applications, you always want FTZ.

I'd say that they are more keen on "you always want fast", and it's not the same as "you always want FTZ" (not on NVIDIA GPUs, at least), and we currently do not have sufficiently flexible controls to let them achieve that goal.

Casual research shows that CUDA, Triton, and XLA all default to FTZ, so it seems like the blast radius here should be pretty small, and I'm surprised we're considering this workaround. It sounds like noftz is a build configuration you maintain for testing purposes to measure the impact of FTZ on the output.

Whichever FTZ mode they pick, some of the atomics will end up as CAS. Most of the time the impact will be negligible, but there are cases where regression is noticeable-to-catastrophic.

My proposal is to roll back default to the old behavior to avoid unnecessarily disrupting the users with valid code while we have no good solution for them, implement AMD-style per-instruction FTZ control, and then switch back to "correct by default". If/when somebody needs atomic to produce just-so variant, we can point to the docs and tell them how they can express that in IR or CUDA.

In short, I agree with your points and and that LLVM should generate correct code by default, I'm trying to do it in the way that's less painful for the users. Considering that we lived with the old behavior for a long time with nobody complaining about it, and that the current correct code causes noticeable perf regressions, I think it makes sense to avoid disrupting users until we can give them a usable way to achieve what they need.

An alternative is to tell every affected user to use the escape hatch option, but then we'll have a problem of removing that option later on, as we'll grow an unknown number of users that depend on it.

@rnk rnk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I talked to some folks here at NVIDIA, and I think I agree with Art. I can't make hard commitments about product release dates and which fixes will be in them, but let's revisit this in the fall, and see if there are new ptxas features that allow us to fix this in a way that gives users the best possible behavior with the least disruption.

I'll go look for this in the issue tracker and write that down as the next step.

@Artem-B
Artem-B merged commit 2b6e9d2 into llvm:main Jul 17, 2026
11 checks passed
maleadt added a commit to JuliaPackaging/Yggdrasil that referenced this pull request Sep 22, 2026
maleadt added a commit to JuliaGPU/CUDA.jl that referenced this pull request Sep 22, 2026
NVPTX_LLVM_Backend_jll 23.1.2+0 backports llvm/llvm-project#206154, which
lowers global Float32 atomic additions to native, subnormal-flushing
atom.add.f32 regardless of the kernel's denormal mode. Fast math therefore
no longer changes atomic lowering, and the tests that expected a CAS loop
(and preserved subnormals) in IEEE mode fail.

Probe the option with a plain addition instead, which selects add.ftz.f32
under fast math, drop the atomic lowering tests, and remove the atomics
motivation from the performance tutorial.
@ilya-biryukov

Copy link
Copy Markdown
Contributor

It seems that #200732 made it into the Clang 23 release and this change did not. From the discussion here and the fact that the PR landed, should we patch it into the next minor Clang 23 release?

@Artem-B

Artem-B commented Oct 3, 2026

Copy link
Copy Markdown
Member Author

It seems that #200732 made it into the Clang 23 release and this change did not. From the discussion here and the fact that the PR landed, should we patch it into the next minor Clang 23 release?

Yes, we probably do want to avoid atomics regression in a new release. Cherry-picking this change is a low-hanging fruit.

@chsigg would be the person to talk to about that.

@rnk

rnk commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

I agree, we should cherry pick it, I think we have some scripts to automate that.

@github-project-automation github-project-automation Bot moved this to Needs Triage in LLVM Release Status Oct 5, 2026
@ilya-biryukov

Copy link
Copy Markdown
Contributor

@llvmbot

llvmbot commented Oct 5, 2026

Copy link
Copy Markdown
Member

/pull-request #229003

@ilya-biryukov

ilya-biryukov commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

#229003 needs a stamp and I don't feel bold enough to do this myself given limited experience with NVPTX.
The presubmits actual seem to be clean, failures failures in libc++ buildbots look unrelated (it can't find a commit by its SHA hash in the logs)

@Artem-B @rnk would any of you mind stamping the PR?

tru pushed a commit that referenced this pull request Oct 12, 2026
This is a follow-up to #200732.

Setting the default value of nvptx-allow-ftz-atomics to true preserves
the status quo. It allows the NVPTX backend to emit native
floating-point atomic add instructions by default, rather than falling
back to the CAS loop required for strict FTZ compliance.

Correct FTZ handling forces a lot of existing code to emit CAS loops.
This introduces thread divergence, breaking code that relies on atomic
operations being non-divergent (for example, warp-wide atomic additions
followed by a load of the reduced value without explicit
synchronization).

While FTZ-correct atomic behavior is desirable as a long-term default,
introducing thread divergence into a common operation that previously
"happened to work" without synchronization is highly disruptive.
Defaulting to true makes the FTZ-correct behavior opt-in for now,
allowing users time to audit and adapt their code.

Test cases are updated to verify both enabled and disabled states of the
option, alongside the new default behavior.

(cherry picked from commit 2b6e9d2)
chsigg added a commit that referenced this pull request Oct 13, 2026
This restores the strict FTZ default originally introduced in #200732
and temporarily relaxed in #206154.

Frontends can now explicitly opt in to native FTZ-mismatched atomics on a
per-instruction basis using !atomic.ignore.denormal.mode metadata (#217585,
#217586), which is supported in Clang, MLIR, and XLA. Users who need the
previous behavior globally can still pass -nvptx-allow-ftz-atomics=true.

Update test cases to verify the new default disallow behavior.
chsigg added a commit that referenced this pull request Oct 13, 2026
This restores the strict FTZ default originally introduced in #200732
and temporarily relaxed in #206154.

Frontends can now explicitly opt in to native FTZ-mismatched atomics on a
per-instruction basis using !atomic.ignore.denormal.mode metadata (#217585,
#217586), which is supported in Clang, MLIR, and XLA. Users who need the
previous behavior globally can still pass -nvptx-allow-ftz-atomics=true.

Update test cases to verify the new default disallow behavior.
chsigg added a commit that referenced this pull request Oct 14, 2026
This restores the strict FTZ default originally introduced in #200732
and temporarily relaxed in #206154.

Frontends can now explicitly opt in to native FTZ-mismatched atomics on a
per-instruction basis using !atomic.ignore.denormal.mode metadata (#217585,
#217586), which is supported in Clang, MLIR, and XLA. Users who need the
previous behavior globally can still pass -nvptx-allow-ftz-atomics=true.

Update test cases to verify the new default disallow behavior.
chsigg added a commit that referenced this pull request Oct 16, 2026
This restores the strict FTZ default originally introduced in #200732
and temporarily relaxed in #206154.

Frontends can now explicitly opt in to native FTZ-mismatched atomics on a
per-instruction basis using !atomic.ignore.denormal.mode metadata (#217585,
#217586), which is supported in Clang, MLIR, and XLA. Users who need the
previous behavior globally can still pass -nvptx-allow-ftz-atomics=true.

Update test cases to verify the new default disallow behavior.
chsigg added a commit that referenced this pull request Oct 17, 2026
This restores the strict FTZ default originally introduced in #200732
and temporarily relaxed in #206154.

Frontends can now explicitly opt in to native FTZ-mismatched atomics on a
per-instruction basis using !atomic.ignore.denormal.mode metadata (#217585,
#217586), which is supported in Clang, MLIR, and XLA. Users who need the
previous behavior globally can still pass -nvptx-allow-ftz-atomics=true.

Update test cases to verify the new default disallow behavior.
chsigg added a commit that referenced this pull request Oct 18, 2026
This restores the strict FTZ default originally introduced in #200732
and temporarily relaxed in #206154.

Frontends can now explicitly opt in to native FTZ-mismatched atomics on a
per-instruction basis using `!atomic.ignore.denormal.mode` metadata (#217585,
#217586, #217587, #217588, #232725, #232726), which is supported in Clang,
Flang, MLIR, and XLA. Users who need the previous behavior globally can still
pass `-nvptx-allow-ftz-atomics=true`.

Update test cases to verify the new default disallow behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Development

Successfully merging this pull request may close these issues.

6 participants