CONTIGUOUS copy-in skipped through assumed-shape wrappers — silent data corruption

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.

Hi gio.cava10,

Thanks for the report and welcome!

I filed a report, TPR #38279, and sent it to our engineers for investigation.

-Mat