Skip to content

Keep the .ymf current during the run, not only after a clean close - #86

Merged
cekees merged 3 commits into
mainfrom
incremental-ymf
Oct 23, 2026
Merged

cekees merged 3 commits into
mainfrom
incremental-ymf

Conversation

@cekees

@cekees cekees commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

C1.1. The archive of record was assembled at close: per-step grid metadata went into the HDF5 as it was produced, and the document describing it was built from that only when close() ran. A run that was killed, hit a scheduler timeout or died on one rank left an archive with the data in it and nothing describing the data — recoverable, but only by a separate pass, which is what scripts/gatherArchives.py exists for.

Depends on cekees/ymf#18, which must merge first — proteus CI installs ymf from @main, so CI here fails until it lands.

Each step is appended, not rewritten

The first version of this rewrote the whole document each step. On review that is the wrong shape: O(steps) per step, O(steps²) over a run, and many proteus runs have very large numbers of steps. The requirement that a document be rewritten to stay well formed is inherited from XML; YAML has no such rule.

steps rewrite append
10 0.6 ms 0.10 ms
100 6.8 ms 0.11 ms
500 35 ms 0.09 ms
5000 ≈350 ms 0.11 ms
5000 cumulative ≈15 min (extrapolated) 0.49 s

Flat per step — the write no longer depends on what the archive already holds.

How it fits together

  • master already holds this step's grids while allGatherIncremental runs, so a record costs no extra HDF5 read and no extra YAML parse;
  • the header is written to <name>.ymf.writing and renamed; the records after it need no such care, because each ends with the document-end marker that tells a reader it is complete;
  • records name their collection rather than indexing the header's list, because proteus creates collections lazily — one per archived FE space, of which there are 13 possible — so a model first archiving at step 40 adds one then;
  • a failed append is logged and swallowed. The step is in the HDF5 regardless, and the authoritative document is still written at close;
  • close() still writes the canonical single document, so a finished run leaves exactly what it leaves today. Only an interrupted run is in the live shape, and read_ymf takes both;
  • writeIncrementalYmf = False restores writing only at close.

Tests (11 new; file goes 33 → 44)

  • test_an_interrupted_run_still_leaves_a_readable_document — the point. Steps are synced, close() never happens, the document is complete and correct.
  • test_what_it_looked_like_before — the control, same run with the feature off: asserts there is no .ymf at all, so the first is not a tautology.
  • test_the_running_document_agrees_with_the_one_written_at_close — the appended stream is folded by the reader; the closing document is re-assembled from YAML in the HDF5. Different code, different sources, same result required. If they diverged, the archive of record would depend on whether the run finished, which is what this removes.
  • test_the_running_document_is_appended_not_rewritten — pins the mechanism: one --- and one ... per step, mid-run.
  • test_close_leaves_a_single_document — and the live shape does not leak into finished archives.
  • plus both layouts, 2 and 3 ranks under mpiexec, and no .writing left behind.

The rank script gained SKIP_CLOSE and NO_INCREMENTAL so an interrupted run is simulated faithfully rather than approximated.

Testing

310 passed, 10 skipped, 1 failed across all six archive files plus test_adr_problem.py and test_pro_cli.py. The failure is test_an_l_shaped_region_converges_at_second_order, in the known pre-existing set verified on a clean tree.

Follow-on

This removes gatherArchives's reason to exist. The script is still shipped (setup.py:1136) and tested, so retiring it is a separate call — ERDC_ISSUE_TRIAGE.md has it as a Tier C close candidate.

🤖 Generated with Claude Code

cekees and others added 3 commits October 22, 2026 23:06
The archive of record was assembled at close: the per-step grid metadata
went into the HDF5 file as it was produced, and the document describing it
was built from that only when close() ran. A run that was killed, hit a
scheduler timeout or died on one rank therefore left an archive with the
data in it and nothing describing the data -- recoverable, but only by a
separate pass, which is what scripts/gatherArchives.py exists for.

Master already holds this step's grids while allGatherIncremental runs, so
the running document costs no extra read and no extra parse: appending is
the same work the close-time assembly does once, done once per step. The
file is written to <name>.ymf.writing and renamed, so a reader sees either
the previous step's document or this one, never a half-written file.

A failed write is logged and swallowed. The authoritative document is still
written at close from the HDF5, and a full disk at step 300 should not
destroy the 300 steps already in the archive.

It is not free, and the cost is measured rather than asserted: a current
document is a whole document, so writing is O(steps) each step -- 0.6 ms at
10 steps, 6.8 ms at 100, 35 ms at 500, which is 8.6 s over a 500-step run
and minutes over a 5,000-step one. Rather than guess a cadence policy, the
cost is made visible: it is accumulated and reported at close, and a single
write over half a second says so once and names the switch. Set
writeIncrementalYmf = False to restore writing only at close.

Nine tests. The one that matters runs the steps and never calls close(),
then reads the document; its control does the same with the feature off and
asserts there is no document at all, so the first is not a tautology. The
structural one compares the running document against the one written at
close: the first is appended in memory from master's grids, the second is
re-assembled from the YAML in the HDF5, and they have to agree -- if they
ever diverge, the archive of record depends on whether the run finished,
which is the property this removes. Both layouts, one to three ranks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review of the first version: rewriting the whole document to record a step
is O(steps) per step, so O(steps^2) over a run, and many proteus runs have
very large numbers of steps. That requirement is inherited from XML, where
a document has to be closed to be well formed. YAML has no such rule, so a
step can be appended and flushed.

Measured on a one-collection archive, per-step write and cumulative:

    steps     rewrite      append
       10      0.6 ms     0.10 ms
      100      6.8 ms     0.11 ms
      500     35.0 ms     0.09 ms
     5000   ~350   ms     0.11 ms

    5000 steps:  ~15 min (extrapolated)  vs  0.49 s

Flat per step, as it should be: the write no longer depends on what the
archive already holds.

This needs ymf's append_step and its stream-aware read_ymf (cekees/ymf#18),
which must merge first -- proteus CI installs ymf from @main. There the
document grows as a stream: a header, then one document per step naming its
collection. Naming rather than indexing matters because proteus creates
collections lazily, one per archived FE space, so a model that first
archives at step 40 adds one then.

close() still writes the canonical single document from the HDF5, so a run
that finishes leaves exactly what it left before; only an interrupted run
is in the live shape, and read_ymf takes both.

The cost accounting and the slow-write warning the first version added are
gone with the cost they measured.

Two tests pin the mechanism rather than the outcome: that the mid-run file
really is a stream, one record per step with each one completed, and that
close collapses it so nothing downstream needs to know the live shape
exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The import sat outside the guard, so a proteus newer than its ymf raised
out of the middle of a timestep instead of losing its live document --
the one failure mode the writer was built to absorb.

CI found it the honest way. Three legs of run 39894170430 resolved
ymf@main at 03:18, five minutes before cekees/ymf#18 merged at 03:23;
the macOS legs resolved it at 04:25 and passed. The provenance probe
this workflow prints made that a two-minute diagnosis rather than a
hunt, but the skew it exposed is real for anyone installing the two
repositories separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cekees
cekees merged commit 88d4a6c into main Oct 23, 2026
10 checks passed
@cekees
cekees deleted the incremental-ymf branch October 23, 2026 18:36
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.

1 participant