Speed up the MPAS-to-XDMF converter on large meshes - #744
Merged
Conversation
Each index of an extra dimension (e.g. each vertical level) becomes its own field in the XDMF output, and the converter was reading those fields one at a time. A single vertical level is strided across the whole variable on disk, so this meant reading the entire variable once per level. On a 4M-cell, 80-layer mesh, one level of `temperature` took 1.6 s while all 80 levels read together took 3.9 s: an ~80x read amplification that dominated the run time. Instead, read as many indices as possible in a single pass over each variable and slice them apart in memory. The amount read at a time is capped by `max_read_bytes` (2 GB by default) so memory use stays bounded; variables larger than that are read in as few passes as the cap allows. Unwrapping the extra dimensions moves out of `_process_extra_dims` and into the writer, which is what makes the grouped read possible. Writing one 3D variable went from 199 s to 6.5 s (30x) on the mesh above. A cell-blocked variant using HDF5 hyperslabs was also tried and was slower (19.8 s), because the scattered writes cost far more than they saved. Also: * add a `float32` option that writes floating-point fields in single precision, halving the size of the HDF5 files and the time ParaView needs to load them. The mesh geometry stays in double precision so that cell shapes are unaffected, and integer fields are left alone. * apply the vertex-to-kite map after reading rather than as a lazy indexed read from netCDF, which is much slower. * write the connectivity as 32-bit integers when the indices fit. The contents of the HDF5 files are otherwise unchanged; only the order in which attributes are listed in the XDMF differs. Co-Authored-By: Claude Opus 5 <[email protected]>
Cover the behavior that has to be preserved now that extra dimensions are unwrapped as fields are written rather than up front: * unwrapped field names and values match the corresponding slice of the source variable, for one and for two extra dimensions, and indices that were not requested are not written * vertex-centered fields are repeated once per kite of the dual mesh * `max_read_bytes` changes only how many indices are read at a time, so forcing one read per index gives byte-identical output * `float32` casts floating-point fields but not integer fields or the geometry * `_process_extra_dims` keeps dimensions that have selected indices and drops variables with dimensions that do not Co-Authored-By: Claude Opus 5 <[email protected]>
Add a section explaining why reading a vertical level at a time was slow, what `max_read_bytes` trades off, and the two ways to cut the volume of data written (`float32` and requesting only the levels of interest). Also list the `-q`, `-f` and `--max-read-gb` command-line arguments. Co-Authored-By: Claude Opus 5 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
mpas_to_xdmfwrites one 2D field per index of every extra dimension, so a 3D field such astemperature(Time, nCells, nVertLevels)becomes 80 separate fields on an 80-layer mesh. The converter was reading those fields one at a time.That is the wrong way round for how MPAS files are stored. A single vertical level is strided across the whole variable on disk, so reading one level means reading the entire variable. Reading 80 levels one at a time therefore reads the variable 80 times.
Measured on a 4,016,915-cell, 80-layer global ocean initial condition:
temperatureSo roughly an 80x read amplification, of which ~24x was pure waste. In a real polaris run this made the export of one
init.nctake 62.7 minutes (50:02 for the cell fields, 12:44 for the edge fields). The HDF5 writing was never the problem — that side was measured at over 900 MB/s.The change
Read as many indices as possible in a single pass over each variable and slice them apart in memory, instead of going back to disk for each one.
Writing one 3D variable on the mesh above:
199 s → 6.5 s (30x)
How much is read at a time is capped by a new
max_read_bytesargument (--max-read-gbon the command line, 2 GB by default) so that memory use stays bounded. Variables bigger than the cap are read in as few passes as it allows, so the behavior degrades gradually rather than falling off a cliff. Peak memory is roughly two to three times the cap, because masking the fill values makes a copy.The main structural change is that unwrapping the extra dimensions moved out of
_process_extra_dimsand into the writer. Previously the dataset was exploded into one variable per index before anything was written, which threw away the knowledge of which fields came from the same variable — exactly the knowledge needed to read them together. Now variables keep their extra dimensions until they are written.Three smaller improvements came along with it:
float32option (-f/--float32) writes floating-point fields in single precision. This halves both the HDF5 file size and the time ParaView needs to load it. The mesh geometry deliberately stays in double precision so that cell shapes are unaffected, and integer fields are left alone.An alternative was tried and rejected: reading contiguous blocks of cells and writing them into pre-created HDF5 datasets with hyperslabs. That gives the fastest possible reads, but the resulting scattered writes cost far more than they save (19.8 s versus 6.5 s for the same variable), so the simpler whole-variable approach won.
Compatibility
Output is unchanged. The HDF5 datasets produced before and after this change were verified byte-for-byte identical — same names, dtypes, shapes and checksums — across cell, edge and vertex files, with multiple time steps, two extra dimensions at once, and non-contiguous index lists. The only difference is the order in which attributes are listed in the
.xdmffile; the attribute-to-dataset mapping for each time step is identical, and ParaView looks attributes up by name.All new options default to the old behavior, so existing callers and scripts need no changes.
Testing
max_read_bytesnot affecting results,float32casting, and the new_process_extra_dimsbehavior. The three tests that target new behavior were confirmed to fail on the pre-change code.init.ncmatch exactly.Note for polaris
The
VizInitStep._export_xdmfstep added in E3SM-Project/polaris#612 exports every level of every 3D field, which is ~40 GB for the mesh above. Once this lands in an MPAS-Tools release, passingfloat32=Truewould halve that, and restrictingextra_dimsto the levels actually of interest would cut it much further.