Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions src/drivers/ncmpio/ncmpio_getput.m4
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ put_varm(NC *ncp,
MPI_Offset nelems=0, bnelems=0, nbytes=0;
MPI_Datatype itype, imaptype;

if (varp == NULL) { /* zero-sized request */
if (varp == NULL || fIsSet(reqMode, NC_REQ_ZERO)) { /* zero-sized request */
itype = MPI_BYTE;
el_size = 0;
bnelems = 0;
Expand Down Expand Up @@ -345,7 +345,7 @@ get_varm(NC *ncp,
MPI_Offset nelems=0, bnelems=0, nbytes=0;
MPI_Datatype itype, imaptype=MPI_DATATYPE_NULL;

if (varp == NULL) { /* zero-sized request */
if (varp == NULL || fIsSet(reqMode, NC_REQ_ZERO)) { /* zero-sized request */
itype = MPI_BYTE;
el_size = 0;
bnelems = 0;
Expand Down Expand Up @@ -509,14 +509,16 @@ ncmpio_$1_var(void *ncdp,

{
NC *ncp=(NC*)ncdp;
NC_var *varp;
NC_var *varp=NULL;

/* Check if this is a true zero-sized request. Note NC_REQ_ZERO is added to
* reqMode only when an error is detected at the dispatcher level.
* reqMode only when an error is detected at the dispatcher level, in which
* case varid may not be valid and must not be used to look up varp.
*/
if (!fIsSet(reqMode, NC_REQ_ZERO)) {
int i;
for (i=0; i<ncp->vars.value[varid]->ndims; i++)
varp = ncp->vars.value[varid];
for (i=0; i<varp->ndims; i++)
if (count[i] == 0) {
reqMode |= NC_REQ_ZERO;
break;
Expand All @@ -529,8 +531,17 @@ ncmpio_$1_var(void *ncdp,
/* In case some processes in an aggregation group have nothing to
* write, they still need to participate the communication part of the
* intra-node aggregation operation.
*
* varp is handed down whenever it is known to be valid, because
* put_varm() consults it to decide whether this is a record variable.
* A zero-length collective request to a record variable must still
* take part in the MPI_Allreduce() and ncmpio_write_numrecs() that
* synchronise numrecs: the processes that do have data call them
* unconditionally, so a process skipping them leaves the communicator
* out of step. varp stays NULL only when NC_REQ_ZERO came from a
* dispatcher-level error, where varid cannot be trusted.
*/
return $1_varm(ncp, NULL, NULL, NULL, NULL, imap, NULL, 0,
return $1_varm(ncp, varp, NULL, NULL, NULL, imap, NULL, 0,
buftype, reqMode);
}

Expand Down
1 change: 1 addition & 0 deletions test/testcases/Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ check_PROGRAMS = file_create_open \
put_all_kinds \
redef1 \
iput_all_kinds \
tst_zero_len_recvar \
tst_version

M4_SRCS = put_all_kinds.m4 \
Expand Down
147 changes: 147 additions & 0 deletions test/testcases/tst_zero_len_recvar.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/******************************************************************************
*
* Copyright (C) 2026, Northwestern University and Argonne National Laboratory
* See COPYRIGHT notice in top-level directory.
*
*****************************************************************************/
/* $Id$ */

/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* This program tests a collective write to a record variable in which one of
* the processes has nothing to contribute, i.e. it passes count == 0. Every
* process still calls the collective API, so the call is legal.
*
* Such a request must not opt out of the collective synchronization of numrecs
* that follows the I/O. If it does, the processes that do have data to write
* are left calling an MPI_Allreduce() alone and the communicator goes out of
* step, which shows up as a wrong error code, as corruption of a later
* collective call, or as a hang.
*
* Fixed-size variables take no part in the numrecs synchronization and are
* therefore not affected; the record variable is the case worth guarding.
*
* To compile:
* mpicc -O2 tst_zero_len_recvar.c -o tst_zero_len_recvar -lpnetcdf
*
* % mpiexec -n 4 ./tst_zero_len_recvar testfile.nc
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <libgen.h> /* basename() */
#include <pnetcdf.h>

#include <testutils.h>

#define NDBL 4 /* number of doubles written per process */

static
int test_io(const char *out_path,
const char *in_path, /* ignored */
int format,
int coll_io,
MPI_Info info)
{
int i, err, nerrs=0, ncid, dimid[2], varid, rank, nprocs, is_empty;
MPI_Offset start[2], count[2], numrecs;
double buf[NDBL], rbuf[NDBL];

MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &nprocs);

/* the last process contributes nothing; with a single process there is
* nobody left to write, so keep its request non-empty
*/
is_empty = (nprocs > 1 && rank == nprocs - 1);

for (i=0; i<NDBL; i++) buf[i] = (double)(rank * NDBL + i);

err = ncmpi_set_default_format(format, NULL); CHECK_ERR

err = ncmpi_create(MPI_COMM_WORLD, out_path, NC_CLOBBER, info, &ncid); CHECK_ERR
err = ncmpi_def_dim(ncid, "time", NC_UNLIMITED, &dimid[0]); CHECK_ERR
err = ncmpi_def_dim(ncid, "nelem", nprocs * NDBL, &dimid[1]); CHECK_ERR
err = ncmpi_def_var(ncid, "v", NC_DOUBLE, 2, dimid, &varid); CHECK_ERR
err = ncmpi_set_fill(ncid, NC_NOFILL, NULL); CHECK_ERR
err = ncmpi_enddef(ncid); CHECK_ERR

/* all processes write into record 0 */
start[0] = 0;
count[0] = 1;
start[1] = (MPI_Offset)rank * NDBL;
count[1] = NDBL;
if (is_empty) { start[1] = 0; count[1] = 0; }

if (coll_io) {
err = ncmpi_put_vara_double_all(ncid, varid, start, count, buf);
CHECK_ERR
}
else {
err = ncmpi_begin_indep_data(ncid); CHECK_ERR
err = ncmpi_put_vara_double(ncid, varid, start, count, buf); CHECK_ERR
err = ncmpi_end_indep_data(ncid); CHECK_ERR
}

/* one record must have been created, and all processes must agree */
err = ncmpi_inq_dimlen(ncid, dimid[0], &numrecs); CHECK_ERR
if (numrecs != 1) {
printf("Error at %s:%d: expecting numrecs 1 but got %lld\n",
__FILE__, __LINE__, numrecs);
nerrs++;
}

err = ncmpi_close(ncid); CHECK_ERR

/* read back and check the contents written by the non-empty processes */
err = ncmpi_open(MPI_COMM_WORLD, out_path, NC_NOWRITE, info, &ncid); CHECK_ERR
err = ncmpi_inq_varid(ncid, "v", &varid); CHECK_ERR

if (!is_empty) {
for (i=0; i<NDBL; i++) rbuf[i] = -1.0;
err = ncmpi_get_vara_double_all(ncid, varid, start, count, rbuf); CHECK_ERR
for (i=0; i<NDBL; i++) {
if (rbuf[i] != buf[i]) {
printf("Error at %s:%d: expecting v[%lld] %g but got %g\n",
__FILE__, __LINE__, start[1]+i, buf[i], rbuf[i]);
nerrs++;
break;
}
}
}
else {
/* still a collective call, with nothing to read */
err = ncmpi_get_vara_double_all(ncid, varid, start, count, rbuf); CHECK_ERR
}

err = ncmpi_close(ncid); CHECK_ERR

return nerrs;
}

int main(int argc, char **argv) {

int err;
int formats[] = {NC_FORMAT_CLASSIC, NC_FORMAT_64BIT_OFFSET,
NC_FORMAT_64BIT_DATA};
loop_opts opt;

MPI_Init(&argc, &argv);

opt.num_fmts = sizeof(formats) / sizeof(int);
opt.formats = formats;
opt.ina = 2; /* enable and disable intra-node aggregation */
opt.drv = 2; /* test GIO and MPI-IO driver */
opt.ibuf = 0; /* test default hint nc_ibuf_size only */
opt.bb = 2; /* enable and disable burst-buffering feature */
opt.mod = 2; /* test both collective and independent data mode */
opt.hdr_diff = true; /* run ncmpidiff for file header */
opt.var_diff = false;/* skip ncmpidiff for variables */

err = tst_main(argc, argv, "zero-length request to a record variable", opt,
test_io);

MPI_Finalize();
return err;
}