nvfortran 26.1: CONTIGUOUS copy-in silently skipped through assumed-shape wrappers
nvfortran 26.1 silently skips the required copy-in when a non-contiguous array section passes through assumed-shape (dimension(:,:,:)) wrapper subroutines before reaching a CONTIGUOUS dummy. This causes silent data corruption with no warning or runtime error.
The bug does not occur when the section is passed directly (1-layer call). It only triggers when there is at least one intermediate wrapper without CONTIGUOUS on its dummy.
This may be related to TPR #33110 (previous thread), which was reported fixed in 23.5.
Standard reference — F2008 12.5.2.7: if the dummy has CONTIGUOUS and the actual argument is not simply contiguous, the compiler must copy into a contiguous temporary.
Environment: nvfortran 26.1-0, x86-64 Linux. gfortran 11.5 produces correct results.
Reproducer
Three files, no MPI, no external libraries. I found this bug in CaNS using 2decomp-fft, but this reproducer isolates it to pure Fortran.
mod_leaf.f90 — leaf subroutine with CONTIGUOUS dummy. Uses c_loc/c_f_pointer to access raw contiguous memory (same as what MPI I/O does with the buffer address):
module mod_leaf
use iso_fortran_env, only: real64
use iso_c_binding, only: c_loc, c_f_pointer, c_ptr
implicit none
private
integer, parameter, public :: wp = real64
type, public :: decomp_info_t
integer :: xsz(3) = 0
end type decomp_info_t
interface write_plane
module procedure write_plane_3d_real
end interface write_plane
public :: write_plane
contains
subroutine write_plane_3d_real(ipencil, var, iplane, n, &
dirname, varname, io_name, opt_decomp)
integer, intent(in) :: ipencil
real(wp), contiguous, dimension(:,:,:), intent(in), target :: var
integer, intent(in) :: iplane, n
character(len=*), intent(in) :: dirname, varname, io_name
type(decomp_info_t), intent(in), optional :: opt_decomp
integer :: n1, n2, n3, iu, plane_size
real(wp), pointer :: flat(:)
type(c_ptr) :: cptr
n1 = size(var,1); n2 = size(var,2); n3 = size(var,3)
cptr = c_loc(var(1,1,1))
call c_f_pointer(cptr, flat, [n1*n2*n3])
open(newunit=iu, file=trim(dirname)//'/'//trim(varname), &
form='unformatted', access='stream', status='replace', action='write')
select case (iplane)
case (3)
plane_size = n1 * n2
write(iu) flat((n-1)*plane_size + 1 : n*plane_size)
end select
close(iu)
end subroutine write_plane_3d_real
end module mod_leaf
mod_wrappers.f90 — two thin assumed-shape wrappers, no CONTIGUOUS:
module mod_wrappers
use mod_leaf, only: wp, write_plane
implicit none
private
public :: wrapper_outer, wrapper_inner
contains
subroutine wrapper_outer(fname, iplane, islice, ipencil, var)
character(len=*), intent(in) :: fname
integer, intent(in) :: iplane, islice, ipencil
real(wp), dimension(:,:,:), intent(in) :: var
call wrapper_inner(fname, iplane, islice, ipencil, var)
end subroutine wrapper_outer
subroutine wrapper_inner(fname, iplane, islice, ipencil, var)
character(len=*), intent(in) :: fname
integer, intent(in) :: iplane, islice, ipencil
real(wp), dimension(:,:,:), intent(in) :: var
call write_plane(ipencil, var, iplane, islice, '.', fname, 'dummy')
end subroutine wrapper_inner
end module mod_wrappers
test_standalone.f90 — allocates an array with halos, writes the same plane three ways:
program test_standalone
use mod_leaf, only: wp, write_plane
use mod_wrappers, only: wrapper_outer
implicit none
integer, parameter :: NX = 192, NY = 192, NZ = 100
real(wp), allocatable :: a(:,:,:), a_contig(:,:,:)
integer :: i, j, k
allocate(a(0:NX+1, 0:NY+1, 0:NZ+1))
a = -999.0_wp
do k = 1, NZ; do j = 1, NY; do i = 1, NX
a(i,j,k) = real(10000*i + 100*j + k, wp)
end do; end do; end do
! Test 1: non-contiguous section through 2-layer wrapper
call wrapper_outer('test1_noncontig.bin', 3, 1, 3, a(1:NX,1:NY,1:NZ))
! Test 2: contiguous temporary through same wrapper
allocate(a_contig(NX,NY,NZ))
a_contig = a(1:NX,1:NY,1:NZ)
call wrapper_outer('test2_contig.bin', 3, 1, 3, a_contig)
! Test 3: non-contiguous section, direct call (no wrappers)
call write_plane(3, a(1:NX,1:NY,1:NZ), 3, 1, '.', 'test3_direct.bin', 'dummy')
write(*,'(a)') 'Verify: cmp test1_noncontig.bin test2_contig.bin'
end program
Build and run
nvfortran -O3 -fast -c mod_leaf.f90 mod_wrappers.f90 test_standalone.f90
nvfortran -O3 -fast mod_leaf.o mod_wrappers.o test_standalone.o -o test
./test
cmp test1_noncontig.bin test2_contig.bin # DIFFER = bug
cmp test3_direct.bin test2_contig.bin # identical = direct call OK
Also reproduces at -O0.
Results
Test 1 (non-contiguous, 2 wrappers): FAIL — 36672/36864 values wrong, 380 halo sentinels
Test 2 (contiguous tmp, 2 wrappers): PASS
Test 3 (non-contiguous, direct call): PASS
gfortran: all three pass.
-Minfo=all diagnostic
Compiling mod_wrappers.f90:
wrapper_outer:
13, subprogram not inlined -- array reshaping not enabled: wrapper_inner, argument 5
The compiler sees that array reshaping (= contiguous copy) is needed but decides not to inline — and also does not generate a copy-in. The non-contiguous descriptor passes through unchanged.
Compiling test_standalone.f90 (the direct call):
test_standalone:
21, Copy in of a in call to write_plane_3d_real
Here it correctly generates the copy-in. The bug is that copy-in is only generated for direct calls, not through wrappers.
I bisected this pretty thoroughly: call depth >= 2 is the only trigger — array size, optimization level (-O0 through -O3 -fast), -Mnoipa, and compilation unit layout are all irrelevant. The workaround is to manually copy the non-contiguous section into a contiguous temporary before the call, but obviously the compiler should be doing this for us.