mirror of
https://github.com/paboyle/Grid.git
synced 2026-08-14 14:39:37 +01:00
1018 lines
40 KiB
C++
1018 lines
40 KiB
C++
/**************************************************************************
|
|
* MPI only parallel file I/O reproducer -- no Grid, no accelerator.
|
|
*
|
|
* Writes an Nd lattice of NWORD-double site objects to a single shared file
|
|
* in *global lexicographic* order (dimension 0 fastest), which is the order
|
|
* that makes a file independent of the process decomposition that wrote it.
|
|
* Three paths produce that file, and are timed and validated against each
|
|
* other:
|
|
*
|
|
* raw each rank writes one disjoint contiguous segment with pwrite.
|
|
* NOT lexicographic -- the file depends on the decomposition.
|
|
* This is the zero-transposition control: the bandwidth the
|
|
* filesystem and the clients can sustain with no reordering.
|
|
*
|
|
* mpiio MPI_Type_create_subarray + MPI_File_set_view +
|
|
* MPI_File_write_all / MPI_File_read_all. The canonical
|
|
* two-phase collective. This is the path under test.
|
|
*
|
|
* aggregate user level two-phase: MPI_Alltoallv within a row
|
|
* subcommunicator to give each rank a few large contiguous
|
|
* file extents, then independent pwrite / pread of those.
|
|
* Produces a byte identical file to mpiio.
|
|
*
|
|
* Validation. Site content is a deterministic function of the *global*
|
|
* lexicographic site index, so a byte that ends up in the wrong place is
|
|
* detectable, not merely a checksum difference. Three independent checks:
|
|
*
|
|
* 1. crc32 per site, combined across sites with rotate-and-xor keyed by
|
|
* the global index (so the combination is order independent) and
|
|
* reduced with MPI_BXOR. Proves the right sites with the right
|
|
* contents are present. Does NOT prove they are in the right place.
|
|
* 2. On read, every rank recomputes the expected content of each global
|
|
* site it believes it owns and memcmps. Proves placement, in
|
|
* parallel, at any volume. Reports the first offending global site.
|
|
* 3. Cross validation: write with mpiio, read with aggregate, and vice
|
|
* versa. Both paths must agree on the same file, so a shared
|
|
* misunderstanding of the layout cannot hide.
|
|
* 4. Optionally (--serial-crc) rank 0 streams the whole file and crc32s
|
|
* it. Slow, incontrovertible, and the two lexicographic files must
|
|
* give the same value.
|
|
*
|
|
* Build: mpicxx -O2 -std=c++11 io_mpi.cc -o io_mpi
|
|
* Run: mpirun -n 32 ./io_mpi --grid 32.32.64.128 --mpi 4.4.2.1
|
|
**************************************************************************/
|
|
|
|
#include <algorithm>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <sstream>
|
|
#include <fstream>
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <stdint.h>
|
|
#include <errno.h>
|
|
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/time.h>
|
|
|
|
#include <mpi.h>
|
|
|
|
/**************************************************************
|
|
* Globals
|
|
**************************************************************/
|
|
static MPI_Comm WorldComm;
|
|
static int WorldRank, WorldSize;
|
|
static int ShmRank, ShmSize;
|
|
|
|
static int Nd = 4;
|
|
static int64_t NWORD = 72; /* doubles per site; 72 -> 576 B, a LorentzColourMatrixD */
|
|
static int64_t fobjSize = 72*sizeof(double);
|
|
|
|
#define CHECK(ierr) do { \
|
|
int _e = (ierr); \
|
|
if ( _e != MPI_SUCCESS ) { \
|
|
char _s[MPI_MAX_ERROR_STRING]; int _l; \
|
|
MPI_Error_string(_e,_s,&_l); \
|
|
printf("rank %d: MPI error at %s:%d : %s\n",WorldRank,__FILE__,__LINE__,_s); \
|
|
fflush(stdout); MPI_Abort(WorldComm,1); \
|
|
} } while(0)
|
|
|
|
#define SYSCHECK(cond,what) do { \
|
|
if ( !(cond) ) { \
|
|
printf("rank %d: %s failed at %s:%d : %s\n",WorldRank,what,__FILE__,__LINE__,strerror(errno)); \
|
|
fflush(stdout); MPI_Abort(WorldComm,1); \
|
|
} } while(0)
|
|
|
|
static inline double usecond(void) {
|
|
struct timeval tv; gettimeofday(&tv,NULL);
|
|
return 1.0e6*tv.tv_sec + 1.0*tv.tv_usec;
|
|
}
|
|
|
|
/**************************************************************
|
|
* crc32 (IEEE 802.3, reflected), table built once. Deliberately
|
|
* the slow obvious implementation: this is a correctness tool and
|
|
* we would rather it be readable than fast.
|
|
**************************************************************/
|
|
static uint32_t crc_table[256];
|
|
static void crc32_init(void)
|
|
{
|
|
for(uint32_t n=0;n<256;n++){
|
|
uint32_t c = n;
|
|
for(int k=0;k<8;k++) c = (c&1) ? (0xEDB88320u ^ (c>>1)) : (c>>1);
|
|
crc_table[n] = c;
|
|
}
|
|
}
|
|
static inline uint32_t crc32_buf(const void *buf,size_t len,uint32_t crc)
|
|
{
|
|
const unsigned char *p = (const unsigned char *)buf;
|
|
crc = crc ^ 0xFFFFFFFFu;
|
|
for(size_t i=0;i<len;i++) crc = crc_table[(crc ^ p[i]) & 0xFF] ^ (crc>>8);
|
|
return crc ^ 0xFFFFFFFFu;
|
|
}
|
|
static inline uint32_t rotl32(uint32_t x,int n){ n &= 31; return n ? ((x<<n)|(x>>(32-n))) : x; }
|
|
|
|
/**************************************************************
|
|
* Lexicographic index <-> coordinate, dimension 0 fastest.
|
|
* This is MPI_ORDER_FORTRAN, and it is the file order.
|
|
**************************************************************/
|
|
static inline void CoorFromIndex(int64_t *coor,int64_t idx,const int64_t *dims,int nd)
|
|
{
|
|
for(int d=0;d<nd;d++){ coor[d] = idx % dims[d]; idx /= dims[d]; }
|
|
}
|
|
static inline int64_t IndexFromCoor(const int64_t *coor,const int64_t *dims,int nd)
|
|
{
|
|
int64_t idx=0, stride=1;
|
|
for(int d=0;d<nd;d++){ idx += coor[d]*stride; stride *= dims[d]; }
|
|
return idx;
|
|
}
|
|
|
|
/**************************************************************
|
|
* Site content as a pure function of the GLOBAL site index, so a
|
|
* misplaced site is detectable and identifiable, not just a
|
|
* checksum difference. splitmix64 mantissa -> exactly
|
|
* representable doubles, so byte comparison is well defined.
|
|
**************************************************************/
|
|
static inline void FillSite(double *s,uint64_t g)
|
|
{
|
|
for(int64_t w=0;w<NWORD;w++){
|
|
uint64_t x = g*0x9E3779B97F4A7C15ull + (uint64_t)w*0xBF58476D1CE4E5B9ull;
|
|
x ^= x>>30; x *= 0xBF58476D1CE4E5B9ull;
|
|
x ^= x>>27; x *= 0x94D049BB133111EBull;
|
|
x ^= x>>31;
|
|
s[w] = (double)(int64_t)(x>>11) * (1.0/9007199254740992.0);
|
|
}
|
|
}
|
|
|
|
/**************************************************************
|
|
* Layout. Everything the three paths need to agree on.
|
|
**************************************************************/
|
|
struct Layout {
|
|
std::vector<int64_t> gLattice, lLattice, psizes, pcoor, gStart;
|
|
int64_t lsites, gsites;
|
|
uint64_t rawOffset; /* this rank's byte offset for the raw control */
|
|
};
|
|
|
|
static void BuildLayout(Layout &L,const std::vector<int64_t> &g,const std::vector<int64_t> &p)
|
|
{
|
|
L.gLattice = g; L.psizes = p;
|
|
L.lLattice.resize(Nd); L.pcoor.resize(Nd); L.gStart.resize(Nd);
|
|
|
|
/* process coordinate: dimension 0 fastest, matching the lattice order.
|
|
Note we do NOT use MPI_Cart_create -- it ranks with the last dimension
|
|
fastest, and mixing the two conventions is the classic way to get a
|
|
subtly transposed file. */
|
|
int64_t r = WorldRank;
|
|
for(int d=0;d<Nd;d++){ L.pcoor[d] = r % p[d]; r /= p[d]; }
|
|
|
|
L.lsites = 1; L.gsites = 1;
|
|
for(int d=0;d<Nd;d++){
|
|
if ( g[d] % p[d] ) {
|
|
if(!WorldRank) printf("global dimension %d (%lld) not divisible by %lld\n",
|
|
d,(long long)g[d],(long long)p[d]);
|
|
MPI_Abort(WorldComm,1);
|
|
}
|
|
L.lLattice[d] = g[d]/p[d];
|
|
L.gStart[d] = L.pcoor[d]*L.lLattice[d];
|
|
L.lsites *= L.lLattice[d];
|
|
L.gsites *= g[d];
|
|
}
|
|
L.rawOffset = (uint64_t)WorldRank * (uint64_t)L.lsites * (uint64_t)fobjSize;
|
|
}
|
|
|
|
/**************************************************************
|
|
* Aggregation plan.
|
|
*
|
|
* Un-splitting the k fastest dimensions of the process grid gives the
|
|
* resulting row of ranks a globally contiguous run of
|
|
* chunk(k) = prod_{d<k} gLattice[d] * lLattice[k]
|
|
* sites. Choose the smallest k that reaches the target extent, so we
|
|
* disturb as few dimensions -- and move as little data -- as possible.
|
|
*
|
|
* The row's box B, restricted from the global lattice, preserves the
|
|
* global lexicographic order. Walking our local sites in local order
|
|
* therefore walks the row index strictly monotonically:
|
|
*
|
|
* incrementing local dim 0 moves ri by +1;
|
|
* a carry out of dim d moves ri by B[d] - lLattice[d] + 1 > 0, since
|
|
* B[d] >= lLattice[d] for every d.
|
|
*
|
|
* so the Alltoallv send buffer is the untouched local array and only the
|
|
* per-destination counts are needed. That is the whole trick.
|
|
**************************************************************/
|
|
struct AggregationPlan {
|
|
int nunsplit, rowsize, rowrank;
|
|
int64_t lsites, chunk;
|
|
MPI_Comm rowcomm;
|
|
std::vector<int> sendcounts, senddispls, recvcounts, recvdispls; /* in sites */
|
|
std::vector<int64_t> scatter;
|
|
std::vector<int64_t> extentGsite, extentLocal, extentSites;
|
|
};
|
|
|
|
static void BuildAggregationPlan(const Layout &L,uint64_t targetBytes,AggregationPlan &p)
|
|
{
|
|
const int64_t *g = &L.gLattice[0], *l = &L.lLattice[0], *ps = &L.psizes[0];
|
|
int64_t lsites = L.lsites;
|
|
p.lsites = lsites;
|
|
|
|
int k = Nd-1;
|
|
for(int trial=1; trial<Nd; trial++){
|
|
int64_t chunk = l[trial];
|
|
for(int d=0;d<trial;d++) chunk *= g[d];
|
|
if ( (uint64_t)(std::min(chunk,lsites)*fobjSize) >= targetBytes ) { k = trial; break; }
|
|
}
|
|
p.nunsplit = k;
|
|
|
|
std::vector<int64_t> B(Nd), S(Nd);
|
|
for(int d=0;d<Nd;d++){
|
|
if ( d<k ) { B[d] = g[d]; S[d] = 0; }
|
|
else { B[d] = l[d]; S[d] = L.gStart[d]; }
|
|
}
|
|
int64_t chunk = l[k];
|
|
for(int d=0;d<k;d++) chunk *= g[d];
|
|
p.chunk = chunk;
|
|
|
|
/* Row communicator. Colour = the still-split (slow) process coordinates;
|
|
key = the lexicographic index of the un-split (fast) ones, so that
|
|
MPI_Comm_split hands back rowrank == logical index and the send/recv
|
|
count arrays can be indexed directly. Grid has to build an explicit
|
|
map here because its Cartesian communicator numbers ranks the other
|
|
way round; a standalone reproducer does not have to inherit that. */
|
|
int colour=0, cstride=1;
|
|
for(int d=k;d<Nd;d++){ colour += (int)(L.pcoor[d]*cstride); cstride *= (int)ps[d]; }
|
|
int logical=0, lstride=1;
|
|
for(int d=0;d<k;d++){ logical += (int)(L.pcoor[d]*lstride); lstride *= (int)ps[d]; }
|
|
|
|
CHECK(MPI_Comm_split(WorldComm,colour,logical,&p.rowcomm));
|
|
MPI_Comm_size(p.rowcomm,&p.rowsize);
|
|
MPI_Comm_rank(p.rowcomm,&p.rowrank);
|
|
if ( p.rowsize != lstride || p.rowrank != logical ) {
|
|
printf("rank %d: row communicator inconsistent (%d vs %d, %d vs %d)\n",
|
|
WorldRank,p.rowsize,lstride,p.rowrank,logical);
|
|
fflush(stdout); MPI_Abort(WorldComm,1);
|
|
}
|
|
|
|
int64_t mystart = (int64_t)p.rowrank * lsites;
|
|
int64_t myend = mystart + lsites;
|
|
|
|
std::vector<int64_t> lcoor(Nd), bcoor(Nd), gcoor(Nd);
|
|
|
|
/* send side: counts only, buffer stays in local order (monotonicity above) */
|
|
p.sendcounts.assign(p.rowsize,0);
|
|
p.senddispls.assign(p.rowsize,0);
|
|
for(int64_t Lx=0; Lx<lsites; Lx++){
|
|
CoorFromIndex(&lcoor[0],Lx,l,Nd);
|
|
for(int d=0;d<Nd;d++) bcoor[d] = (d<k) ? (L.gStart[d]+lcoor[d]) : lcoor[d];
|
|
int64_t ri = IndexFromCoor(&bcoor[0],&B[0],Nd);
|
|
p.sendcounts[ ri/lsites ]++;
|
|
}
|
|
{ int64_t disp=0;
|
|
for(int d=0;d<p.rowsize;d++){ p.senddispls[d] = (int)disp; disp += p.sendcounts[d]; } }
|
|
|
|
/* receive side: which rank of the row owns each slot of our aggregated
|
|
range. Within one source the slots arrive in increasing row index
|
|
order, which is the order that source sends them in. */
|
|
p.recvcounts.assign(p.rowsize,0);
|
|
p.recvdispls.assign(p.rowsize,0);
|
|
std::vector<int> source(lsites);
|
|
for(int64_t pos=0; pos<lsites; pos++){
|
|
CoorFromIndex(&bcoor[0],mystart+pos,&B[0],Nd);
|
|
int64_t j=0, jstride=1;
|
|
for(int d=0;d<k;d++){ j += (bcoor[d]/l[d])*jstride; jstride *= ps[d]; }
|
|
source[pos] = (int)j;
|
|
p.recvcounts[j]++;
|
|
}
|
|
{ int64_t disp=0;
|
|
for(int s=0;s<p.rowsize;s++){ p.recvdispls[s] = (int)disp; disp += p.recvcounts[s]; } }
|
|
|
|
p.scatter.resize(lsites);
|
|
{ std::vector<int> fill(p.rowsize,0);
|
|
for(int64_t pos=0; pos<lsites; pos++){
|
|
int j = source[pos];
|
|
p.scatter[ p.recvdispls[j] + fill[j]++ ] = pos;
|
|
} }
|
|
|
|
/* the two sides were derived independently; make them check each other */
|
|
{ std::vector<int> back(p.rowsize,0);
|
|
CHECK(MPI_Alltoall(&p.sendcounts[0],1,MPI_INT,&back[0],1,MPI_INT,p.rowcomm));
|
|
for(int c=0;c<p.rowsize;c++){
|
|
if ( back[c] != p.recvcounts[c] ) {
|
|
printf("rank %d: aggregation plan inconsistent at %d: %d vs %d\n",
|
|
WorldRank,c,back[c],p.recvcounts[c]);
|
|
fflush(stdout); MPI_Abort(WorldComm,1);
|
|
}
|
|
} }
|
|
|
|
/* decompose our aggregated range into globally contiguous file extents */
|
|
for(int64_t c = mystart/chunk; c <= (myend-1)/chunk; c++){
|
|
int64_t lo = std::max(mystart, c*chunk);
|
|
int64_t hi = std::min(myend, (c+1)*chunk);
|
|
CoorFromIndex(&bcoor[0],c*chunk,&B[0],Nd);
|
|
for(int d=0;d<Nd;d++) gcoor[d] = (d<k) ? bcoor[d] : bcoor[d]+S[d];
|
|
int64_t gbase = IndexFromCoor(&gcoor[0],g,Nd);
|
|
p.extentGsite.push_back(gbase + (lo - c*chunk));
|
|
p.extentLocal.push_back(lo - mystart);
|
|
p.extentSites.push_back(hi - lo);
|
|
}
|
|
}
|
|
|
|
static void ReportPlan(const AggregationPlan &p,uint64_t targetBytes)
|
|
{
|
|
/* Whether the counts differ between destinations decides which branch of
|
|
MPI_Alltoallv is exercised. Say so, rather than leaving the reader to
|
|
infer it from the geometry: a sweep that never leaves the uniform case
|
|
has not tested the interesting one. */
|
|
int lo = p.sendcounts[0], hi = p.sendcounts[0];
|
|
for(int c=0;c<p.rowsize;c++){ lo = std::min(lo,p.sendcounts[c]); hi = std::max(hi,p.sendcounts[c]); }
|
|
int uniform = (lo==hi), alluniform;
|
|
CHECK(MPI_Allreduce(&uniform,&alluniform,1,MPI_INT,MPI_MIN,WorldComm));
|
|
|
|
if ( WorldRank ) return;
|
|
printf("= aggregate: un-splitting %d fastest dimension(s), row of %d ranks, counts %s\n",
|
|
p.nunsplit,p.rowsize,alluniform?"UNIFORM":"NON-UNIFORM");
|
|
printf("= aggregate: %d extent(s)/rank, first %.1f MB (target %.1f MB)\n",
|
|
(int)p.extentSites.size(),p.extentSites[0]*fobjSize/1048576.0,targetBytes/1048576.0);
|
|
printf("= aggregate: buffer overhead %.1f MB/rank\n",p.lsites*fobjSize/1048576.0);
|
|
fflush(stdout);
|
|
}
|
|
|
|
/* forward: iodata (local order) -> aggregated (lexicographic order) */
|
|
static void AggregateExchange(AggregationPlan &p,double *iodata,double *aggregated,
|
|
MPI_Datatype siteType,int forward,double *t_comm,double *t_perm)
|
|
{
|
|
int64_t lsites = p.lsites;
|
|
std::vector<double> tmp(lsites*NWORD);
|
|
double t0,t1,t2;
|
|
|
|
if ( forward ) {
|
|
t0 = usecond();
|
|
CHECK(MPI_Alltoallv(iodata, &p.sendcounts[0],&p.senddispls[0],siteType,
|
|
&tmp[0],&p.recvcounts[0],&p.recvdispls[0],siteType,p.rowcomm));
|
|
t1 = usecond();
|
|
for(int64_t s=0;s<lsites;s++)
|
|
memcpy(aggregated + p.scatter[s]*NWORD, &tmp[s*NWORD], fobjSize);
|
|
t2 = usecond();
|
|
*t_comm += t1-t0; *t_perm += t2-t1;
|
|
} else {
|
|
t0 = usecond();
|
|
for(int64_t s=0;s<lsites;s++)
|
|
memcpy(&tmp[s*NWORD], aggregated + p.scatter[s]*NWORD, fobjSize);
|
|
t1 = usecond();
|
|
CHECK(MPI_Alltoallv(&tmp[0],&p.recvcounts[0],&p.recvdispls[0],siteType,
|
|
iodata, &p.sendcounts[0],&p.senddispls[0],siteType,p.rowcomm));
|
|
t2 = usecond();
|
|
*t_perm += t1-t0; *t_comm += t2-t1;
|
|
}
|
|
}
|
|
|
|
/**************************************************************
|
|
* Checksums.
|
|
*
|
|
* Order independent site checksum: crc32 of each site rotated by the
|
|
* global index and xor-combined, then MPI_BXOR reduced. This is the
|
|
* SciDAC csuma/csumb scheme and it proves the right site contents are
|
|
* present; it says nothing about where in the file they ended up.
|
|
* Placement is proved separately, by ValidateLocal below.
|
|
**************************************************************/
|
|
struct Checksum { uint32_t a,b; };
|
|
|
|
static Checksum SiteChecksum(const double *data,int64_t nsite,
|
|
const int64_t *gindex) /* gindex[i] = global index of site i */
|
|
{
|
|
Checksum c; c.a = 0; c.b = 0;
|
|
for(int64_t i=0;i<nsite;i++){
|
|
uint32_t crc = crc32_buf(data + i*NWORD,fobjSize,0);
|
|
uint64_t g = (uint64_t)gindex[i];
|
|
c.a ^= rotl32(crc,(int)(g%29));
|
|
c.b ^= rotl32(crc,(int)(g%31));
|
|
}
|
|
uint32_t r[2] = { c.a, c.b }, s[2];
|
|
CHECK(MPI_Allreduce(r,s,2,MPI_UINT32_T,MPI_BXOR,WorldComm));
|
|
c.a = s[0]; c.b = s[1];
|
|
return c;
|
|
}
|
|
|
|
/* every rank recomputes what it should have read and compares. Parallel,
|
|
affordable at any volume, and it proves placement because each rank
|
|
checks the bytes it found at the file positions it believes it owns. */
|
|
static int64_t ValidateLocal(const double *data,int64_t nsite,const int64_t *gindex)
|
|
{
|
|
std::vector<double> want(NWORD);
|
|
for(int64_t i=0;i<nsite;i++){
|
|
FillSite(&want[0],(uint64_t)gindex[i]);
|
|
if ( memcmp(&want[0],data + i*NWORD,fobjSize) ) return gindex[i];
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* rank 0 streams the file and crc32s it. Slow; the incontrovertible check. */
|
|
static uint32_t SerialFileCrc(const char *file,uint64_t offset,uint64_t bytes)
|
|
{
|
|
uint32_t crc = 0;
|
|
if ( !WorldRank ) {
|
|
int fd = open(file,O_RDONLY);
|
|
SYSCHECK(fd>=0,"open for serial crc");
|
|
const size_t bufsz = 8*1024*1024;
|
|
std::vector<char> buf(bufsz);
|
|
uint64_t done=0;
|
|
while ( done < bytes ) {
|
|
size_t want = (size_t)std::min((uint64_t)bufsz,bytes-done);
|
|
ssize_t got = pread(fd,&buf[0],want,(off_t)(offset+done));
|
|
SYSCHECK(got==(ssize_t)want,"pread for serial crc");
|
|
crc = crc32_buf(&buf[0],want,crc);
|
|
done += want;
|
|
}
|
|
close(fd);
|
|
}
|
|
CHECK(MPI_Bcast(&crc,1,MPI_UINT32_T,0,WorldComm));
|
|
return crc;
|
|
}
|
|
|
|
/**************************************************************
|
|
* The three write / read paths. Each returns seconds, measured as the
|
|
* maximum over ranks of the interval spanning open..close, with a
|
|
* barrier before the clock starts.
|
|
**************************************************************/
|
|
static int do_fsync = 0;
|
|
static int do_memsub = 0;
|
|
static int use_stdio = 0;
|
|
static int reuse_plan = 0;
|
|
static uint64_t file_offset = 0;
|
|
static MPI_Info io_hints = MPI_INFO_NULL;
|
|
static int hints_reported = 0;
|
|
|
|
/* Grid's bracket, exactly: barrier, start, work, barrier, stop, and quote
|
|
the boss rank's own stopwatch. The trailing barrier makes that the
|
|
slowest rank plus the barrier itself, which is what BinaryIO.h reports;
|
|
an allreduced max of per-rank spans is a slightly different -- and
|
|
slightly smaller -- number, and the difference would show up as the two
|
|
tools disagreeing for no reason to do with I/O. */
|
|
static double Elapsed(double t0)
|
|
{
|
|
CHECK(MPI_Barrier(WorldComm));
|
|
double dt = (usecond()-t0)/1.0e6;
|
|
CHECK(MPI_Bcast(&dt,1,MPI_DOUBLE,0,WorldComm));
|
|
return dt;
|
|
}
|
|
|
|
/**************************************************************
|
|
* File sink / source.
|
|
*
|
|
* Grid writes through std::ofstream (buffered) and this reproducer
|
|
* defaults to pwrite (unbuffered). --stdio selects the former, so that
|
|
* if the two tools disagree on the POSIX paths the buffering can be
|
|
* ruled in or out without editing anything.
|
|
**************************************************************/
|
|
struct Sink { int fd; std::ofstream *os; };
|
|
struct Source { int fd; std::ifstream *is; };
|
|
|
|
/* boss creates, everyone else only seeks and writes into a file that
|
|
already exists. No rank but 0 ever uses O_CREAT/O_TRUNC or ios::out:
|
|
an unsynchronised truncation from a late opener silently erases an
|
|
earlier rank's segment. */
|
|
static Sink SinkOpen(const char *file)
|
|
{
|
|
Sink s; s.fd = -1; s.os = NULL;
|
|
if ( !WorldRank ) {
|
|
int fd = open(file,O_WRONLY|O_CREAT,0644);
|
|
SYSCHECK(fd>=0,"create");
|
|
close(fd);
|
|
}
|
|
CHECK(MPI_Barrier(WorldComm));
|
|
if ( use_stdio ) {
|
|
s.os = new std::ofstream(file,std::ios::binary|std::ios::out|std::ios::in);
|
|
SYSCHECK(s.os->is_open(),"ofstream open for write");
|
|
} else {
|
|
s.fd = open(file,O_WRONLY);
|
|
SYSCHECK(s.fd>=0,"open for write");
|
|
}
|
|
return s;
|
|
}
|
|
static void SinkWrite(Sink &s,const char *buf,uint64_t bytes,uint64_t off)
|
|
{
|
|
if ( s.os ) {
|
|
s.os->seekp((std::streamoff)off);
|
|
s.os->write(buf,(std::streamsize)bytes);
|
|
SYSCHECK(!s.os->fail(),"ofstream write");
|
|
return;
|
|
}
|
|
uint64_t done=0;
|
|
while ( done < bytes ) {
|
|
ssize_t w = pwrite(s.fd,buf+done,bytes-done,(off_t)(off+done));
|
|
SYSCHECK(w>0,"pwrite");
|
|
done += (uint64_t)w;
|
|
}
|
|
}
|
|
static void SinkClose(Sink &s,const char *file,uint64_t need)
|
|
{
|
|
if ( s.os ) {
|
|
s.os->flush();
|
|
if ( do_fsync ) { /* no portable fd from ofstream; reopen to sync */
|
|
int fd = open(file,O_WRONLY); if ( fd>=0 ) { fsync(fd); close(fd); }
|
|
}
|
|
s.os->close(); delete s.os; s.os = NULL;
|
|
} else {
|
|
if ( do_fsync ) SYSCHECK(fsync(s.fd)==0,"fsync");
|
|
SYSCHECK(close(s.fd)==0,"close");
|
|
s.fd = -1;
|
|
}
|
|
CHECK(MPI_Barrier(WorldComm)); /* every extent must be on its way */
|
|
if ( !WorldRank ) {
|
|
struct stat sb;
|
|
SYSCHECK(stat(file,&sb)==0,"stat");
|
|
if ( (uint64_t)sb.st_size != need ) /* only when a longer record preceded us */
|
|
SYSCHECK(truncate(file,(off_t)need)==0,"truncate");
|
|
}
|
|
CHECK(MPI_Barrier(WorldComm));
|
|
}
|
|
|
|
static Source SourceOpen(const char *file)
|
|
{
|
|
Source s; s.fd = -1; s.is = NULL;
|
|
if ( use_stdio ) {
|
|
s.is = new std::ifstream(file,std::ios::binary|std::ios::in);
|
|
SYSCHECK(s.is->is_open(),"ifstream open for read");
|
|
} else {
|
|
s.fd = open(file,O_RDONLY);
|
|
SYSCHECK(s.fd>=0,"open for read");
|
|
}
|
|
return s;
|
|
}
|
|
static void SourceRead(Source &s,char *buf,uint64_t bytes,uint64_t off)
|
|
{
|
|
if ( s.is ) {
|
|
s.is->seekg((std::streamoff)off);
|
|
s.is->read(buf,(std::streamsize)bytes);
|
|
SYSCHECK(!s.is->fail(),"ifstream read");
|
|
return;
|
|
}
|
|
uint64_t done=0;
|
|
while ( done < bytes ) {
|
|
ssize_t r = pread(s.fd,buf+done,bytes-done,(off_t)(off+done));
|
|
SYSCHECK(r>0,"pread");
|
|
done += (uint64_t)r;
|
|
}
|
|
}
|
|
static void SourceClose(Source &s)
|
|
{
|
|
if ( s.is ) { s.is->close(); delete s.is; s.is = NULL; }
|
|
else { close(s.fd); s.fd = -1; }
|
|
}
|
|
|
|
/*-------------------- raw: one contiguous segment per rank ----------------*/
|
|
static double WriteRaw(const Layout &L,const double *data,const char *file)
|
|
{
|
|
uint64_t bytes = (uint64_t)L.lsites*fobjSize;
|
|
CHECK(MPI_Barrier(WorldComm));
|
|
double t0 = usecond();
|
|
Sink s = SinkOpen(file);
|
|
SinkWrite(s,(const char *)data,bytes,file_offset+L.rawOffset);
|
|
SinkClose(s,file,file_offset+(uint64_t)L.gsites*fobjSize);
|
|
return Elapsed(t0);
|
|
}
|
|
static double ReadRaw(const Layout &L,double *data,const char *file)
|
|
{
|
|
uint64_t bytes = (uint64_t)L.lsites*fobjSize;
|
|
CHECK(MPI_Barrier(WorldComm));
|
|
double t0 = usecond();
|
|
Source s = SourceOpen(file);
|
|
SourceRead(s,(char *)data,bytes,file_offset+L.rawOffset);
|
|
SourceClose(s);
|
|
return Elapsed(t0);
|
|
}
|
|
|
|
/*-------------------- mpiio: subarray view + collective -------------------*/
|
|
static void ReportHints(MPI_File fh)
|
|
{
|
|
if ( WorldRank || hints_reported ) return;
|
|
hints_reported = 1;
|
|
MPI_Info info;
|
|
if ( MPI_File_get_info(fh,&info) != MPI_SUCCESS ) return;
|
|
int nkeys; MPI_Info_get_nkeys(info,&nkeys);
|
|
printf("= ROMIO hints actually in force (%d):\n",nkeys);
|
|
for(int i=0;i<nkeys;i++){
|
|
char key[MPI_MAX_INFO_KEY], val[MPI_MAX_INFO_VAL]; int flag;
|
|
MPI_Info_get_nthkey(info,i,key);
|
|
MPI_Info_get(info,key,MPI_MAX_INFO_VAL-1,val,&flag);
|
|
if ( flag ) printf("= %-32s %s\n",key,val);
|
|
}
|
|
MPI_Info_free(&info);
|
|
fflush(stdout);
|
|
}
|
|
|
|
static void MPIIOTypes(const Layout &L,MPI_Datatype siteType,
|
|
MPI_Datatype *fileArray,MPI_Datatype *memType)
|
|
{
|
|
std::vector<int> g(Nd),l(Nd),s(Nd),z(Nd,0);
|
|
for(int d=0;d<Nd;d++){ g[d]=(int)L.gLattice[d]; l[d]=(int)L.lLattice[d]; s[d]=(int)L.gStart[d]; }
|
|
|
|
CHECK(MPI_Type_create_subarray(Nd,&g[0],&l[0],&s[0],MPI_ORDER_FORTRAN,siteType,fileArray));
|
|
CHECK(MPI_Type_commit(fileArray));
|
|
|
|
if ( do_memsub ) { /* the degenerate subarray Grid builds, for exactness */
|
|
CHECK(MPI_Type_create_subarray(Nd,&l[0],&l[0],&z[0],MPI_ORDER_FORTRAN,siteType,memType));
|
|
CHECK(MPI_Type_commit(memType));
|
|
} else {
|
|
*memType = MPI_DATATYPE_NULL;
|
|
}
|
|
}
|
|
|
|
static double MPIIO(const Layout &L,double *data,const char *file,
|
|
MPI_Datatype siteType,int write)
|
|
{
|
|
MPI_Datatype fileArray,memType;
|
|
MPIIOTypes(L,siteType,&fileArray,&memType);
|
|
|
|
MPI_File fh; MPI_Status st;
|
|
int amode = write ? (MPI_MODE_RDWR|MPI_MODE_CREATE) : MPI_MODE_RDONLY;
|
|
|
|
CHECK(MPI_Barrier(WorldComm));
|
|
double t0 = usecond();
|
|
CHECK(MPI_File_open(WorldComm,(char *)file,amode,io_hints,&fh));
|
|
CHECK(MPI_File_set_view(fh,(MPI_Offset)file_offset,siteType,fileArray,
|
|
(char *)"native",io_hints));
|
|
if ( write ) {
|
|
if ( do_memsub ) CHECK(MPI_File_write_all(fh,data,1,memType,&st));
|
|
else CHECK(MPI_File_write_all(fh,data,(int)L.lsites,siteType,&st));
|
|
if ( do_fsync ) CHECK(MPI_File_sync(fh));
|
|
/* Grid recovers the next record's displacement this way, and it is
|
|
inside its timed region, so it is inside ours. */
|
|
MPI_Offset os,disp;
|
|
CHECK(MPI_File_get_position(fh,&os));
|
|
CHECK(MPI_File_get_byte_offset(fh,os,&disp));
|
|
} else {
|
|
if ( do_memsub ) CHECK(MPI_File_read_all(fh,data,1,memType,&st));
|
|
else CHECK(MPI_File_read_all(fh,data,(int)L.lsites,siteType,&st));
|
|
}
|
|
ReportHints(fh);
|
|
CHECK(MPI_File_close(&fh));
|
|
double dt = Elapsed(t0);
|
|
|
|
MPI_Type_free(&fileArray);
|
|
if ( do_memsub ) MPI_Type_free(&memType);
|
|
return dt;
|
|
}
|
|
|
|
/*-------------------- aggregate: Alltoallv + big pwrites ------------------*/
|
|
static double AggregateIO(const Layout &L,AggregationPlan &hoisted,uint64_t target,
|
|
double *data,const char *file,
|
|
MPI_Datatype siteType,int write,
|
|
double *t_comm,double *t_perm,double *t_plan)
|
|
{
|
|
uint64_t need = file_offset + (uint64_t)L.gsites*fobjSize;
|
|
|
|
CHECK(MPI_Barrier(WorldComm));
|
|
double t0 = usecond();
|
|
|
|
/* Grid rebuilds the plan on every IOobject call, inside its timed region,
|
|
so by default so do we -- otherwise the two tools are not measuring the
|
|
same thing. --reuse-plan hoists it out and reports what it cost, which
|
|
is the way to show the plan is not where the time goes. */
|
|
AggregationPlan local;
|
|
AggregationPlan &p = reuse_plan ? hoisted : local;
|
|
if ( !reuse_plan ) {
|
|
double tp0 = usecond();
|
|
BuildAggregationPlan(L,target,local);
|
|
*t_plan += usecond()-tp0;
|
|
}
|
|
std::vector<double> aggregated(L.lsites*NWORD);
|
|
|
|
if ( write ) {
|
|
AggregateExchange(p,data,&aggregated[0],siteType,1,t_comm,t_perm);
|
|
Sink s = SinkOpen(file);
|
|
for(size_t e=0;e<p.extentSites.size();e++)
|
|
SinkWrite(s,(const char *)(&aggregated[0] + p.extentLocal[e]*NWORD),
|
|
(uint64_t)p.extentSites[e]*fobjSize,
|
|
file_offset + (uint64_t)p.extentGsite[e]*fobjSize);
|
|
SinkClose(s,file,need);
|
|
} else {
|
|
Source s = SourceOpen(file);
|
|
for(size_t e=0;e<p.extentSites.size();e++)
|
|
SourceRead(s,(char *)(&aggregated[0] + p.extentLocal[e]*NWORD),
|
|
(uint64_t)p.extentSites[e]*fobjSize,
|
|
file_offset + (uint64_t)p.extentGsite[e]*fobjSize);
|
|
SourceClose(s);
|
|
AggregateExchange(p,data,&aggregated[0],siteType,0,t_comm,t_perm);
|
|
}
|
|
double dt = Elapsed(t0);
|
|
if ( !reuse_plan ) MPI_Comm_free(&local.rowcomm);
|
|
return dt;
|
|
}
|
|
|
|
/**************************************************************
|
|
* Drop whatever of the file this client is caching. Advisory only,
|
|
* and Lustre is free to ignore it, but without it every read after a
|
|
* write in the same job is a page cache measurement.
|
|
**************************************************************/
|
|
static void DropCache(const char *file)
|
|
{
|
|
int fd = open(file,O_RDONLY);
|
|
if ( fd>=0 ) {
|
|
#ifdef POSIX_FADV_DONTNEED
|
|
posix_fadvise(fd,0,0,POSIX_FADV_DONTNEED);
|
|
#endif
|
|
close(fd);
|
|
}
|
|
CHECK(MPI_Barrier(WorldComm));
|
|
}
|
|
|
|
/**************************************************************
|
|
* Command line
|
|
**************************************************************/
|
|
static std::string CmdPayload(char **b,char **e,const std::string &opt)
|
|
{
|
|
char **itr = std::find(b,e,opt);
|
|
if ( itr != e && ++itr != e ) return std::string(*itr);
|
|
return std::string("");
|
|
}
|
|
static bool CmdExists(char **b,char **e,const std::string &opt){ return std::find(b,e,opt) != e; }
|
|
static void CmdIntVector(const std::string &str,std::vector<int64_t> &vec)
|
|
{
|
|
vec.resize(0);
|
|
std::stringstream ss(str);
|
|
long long i;
|
|
while ( ss >> i ) { vec.push_back((int64_t)i); if ( ispunct(ss.peek()) ) ss.ignore(); }
|
|
}
|
|
static void ParseHints(const std::string &str)
|
|
{
|
|
if ( str.empty() ) return;
|
|
CHECK(MPI_Info_create(&io_hints));
|
|
std::stringstream ss(str);
|
|
std::string kv;
|
|
while ( std::getline(ss,kv,',') ) {
|
|
size_t eq = kv.find('=');
|
|
if ( eq == std::string::npos ) continue;
|
|
std::string k = kv.substr(0,eq), v = kv.substr(eq+1);
|
|
CHECK(MPI_Info_set(io_hints,(char *)k.c_str(),(char *)v.c_str()));
|
|
if ( !WorldRank ) printf("= hint %s = %s\n",k.c_str(),v.c_str());
|
|
}
|
|
}
|
|
|
|
/**************************************************************
|
|
* Reporting
|
|
**************************************************************/
|
|
struct Samples { std::vector<double> s; };
|
|
|
|
/* MiB/s, dividing by 1024*1024 -- which is what BinaryIO.h computes for
|
|
lastPerf.mbytesPerSecond and prints as "MB/s". Quoting true decimal MB/s
|
|
here would make this tool read 4.86% faster than Grid on identical work,
|
|
and that discrepancy would be blamed on something real. */
|
|
static void Record(Samples &S,double bytes,double secs){ S.s.push_back(bytes/1024.0/1024.0/secs); }
|
|
|
|
static void ReportSamples(const char *what,const char *path,const Samples &S)
|
|
{
|
|
if ( WorldRank || S.s.empty() ) return;
|
|
double best=0, sum=0;
|
|
for(size_t i=0;i<S.s.size();i++){ best = std::max(best,S.s[i]); sum += S.s[i]; }
|
|
printf("PERF %-5s %-9s best %10.1f MiB/s mean %10.1f MiB/s first %10.1f MiB/s samples",
|
|
what,path,best,sum/S.s.size(),S.s[0]);
|
|
for(size_t i=0;i<S.s.size();i++) printf(" %.1f",S.s[i]);
|
|
printf("\n"); fflush(stdout);
|
|
}
|
|
|
|
/**************************************************************
|
|
* Main
|
|
**************************************************************/
|
|
int main(int argc,char **argv)
|
|
{
|
|
MPI_Init(&argc,&argv);
|
|
WorldComm = MPI_COMM_WORLD;
|
|
MPI_Comm_rank(WorldComm,&WorldRank);
|
|
MPI_Comm_size(WorldComm,&WorldSize);
|
|
MPI_Comm shm;
|
|
MPI_Comm_split_type(WorldComm,MPI_COMM_TYPE_SHARED,0,MPI_INFO_NULL,&shm);
|
|
MPI_Comm_rank(shm,&ShmRank); MPI_Comm_size(shm,&ShmSize);
|
|
MPI_Comm_free(&shm);
|
|
|
|
crc32_init();
|
|
|
|
std::vector<int64_t> g,p;
|
|
if ( CmdExists(argv,argv+argc,"--grid") ) CmdIntVector(CmdPayload(argv,argv+argc,"--grid"),g);
|
|
if ( CmdExists(argv,argv+argc,"--mpi") ) CmdIntVector(CmdPayload(argv,argv+argc,"--mpi"), p);
|
|
if ( g.size()==0 || p.size()==0 || g.size()!=p.size() ) {
|
|
if ( !WorldRank ) {
|
|
printf("usage: io_mpi --grid n1.n2.n3.n4 --mpi p1.p2.p3.p4 [options]\n");
|
|
printf(" --words N doubles per site (default 72, = 576 B)\n");
|
|
printf(" --target BYTES aggregate extent target (default 4194304)\n");
|
|
printf(" --reps N timed repetitions (default 3)\n");
|
|
printf(" --offset BYTES record displacement in the file (default 0)\n");
|
|
printf(" --hints k=v,k=v MPI_Info passed to open and set_view\n");
|
|
printf(" --fsync include fsync / MPI_File_sync in the timed region\n");
|
|
printf(" --drop-cache posix_fadvise(DONTNEED) between write and read\n");
|
|
printf(" --mem-subarray use a degenerate memory subarray, as Grid does\n");
|
|
printf(" --stdio POSIX paths via std::ofstream/ifstream, as Grid does\n");
|
|
printf(" --reuse-plan build the aggregation plan once, outside the timed region\n");
|
|
printf(" --serial-crc rank 0 streams and crc32s each file (slow)\n");
|
|
printf(" --no-validate skip correctness, timing only\n");
|
|
}
|
|
MPI_Finalize(); return 0;
|
|
}
|
|
Nd = (int)g.size();
|
|
|
|
int64_t reps = 3;
|
|
uint64_t target = 4*1024*1024;
|
|
int validate = !CmdExists(argv,argv+argc,"--no-validate");
|
|
int serialcrc = CmdExists(argv,argv+argc,"--serial-crc");
|
|
int dropcache = CmdExists(argv,argv+argc,"--drop-cache");
|
|
do_fsync = CmdExists(argv,argv+argc,"--fsync");
|
|
do_memsub = CmdExists(argv,argv+argc,"--mem-subarray");
|
|
use_stdio = CmdExists(argv,argv+argc,"--stdio");
|
|
reuse_plan = CmdExists(argv,argv+argc,"--reuse-plan");
|
|
if ( CmdExists(argv,argv+argc,"--words") ) NWORD = atoll(CmdPayload(argv,argv+argc,"--words").c_str());
|
|
if ( CmdExists(argv,argv+argc,"--target") ) target = strtoull(CmdPayload(argv,argv+argc,"--target").c_str(),NULL,0);
|
|
if ( CmdExists(argv,argv+argc,"--reps") ) reps = atoll(CmdPayload(argv,argv+argc,"--reps").c_str());
|
|
if ( CmdExists(argv,argv+argc,"--offset") ) file_offset = strtoull(CmdPayload(argv,argv+argc,"--offset").c_str(),NULL,0);
|
|
fobjSize = NWORD*sizeof(double);
|
|
|
|
Layout L;
|
|
BuildLayout(L,g,p);
|
|
|
|
{ int64_t prod=1; for(int d=0;d<Nd;d++) prod *= p[d];
|
|
if ( prod != WorldSize ) {
|
|
if(!WorldRank) printf("--mpi product %lld != %d ranks\n",(long long)prod,WorldSize);
|
|
MPI_Finalize(); return 1;
|
|
} }
|
|
|
|
ParseHints(CmdPayload(argv,argv+argc,"--hints"));
|
|
|
|
MPI_Datatype siteType;
|
|
CHECK(MPI_Type_contiguous((int)fobjSize,MPI_BYTE,&siteType));
|
|
CHECK(MPI_Type_commit(&siteType));
|
|
|
|
AggregationPlan plan;
|
|
BuildAggregationPlan(L,target,plan);
|
|
|
|
uint64_t payload = (uint64_t)L.gsites*fobjSize;
|
|
|
|
if ( !WorldRank ) {
|
|
printf("===========================================================\n");
|
|
printf("= io_mpi : %d ranks, %d ranks/node, %d nodes\n",
|
|
WorldSize,ShmSize,WorldSize/ShmSize);
|
|
printf("= global "); for(int d=0;d<Nd;d++) printf("%lld ",(long long)g[d]);
|
|
printf(" mpi "); for(int d=0;d<Nd;d++) printf("%lld ",(long long)p[d]);
|
|
printf(" local "); for(int d=0;d<Nd;d++) printf("%lld ",(long long)L.lLattice[d]);
|
|
printf("\n");
|
|
printf("= site %lld B, %lld sites/rank = %.1f MB/rank, record %.2f GB\n",
|
|
(long long)fobjSize,(long long)L.lsites,
|
|
L.lsites*fobjSize/1048576.0,payload/1073741824.0);
|
|
printf("= file view: %lld contiguous run(s)/rank of %lld B\n",
|
|
(long long)(L.lsites/L.lLattice[0]),(long long)(L.lLattice[0]*fobjSize));
|
|
printf("= offset %llu, fsync %s, mem-subarray %s, stdio %s, plan %s, reps %lld\n",
|
|
(unsigned long long)file_offset,do_fsync?"on":"off",
|
|
do_memsub?"on":"off",use_stdio?"on":"off",
|
|
reuse_plan?"hoisted":"timed (as Grid)",(long long)reps);
|
|
printf("===========================================================\n");
|
|
fflush(stdout);
|
|
}
|
|
ReportPlan(plan,target);
|
|
|
|
/* global index of each local site, in local lexicographic order. The
|
|
three paths all present data in this order, so one table serves all. */
|
|
std::vector<int64_t> gindex(L.lsites);
|
|
{ std::vector<int64_t> lc(Nd), gc(Nd);
|
|
for(int64_t i=0;i<L.lsites;i++){
|
|
CoorFromIndex(&lc[0],i,&L.lLattice[0],Nd);
|
|
for(int d=0;d<Nd;d++) gc[d] = L.gStart[d]+lc[d];
|
|
gindex[i] = IndexFromCoor(&gc[0],&L.gLattice[0],Nd);
|
|
} }
|
|
|
|
std::vector<double> data(L.lsites*NWORD), back(L.lsites*NWORD);
|
|
for(int64_t i=0;i<L.lsites;i++) FillSite(&data[i*NWORD],(uint64_t)gindex[i]);
|
|
|
|
Checksum ref = SiteChecksum(&data[0],L.lsites,&gindex[0]);
|
|
if ( !WorldRank ) printf("= reference site checksum %08x %08x\n",ref.a,ref.b);
|
|
|
|
const char *fraw = "io_raw.bin", *fmpi = "io_mpiio.bin", *fagg = "io_agg.bin";
|
|
int failures = 0;
|
|
|
|
/* Start from nothing. MPI-IO never shortens a file, so a leftover file
|
|
from a larger configuration would survive underneath this record and
|
|
the length check below would fire on the stale tail rather than on
|
|
anything this run did. (That the tail survives at all is the reason
|
|
the aggregate path truncates; it is not what we are testing here.) */
|
|
if ( !WorldRank ) { unlink(fraw); unlink(fmpi); unlink(fagg); }
|
|
CHECK(MPI_Barrier(WorldComm));
|
|
|
|
/*=================== correctness ===================*/
|
|
if ( validate ) {
|
|
if ( !WorldRank ) { printf("\n= CORRECTNESS\n"); fflush(stdout); }
|
|
|
|
WriteRaw(L,&data[0],fraw);
|
|
MPIIO(L,&data[0],fmpi,siteType,1);
|
|
{ double tc=0,tp=0,tb=0; AggregateIO(L,plan,target,&data[0],fagg,siteType,1,&tc,&tp,&tb); }
|
|
|
|
/* the two lexicographic files must be identical, and both must read
|
|
back correctly through BOTH lexicographic paths. Cross reading is
|
|
the point: a shared misunderstanding of the layout cannot survive
|
|
mpiio-write/aggregate-read agreeing with aggregate-write/mpiio-read. */
|
|
struct { const char *file; const char *wpath; } lex[2] = { {fmpi,"mpiio"}, {fagg,"aggregate"} };
|
|
for(int w=0;w<2;w++){
|
|
for(int r=0;r<2;r++){
|
|
memset(&back[0],0,back.size()*sizeof(double));
|
|
double tc=0,tp=0,tb=0;
|
|
if ( r==0 ) MPIIO(L,&back[0],lex[w].file,siteType,0);
|
|
else AggregateIO(L,plan,target,&back[0],lex[w].file,siteType,0,&tc,&tp,&tb);
|
|
|
|
Checksum c = SiteChecksum(&back[0],L.lsites,&gindex[0]);
|
|
int64_t bad = ValidateLocal(&back[0],L.lsites,&gindex[0]);
|
|
int64_t worst;
|
|
CHECK(MPI_Allreduce(&bad,&worst,1,MPI_INT64_T,MPI_MAX,WorldComm));
|
|
|
|
int ok = (c.a==ref.a) && (c.b==ref.b) && (worst<0);
|
|
if ( !ok ) failures++;
|
|
if ( !WorldRank ) {
|
|
printf("= write %-9s read %-9s : checksum %08x %08x %s, placement ",
|
|
lex[w].wpath, r==0?"mpiio":"aggregate", c.a,c.b,
|
|
(c.a==ref.a&&c.b==ref.b)?"OK":"MISMATCH");
|
|
if ( worst<0 ) printf("OK\n");
|
|
else printf("WRONG, first bad global site %lld\n",(long long)worst);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* raw is a different layout, so it validates only against itself */
|
|
{ memset(&back[0],0,back.size()*sizeof(double));
|
|
ReadRaw(L,&back[0],fraw);
|
|
Checksum c = SiteChecksum(&back[0],L.lsites,&gindex[0]);
|
|
int64_t bad = ValidateLocal(&back[0],L.lsites,&gindex[0]);
|
|
int64_t worst; CHECK(MPI_Allreduce(&bad,&worst,1,MPI_INT64_T,MPI_MAX,WorldComm));
|
|
int ok = (c.a==ref.a) && (c.b==ref.b) && (worst<0);
|
|
if ( !ok ) failures++;
|
|
if ( !WorldRank ) printf("= write raw read raw : %s (control, non-lexicographic)\n",
|
|
ok?"OK":"FAILED"); }
|
|
|
|
/* file lengths must be exactly offset + payload */
|
|
if ( !WorldRank ) {
|
|
const char *f[3] = { fraw,fmpi,fagg };
|
|
for(int i=0;i<3;i++){
|
|
struct stat sb;
|
|
if ( stat(f[i],&sb)==0 && (uint64_t)sb.st_size != file_offset+payload ) {
|
|
printf("= LENGTH WRONG %s : %llu, expected %llu\n",
|
|
f[i],(unsigned long long)sb.st_size,
|
|
(unsigned long long)(file_offset+payload));
|
|
failures++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( serialcrc ) {
|
|
uint32_t cm = SerialFileCrc(fmpi,file_offset,payload);
|
|
uint32_t ca = SerialFileCrc(fagg,file_offset,payload);
|
|
if ( !WorldRank ) {
|
|
printf("= serial whole-file crc32: mpiio %08x aggregate %08x %s\n",
|
|
cm,ca,(cm==ca)?"IDENTICAL":"DIFFER");
|
|
fflush(stdout);
|
|
}
|
|
if ( cm != ca ) failures++;
|
|
}
|
|
|
|
int f; CHECK(MPI_Allreduce(&failures,&f,1,MPI_INT,MPI_MAX,WorldComm)); failures = f;
|
|
if ( !WorldRank ) printf("= correctness: %s\n",failures?"FAILED":"all checks passed");
|
|
}
|
|
|
|
/*=================== performance ===================*/
|
|
if ( reps > 0 ) {
|
|
if ( !WorldRank ) { printf("\n= PERFORMANCE (%lld reps)\n",(long long)reps); fflush(stdout); }
|
|
|
|
Samples wraw,wmpi,wagg,rraw,rmpi,ragg;
|
|
double bytes = (double)payload;
|
|
double tc=0,tp=0,tb=0;
|
|
|
|
for(int64_t n=0;n<reps;n++){
|
|
Record(wraw,bytes,WriteRaw(L,&data[0],fraw));
|
|
Record(wmpi,bytes,MPIIO(L,&data[0],fmpi,siteType,1));
|
|
Record(wagg,bytes,AggregateIO(L,plan,target,&data[0],fagg,siteType,1,&tc,&tp,&tb));
|
|
}
|
|
if ( dropcache ) { DropCache(fraw); DropCache(fmpi); DropCache(fagg); }
|
|
for(int64_t n=0;n<reps;n++){
|
|
Record(rraw,bytes,ReadRaw(L,&back[0],fraw));
|
|
Record(rmpi,bytes,MPIIO(L,&back[0],fmpi,siteType,0));
|
|
Record(ragg,bytes,AggregateIO(L,plan,target,&back[0],fagg,siteType,0,&tc,&tp,&tb));
|
|
}
|
|
|
|
ReportSamples("write","raw", wraw);
|
|
ReportSamples("write","mpiio", wmpi);
|
|
ReportSamples("write","aggregate",wagg);
|
|
ReportSamples("read", "raw", rraw);
|
|
ReportSamples("read", "mpiio", rmpi);
|
|
ReportSamples("read", "aggregate",ragg);
|
|
|
|
if ( !WorldRank )
|
|
printf("PERF alltoallv %.3f s, permute %.3f s, plan %.3f s over all %lld aggregate calls "
|
|
"(rank 0; the redistribution cost, inside the timed region)\n",
|
|
tc/1.0e6,tp/1.0e6,tb/1.0e6,(long long)(2*reps));
|
|
}
|
|
|
|
if ( !WorldRank ) { printf("= done\n"); fflush(stdout); }
|
|
|
|
MPI_Type_free(&siteType);
|
|
MPI_Comm_free(&plan.rowcomm);
|
|
if ( io_hints != MPI_INFO_NULL ) MPI_Info_free(&io_hints);
|
|
MPI_Finalize();
|
|
return failures ? 1 : 0;
|
|
}
|