Found while benchmarking copy strategies for dataset concatenation (#334).
gvl.write writes all bulk output through np.memmap(..., mode="w+"):
_write_phased_variants_chunk — variant_idxs.npy, offsets.npy, dosages.npy
_write_ragged_intervals — starts.npy, ends.npy, values.npy, offsets.npy
_write_from_svar — offsets.npy (2,R,S,P)
_write_from_svar2 — svar2_ranges/*.npy
On NFSv3 this is roughly 5-6x slower than buffered file.write(). Page faults go out as 4 KiB RPCs instead of using the mount's 1 MiB rsize/wsize.
Measurement
512 MiB, 16 MiB chunks, 3 reps. Sources evicted with posix_fadvise(POSIX_FADV_DONTNEED) before every read, so reads are genuinely cold on both filesystems. carter-cn-03, Slurm job 13336789.
NFSv3 (carter-storage:/carter, vers=3,rsize=1048576,wsize=1048576,hard,proto=tcp):
| pattern |
rep1 |
rep2 |
rep3 |
| buffered read → buffered write |
85.6 |
102.0 |
93.0 MB/s |
| memmap read → buffered write |
16.9 |
19.7 |
14.4 MB/s |
| buffered read → memmap write |
13.8 |
15.7 |
12.6 MB/s |
| memmap read → memmap write |
13.6 |
14.1 |
14.4 MB/s |
Local XFS (/dev/md0 on /tmp, node-local):
| pattern |
rep1 |
rep2 |
rep3 |
| buffered read → buffered write |
101.2 |
106.7 |
98.8 MB/s |
| memmap read → buffered write |
103.1 |
103.1 |
100.1 MB/s |
| buffered read → memmap write |
101.2 |
98.7 |
98.8 MB/s |
| memmap read → memmap write |
107.9 |
105.1 |
108.2 MB/s |
Conclusions
- On local XFS the pattern does not matter — all four are within noise of ~100 MB/s, the device limit. memmap is not harmful there.
- On NFSv3 any memmap in the path costs ~5-6x, read side or write side or both. Buffered IO on NFS matches local-disk throughput.
Correction to the first version of this issue
The original text reported memmap reads at 316 MB/s and concluded "memmap reads are in fact the fastest option and should be kept." That was wrong. That run did not evict the page cache and the sources had just been written, so it measured RAM, not NFS. Re-measured with fadvise(DONTNEED), memmap reads are slow on NFS too. The recommendation below is correspondingly broader than it first was.
Proposal
Use buffered read()/write() in ~16 MiB chunks for bulk sequential array IO instead of assigning into / streaming out of a memmap. Buffered IO is not slower on local disk, so this can be unconditional rather than an NFS special case.
Important scope limit
These are sequential-streaming numbers. gvl's read path (_haps.py, _tracks.py) does random, fancy-indexed access into memmapped arrays — a different workload, and one where memmap's page cache is a genuine asset across epochs. Nothing here says the read path is slow, and it should not be changed on the basis of this issue. Someone should measure the random-access case separately before touching it.
Out of scope for the #334 concat PR, which uses buffered IO on both sides from the start.
Found while benchmarking copy strategies for dataset concatenation (#334).
gvl.writewrites all bulk output throughnp.memmap(..., mode="w+"):_write_phased_variants_chunk—variant_idxs.npy,offsets.npy,dosages.npy_write_ragged_intervals—starts.npy,ends.npy,values.npy,offsets.npy_write_from_svar—offsets.npy(2,R,S,P)_write_from_svar2—svar2_ranges/*.npyOn NFSv3 this is roughly 5-6x slower than buffered
file.write(). Page faults go out as 4 KiB RPCs instead of using the mount's 1 MiBrsize/wsize.Measurement
512 MiB, 16 MiB chunks, 3 reps. Sources evicted with
posix_fadvise(POSIX_FADV_DONTNEED)before every read, so reads are genuinely cold on both filesystems.carter-cn-03, Slurm job 13336789.NFSv3 (
carter-storage:/carter,vers=3,rsize=1048576,wsize=1048576,hard,proto=tcp):Local XFS (
/dev/md0on/tmp, node-local):Conclusions
Correction to the first version of this issue
The original text reported memmap reads at 316 MB/s and concluded "memmap reads are in fact the fastest option and should be kept." That was wrong. That run did not evict the page cache and the sources had just been written, so it measured RAM, not NFS. Re-measured with
fadvise(DONTNEED), memmap reads are slow on NFS too. The recommendation below is correspondingly broader than it first was.Proposal
Use buffered
read()/write()in ~16 MiB chunks for bulk sequential array IO instead of assigning into / streaming out of a memmap. Buffered IO is not slower on local disk, so this can be unconditional rather than an NFS special case.Important scope limit
These are sequential-streaming numbers. gvl's read path (
_haps.py,_tracks.py) does random, fancy-indexed access into memmapped arrays — a different workload, and one where memmap's page cache is a genuine asset across epochs. Nothing here says the read path is slow, and it should not be changed on the basis of this issue. Someone should measure the random-access case separately before touching it.Out of scope for the #334 concat PR, which uses buffered IO on both sides from the start.