/************************************************************************** * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include /************************************************************** * 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>8); return crc ^ 0xFFFFFFFFu; } static inline uint32_t rotl32(uint32_t x,int n){ n &= 31; return 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 exactly * representable doubles, so byte comparison is well defined. **************************************************************/ static inline void FillSite(double *s,uint64_t g) { for(int64_t w=0;w>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 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 &g,const std::vector &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 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 sendcounts, senddispls, recvcounts, recvdispls; /* in sites */ std::vector scatter; std::vector 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= targetBytes ) { k = trial; break; } } p.nunsplit = k; std::vector B(Nd), S(Nd); for(int d=0;d 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 source(lsites); for(int64_t pos=0; pos fill(p.rowsize,0); for(int64_t pos=0; pos 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 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 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 want(NWORD); for(int64_t i=0;i=0,"open for serial crc"); const size_t bufsz = 8*1024*1024; std::vector 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 g(Nd),l(Nd),s(Nd),z(Nd,0); for(int d=0;d 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=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 &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 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 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 gindex(L.lsites); { std::vector lc(Nd), gc(Nd); for(int64_t i=0;i data(L.lsites*NWORD), back(L.lsites*NWORD); for(int64_t i=0;i 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