Skip to content

NetCDF output: fixed dims, batched writes, workers write one shared file (no merge) - #11

Open
JoshCu wants to merge 2 commits into
mainfrom
claude/eager-goodall-eudoao
Open

JoshCu wants to merge 2 commits into
mainfrom
claude/eager-goodall-eudoao

Conversation

@JoshCu

@JoshCu JoshCu commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Brings over the NetCDF performance lessons from rs_route's writer, then goes one step further so workers write straight into one shared file and the merge step goes away.

Changes

Fixed dimensions and batched writes (04e4a96)

  • The id dimension is now fixed-size instead of UNLIMITED.
  • Results are buffered 100 locations at a time. Each variable is written with a single call per batch, put_values(&data, (start..end, ..)), instead of one row per call.
  • The writer thread now waits for results instead of flushing every 100 ms. The timer flush meant any model slower than 100 ms per location wrote batches of 1.

Shared file, no merge (9355080)
HDF5 can't have several processes writing one file through the library, because each open handle caches the file's metadata and writes it back. Since every size is known before the run, the library isn't needed for the data writes:

  • Parent (create_netcdf_store, next to the existing create_zarr_store call):
    • Creates results.nc with fixed dims and writes all location IDs.
    • Stores each output variable as a contiguous, unfiltered f32 block with its disk space reserved up front.
    • NOFILL keeps that reservation sparse: 1.4 GB apparent size, 0 MB allocated in testing. _FillValue is re-added afterwards so readers still mask -9999.
    • Checks the storage layout once, so an unusable file fails before any worker starts.
  • Workers (NetCdfWriter::new(path, global_start)):
    • Look up each variable's data offset with a read-only HDF5 open, then close it.
    • Write batches with pwrite at offset + row × n_times × 4. Each worker's rows form one contiguous byte range per variable, and no HDF5 handle is open while writing.
  • Location IDs are written with one nc_put_var_string call through netcdf-sys, now a direct dependency with no extra features. The crate's per-string put_string took about 80 µs per ID: 6.2 s → 0.36 s for 80k IDs.
  • The per-worker tmp_*.nc files and merge_netcdf_files are removed.

Behaviour changes

  • Variable names come from the parent's pre-run discovery, the same list Zarr uses. Short rows are padded and missing variables written as -9999.
  • If a run fails partway, rows that were never written read back as 0 rather than -9999. The file is still valid.
  • NetCdfWriter::new now takes (path, start_idx).

Testing

  • cargo test: 40 unit tests and 7 output-format tests pass. New tests cover several writers interleaving into one file, padding of short rows and missing variables, _FillValue surviving NOFILL, and a fixed id dimension.
  • cargo build and cargo build --no-default-features are warning-free.
  • Not yet benchmarked on a real model run.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CHF3fXG3QAnmb8ZeJtsJ2F


Generated by Claude Code

Mirror the rs_route NetCDF writer optimisations:
- Size the `id` dimension up front instead of using an unlimited
  dimension; workers pass their location count and the merge sums the
  per-worker counts before creating the output file.
- Buffer 100 locations and write each variable for the whole batch with
  a single put_values over (start..end, ..), instead of one row per call.
- Writer thread blocks on recv instead of flushing on a 100ms timeout,
  which was degrading batches to single locations for slower models.
- Merge copies variables in large contiguous row blocks rather than one
  row at a time.
- Rows are padded/truncated to the time dimension and missing variables
  left as fill, since whole-batch writes need a rectangular buffer.

Add a merge test covering multiple worker files and the fixed dimension.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CHF3fXG3QAnmb8ZeJtsJ2F
HDF5 can't have several processes writing one file through the library,
since each handle caches and rewrites the file's metadata. With every
size known up front that isn't needed:

- The parent creates results.nc before spawning workers (as it already
  does for zarr): fixed dims, all location IDs written, and each output
  variable contiguous, unfiltered f32 with its space allocated. NOFILL
  keeps that allocation sparse instead of writing -9999 over the whole
  file. HDF5 metadata is final once the parent closes it.
- Each worker reads every variable's data offset with HDF5 read-only,
  then writes its batches with positioned writes at
  offset + global_row * n_times * 4. A worker's rows are one contiguous
  byte range per variable, so no HDF5 handle is open while writing and
  there is nothing to merge.
- Location IDs are written with a single nc_put_var_string call via
  netcdf-sys; one put_string per ID took ~80us each (6.2s -> 0.36s for
  80k IDs).
- The layout (contiguous, no filters, 4-byte type, allocated) is checked
  in the parent so an unusable file fails once, before any worker runs.

Tests cover several writers interleaving into one file, padding of short
rows / missing variables, and that _FillValue survives NOFILL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CHF3fXG3QAnmb8ZeJtsJ2F
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants