Skip to content

Speed up the MPAS-to-XDMF converter on large meshes - #744

Merged
xylar merged 3 commits into
MPAS-Dev:masterfrom
xylar:improve-xdmf-converter-performance
Aug 4, 2026
Merged

Speed up the MPAS-to-XDMF converter on large meshes#744
xylar merged 3 commits into
MPAS-Dev:masterfrom
xylar:improve-xdmf-converter-performance

Conversation

@xylar

@xylar xylar commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

The problem

mpas_to_xdmf writes one 2D field per index of every extra dimension, so a 3D field such as temperature(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:

time
read one level of temperature 1.61 s
read all 80 levels together 3.85 s

So roughly an 80x read amplification, of which ~24x was pure waste. In a real polaris run this made the export of one init.nc take 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_bytes argument (--max-read-gb on 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_dims and 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:

  • A new float32 option (-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.
  • Vertex-centered fields map onto the kites of the dual mesh. That mapping was being applied as a lazy fancy-index read from netCDF, which is slow; it is now applied after the data has been read.
  • The connectivity array is written as 32-bit integers when the indices fit, halving the largest static array in each file.

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 .xdmf file; 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

  • Existing tests pass, plus five new ones covering unwrapped field names and values, vertex-to-kite mapping, max_read_bytes not affecting results, float32 casting, and the new _process_extra_dims behavior. The three tests that target new behavior were confirmed to fail on the pre-change code.
  • Spot checks against a direct read of the real 39 GB init.nc match exactly.

Note for polaris

The VizInitStep._export_xdmf step 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, passing float32=True would halve that, and restricting extra_dims to the levels actually of interest would cut it much further.

xylar and others added 3 commits August 4, 2026 02:41
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]>
@xylar xylar self-assigned this Aug 4, 2026
@xylar
xylar merged commit 4b5c11b into MPAS-Dev:master Aug 4, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant