[BUG] NVFortran HPC SDK 24.7 — Silent Constant Folding Error for BTEST on INTEGER(1) in OpenACC Device Routines
Hi All,
I’ve been porting an ocean simulation model to GPU using OpenACC, and after days of debugging unexpected numerical drift in the particle boundary repulsion logic, I finally tracked it down to what I believe is a compiler bug in NVFortran 24.7.
Posting here in case others hit the same thing — it’s a silent correctness failure, which makes it especially nasty to find.
Environment
| Compiler | NVFortran / HPC SDK 24.7 |
| CUDA | 12.5 |
| GPU / Arch | RTX 4090 / sm_89 |
| Mode | OpenACC with !$acc routine seq |
What’s Happening
In the ocean model, boundary mask information is packed into INTEGER(1) bit fields for memory efficiency. During porting, I started getting incorrect particle repulsion at domain boundaries — particles were being pushed away from walls they should have been ignoring.
After some digging, I isolated it to this pattern:
x = MERGE(max(x, repul), x, .NOT. BTEST(m_packed, 1))
For m_packed = -1_1 (all bits set), .NOT. BTEST(m_packed, 1) should be .FALSE. — bit 1 is set, so BTEST returns .TRUE., and .NOT. flips it. The MERGE should return x unchanged.
On GPU, it returns max(x, repul) instead. The compiler is folding the condition to .TRUE. at compile-time.
My hypothesis: the constant folding pass doesn’t correctly handle 8-bit sign extension when evaluating BTEST at compile-time for device code. -1_1 in two’s complement is 0xFF, but I suspect the optimizer is treating it as a wider integer before the bit test, producing a wrong result.
Minimal Reproducible Example
module bug_mod
implicit none
contains
subroutine test_cross_logic(x_btest, x_iand, m_packed, repul)
!$acc routine seq
real, intent(inout) :: x_btest, x_iand
integer(1), intent(in) :: m_packed
real, intent(in) :: repul
! [BUG] — condition incorrectly folded to .TRUE. on GPU
x_btest = MERGE(max(x_btest, repul), x_btest, .NOT. BTEST(m_packed, 1))
! [WORKAROUND] — equivalent IAND logic works correctly
x_iand = MERGE(max(x_iand, repul), x_iand, IAND(m_packed, 2_1) == 0_1)
end subroutine test_cross_logic
end module bug_mod
program btest_bug_test
use bug_mod
implicit none
integer(1) :: m_packed = -1_1 ! All bits set
real :: x_btest = 4.0e-6, x_iand = 4.0e-6, repul = 0.01
!$acc parallel copyin(m_packed, repul) copy(x_btest, x_iand)
call test_cross_logic(x_btest, x_iand, m_packed, repul)
!$acc end parallel
print *, "BTEST Result (Expected 4.0E-06): ", x_btest
print *, "IAND Result (Expected 4.0E-06): ", x_iand
end program btest_bug_test
Compile:
nvfortran -acc -gpu=cc89 -O2 merge_bug.f90 -o merge_bug
Expected:
BTEST Result (Expected 4.0E-06): 4.000000E-06
IAND Result (Expected 4.0E-06): 4.000000E-06
Actual:
BTEST Result (Expected 4.0E-06): 1.000000E-02 ← Wrong
IAND Result (Expected 4.0E-06): 4.000000E-06 ← Correct
I also confirmed that -O0 produces the correct result for BTEST, which points squarely at an optimization pass issue.
SASS Confirms It
The generated SASS for test_cross_logic shows no bitwise instructions and no predication — the conditional was completely eliminated:
/*0040*/ LDG.E R0, [R2.64] ;
/*0050*/ FMNMX.FTZ R5, R0, 0.01, !PT ; // Unconditional max
/*0060*/ STG.E [R2.64], R5 ; // Unconditionally stores 0.01
/*0070*/ EXIT ;
I’d expect to see something like LOP3 or BFE for the bit test, and a predicated SEL or branch for the MERGE. None of that is there. The optimizer decided the condition was always true and removed the entire branch.
I also verified with cuobjdump on both -O0 and -O2 builds — the -O0 SASS correctly generates the predicated path.
Workaround
Replacing BTEST with an equivalent IAND check restores correct behavior:
! Buggy
x = MERGE(max(x, repul), x, .NOT. BTEST(m_packed, 1))
! Fixed
x = MERGE(max(x, repul), x, IAND(m_packed, 2_1) == 0_1)
This is semantically identical, but the IAND path generates correct SASS with proper predication.