Nvfortran 26.9: wrong results with private + intent(out) argument to declare target callee in target teams loop

Another bug report on the nvfortran compiler + openmp in GPUs. Take a look, thanks!

nvfortran_private_callee_bug.tar.gz (3.9 KB)

nvfortran 26.9: wrong results on GPU for a private() scalar passed as an INTENT(OUT)
actual argument to a “declare target” subroutine inside “!$omp target teams loop”
(plus an observation: “target teams loop” runs one thread per team when the body
calls a declare-target routine)

Summary
Reproducer 1 (priv_callee.F90). A scalar h is listed in private(h) on
“!$omp target teams loop collapse(3)” and is passed as the INTENT(OUT) actual
argument of a “declare target” subroutine (call callee(a(i,j,k), h)). The value
read back from h afterwards is garbage. With a(:) random in [0,1) the expected
result is b = (2a+1)*a; at -O2 all 4096 of 4096 elements are wrong.
Correct in each of these variants of the same loop:

  • no private clause (h then shared/implicit),
  • a declare-target FUNCTION returning the value instead of an INTENT(OUT) argument,
  • “target teams distribute parallel do” with private(h),
  • -O2 with -Minline (callee inlined) in this small case.
    Wrong at -O1 and -O2 without -Minline. At -O0 the file does not compile (see
    “Other observation” below), so -O0 could not be used as a reference here.
    In the full application kernel the result is wrong with -Minline as well.

Reproducer 2 (slow_teams_loop.F90). Same loop body, 192^3 cells, 20 timed
repetitions. -Minfo=mp shows “Loop parallelized across teams collapse(3) !
blockidx%x” with NO threads(128) for the teams-loop kernel that calls a
declare-target routine, i.e. one thread per block. Best-of-20 kernel time
6.7 ms versus 0.5-0.6 ms for “target teams distribute parallel do” (about 12-13x).
A teams-loop kernel with a private local array w(3) and no call was NOT slow in
this small test (0.5 ms, threads(128) generated), so the local-array case seen in
the full application is not reproduced here; only the callee case is.

Environment
nvfortran 26.9-0 64-bit, target x86-64 Linux (-tp alderlake), bundled CUDA 13.3
NVIDIA GeForce RTX 4070 Laptop GPU, compute capability 8.9, driver 580.173.02
Pop!_OS 24.04 LTS (Ubuntu 24.04 base)

Files
priv_callee.F90 reproducer 1: module with callee/fcallee and 4 loop variants, test program
slow_teams_loop.F90 reproducer 2: timing of teams loop vs teams distribute parallel do
build_run.sh builds and runs both (edit NV= if the SDK is installed elsewhere;
CC_=89 selects the GPU architecture)
output.txt complete compiler -Minfo output and program output from build_run.sh

To reproduce
bash build_run.sh
or by hand:
nvfortran -O2 -mp=gpu -gpu=cc89,nofma priv_callee.F90 -o priv_callee && ./priv_callee
nvfortran -O2 -mp=gpu -gpu=cc89,nofma slow_teams_loop.F90 -o slow_teams_loop && ./slow_teams_loop

Expected (reproducer 1): every variant prints “0 of 4096 wrong OK”.
Actual at -O2 (from output.txt):
variant 1: teams loop + private(h) + intent(out) actual (subroutine) : 4096 of 4096 wrong WRONG
variant 2: teams loop, no private clause (subroutine) : 0 of 4096 wrong OK
variant 3: teams loop + private(h), function result : 0 of 4096 wrong OK
variant 4: teams distribute parallel do + private(h) (subroutine) : 0 of 4096 wrong OK
(same at -O1; with -O2 -Minline variant 1 prints “0 of 4096 wrong OK”)

Expected (reproducer 2): teams loop and teams distribute parallel do take about the same time.
Actual at -O2 (from output.txt):
teams loop + callee : 6.658 ms (best of 20)
teams distribute parallel do + callee : 0.562 ms (best of 20)
teams loop + local array: 0.564 ms (best of 20)
teams distribute parallel do + local array: 0.568 ms (best of 20)
slowdown teams loop / dpf: callee 11.8x local array 1.0
(timings vary a little run to run; an earlier run gave 6.657 / 0.495 ms, 13.4x)

Other observation
Compiling priv_callee.F90 with -O0 -mp=gpu fails in the device compiler:
nvvmCompileProgram: failed with error 9: NVVM_ERROR_COMPILATION
nvnvvmd: error: … parse invalid cast opcode for cast from ‘double’ to ‘i8*’
NVFORTRAN-W-0155-Compiler failed to translate accelerator region …
(build_run.sh does not run -O0 for this reason.) Not investigated further.

Workaround
Use “target teams distribute parallel do” with private(h), or avoid the INTENT(OUT)
argument by using a declare-target function result, or drop the private clause
under “teams loop” (only safe where no thread-sharing of the scalar can occur).

Hi marcos.vanella and thanks for the reports!

For the performance issue, this is a known issue with the OpenMP standard for “loop”.

The issue being that since you have a call, the compiler must presume that the subroutine contains a parallel loop so can only schedule the outer loops across the teams. Hence only one thread is active and you get the poor performance.

OpenACC has the “routine” directive for this, but no similar directive exists in OpenMP. Instead, you need to use the “bind” clause to define the schedule.

!$omp target teams loop collapse(3) private(h) bind(teams,parallel)
do k = 0, n; do j = 0, n; do i = 0, n
   call callee(a(i,j,k), h); b(i,j,k) = h*a(i,j,k)
enddo; enddo; enddo

Note that “bind(teams,parallel)” is an NVHPC extension so may not be accepted by other compilers. If you need to code to be portable, you’ll need to split the loops.

!$omp target teams loop collapse(2)
do k = 0, n; do j = 0, n
!$omp loop private(h)
  do i = 0, n
   call callee(a(i,j,k), h); b(i,j,k) = h*a(i,j,k)
enddo; enddo; enddo

For the correctness issue, I filed a problem report, TPR #39018. Adding “bind(teams,parallel)” works around the issue as does initializing “h” before the call.

!$omp target teams loop collapse(3) private(h) bind(teams, parallel)  !<< add either this
!omp target teams loop collapse(3) private(h)
do k = 0, n; do j = 0, n; do i = 0, n
    h = 0.0  ! << or this
   call callee(a(i,j,k), h); b(i,j,k) = h*a(i,j,k)
enddo; enddo; enddo

-Mat

Hi Mat, thanks. I confirmed both points on our real kernels (nvfortran 26.9, RTX 4070 Laptop GPU, cc 8.9, 128³ grid, 100 repetitions, results compared bitwise against a CPU reference).

Performance: bind(teams,parallel) fixes it. For example, one kernel with a callee drops from 11,276 µs with plain target teams loop to 643 µs, matching target teams distribute parallel do within about 3% on all seven kernels.

Correctness: two of our seven kernels give wrong results with plain target teams loop and a private scalar passed to the callee (4096 of 4096 elements wrong in your small reproducer). bind(teams,parallel) fixes both.

One caveat on the second workaround you suggested, initializing the private variable before the call: it fixes the small reproducer, but not our real kernels. With the variable set to zero before each call, one kernel became correct while the other stayed wrong, and both stayed slow. Compiling with -Minline also hides the failure in the reproducer, but the real kernels still fail with it. So bind(teams,parallel) is the only workaround that has held for us in every case. You may want to add the real-kernel case to the report.

Also, -O0 -mp=gpu fails in the device compiler with an NVVM error (“invalid cast from double to i8*”) on the reproducer; details are in the README of the package.

Thanks again.