[BUG] NVFortran HPC SDK 24.7 — Silent Constant Folding Error for BTEST on INTEGER(1) in OpenACC Device Routines

[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.

Hi chen.weilun and welcome!

Thanks for the report and nice reproducing example. I’ve filed a report, TPR#38285, and sent to engineering for investigation.

-Mat