Retrospective: M8b (the hypergraph container)#
Status: done (all five steps and the review fixes), pending the hosted CI and the author’s merge of its pull
request, on the branch m8b-hypergraph, created by the orchestrator from the head of the accepted
M8a branch m8a-escher-store at b45e685 (origin/main 30c3eef, 0.2.0 released and 0.3.0.dev0,
was merged into M8a at 889953a; the author merged M8a as #14, 2de6ebe, which step 5 merged
into this branch, 8278b89). M8b is the second half of PLAN 11.3’s M8
row: the public hypergraph on the ESCHER store of M8a, its line graph, update path, I/O,
generators and Python, and the freeze of the hypergraph headers; it runs in five steps, and each
step adds its section here. Release 0.3.0 = M8a + M8b + M9 (PLAN Appendix F).
The author’s decisions for M8 (2026-10-07; GOVERNANCE.md approvals log, already recorded on
this branch by M8a). (1) The memory gate is read as store accounting: dynG’s device bytes
against ESCHER-GPU@abcf9b2’s own DynamicHypergraph::deviceBytes() on coauth-DBLP after the build
and after each of 3 x 50K batches (seed 1); the Nsight Systems peak of both processes is recorded,
not gated, in M8 (it is gated in M9). (2) O16: the hypergraph id_reuse default is
erase_first; the H-SOSP parity runs set insert_first. Until the author decides at the 0.3.0
release, the plan’s default holds: the hypergraph headers are frozen at the end of M8b and stable
from 0.3.0 (PLAN 11.3), the .hg and .dgb formats at version 1 in the stable tier; this awaits
the author’s confirmation at the release.
Step 1: the container on the ESCHER store (container-core)#
What was built#
Commit |
What |
|---|---|
|
ADR 0041 (before any code): PLAN 5.2 and the sketch reconciled item by item, the sketch’s three open questions answered (dead rows, |
|
|
|
the six public headers under |
|
|
|
|
|
|
|
|
|
the mutations |
|
reduced sizes under compute-sanitizer; the profiler-stage test; A’s store bytes in the replay report |
(the records commit) |
|
Deviations from the plan and the step’s text (each with the same intent)#
Deletion by content cannot reproduce A’s ids where A’s deleted set has duplicates. The step asks that “deletion by id and deletion by content give the same state”. When a deleted member set is held by several live hyperedges (A’s generator samples copies of live hyperedges, and its random rows of one vertex repeat), no rule that sees only the content can know which of them A deleted, and best fit then gives later inserts other ids. The harness resolves each deleted set to the smallest live id with that set not yet taken in the batch (a documented, deterministic rule), and requires (a) the same live member-set multiset as A’s model after every batch (the state seen by content), and (b) A’s exact ids and rows by id for every batch up to the first ambiguous resolution. The ambiguous resolutions are counted per set (results, section 1.3). Deletion by id is compared with A in full.
MOSP_ESCHER’s Caps factor is not always 1.3.
mosp_escher(extra)reproduces B’s defaultheadroomFactor1.3; its tests set 2.0, 1.25 and 1.5 (test_h2h_delta,test_hsosp_matches_dijkstra,test_hsosp_scale,hsospStress). The harness derives the factor and the extra on which a session’s trees agree and replays undermosp_escher(extra)withinitial_headroom = factor - 1(exact for these factors; tested). The preset itself is unchanged.skipped_opsis broken down. dynG counts the repeated members it drops inskipped_ops(the step’s criterion 3), MOSP_ESCHER deduplicates them silently, so “skipped_ops plus the harness-filtered virtual-id ops == skippedOps” cannot hold literally on a batch with repeated members (B’shsospBenchreal hyperedge batches have 841 over 3 batches, its DBLP 50K hyperedge batches 22,703).hyper_apply_summarycarriesignored_deletionsanddropped_members(skipped_opsis their sum); the harness requiresignored_deletions + filtered == skippedOpsanddropped_members ==B’s deduplicated count (itshe_insertvalues minus the payload of its H2V insert).B’s replayed hyperedge-kind batches have no skipped deletions. In every set the
skippedOpsof the batches before a session’s first vertex-kind batch are 0 (B’s skipped ops are in its vertex-kind batches, M11’s), and no replayed batch names a virtual hyperedge. The comparison runs (0 == 0 on the 130 replayed B batches of the two sets); B’s skip rules are covered by the container’s unit tests (SkippedOpsCountDeadOutOfRangeRepeatedAndDuplicates, thelimitsmix of the random streams) instead.A B session stops at its first vertex-kind batch. B’s tests interleave hyperedge-kind and vertex-kind batches on one hypergraph; once a vertex-kind batch (incidence edits, 0.4) has run, B’s state cannot be reproduced. Each session is replayed up to that batch. (Corrected by the review:
vertex_batchescounted the stopped sessions, and the hyperedge-kind batches after a stop were counted nowhere. In B’smake testset 498 vertex-kind batches stop 96 sessions, and 97 of its 226 hyperedge-kind batches, holding 40,386 ids, about 43 %, come after a stop and are not compared until M11 replays the sessions whole; the report now countsvertex_batches,stopped_sessions,hyperedge_batches_after_stopandids_after_stop, results 6.1.)The batch extracts are a new committed artifact. Hosted CI needs B’s batch records and M8a’s committed subset has none (its B traces are
test_cbst_opsruns); a whole trace with batches is several MB.parity/escher_trace.py extract-batcheswrites a format-1 trace without digests (batch and tree records, the construct and insert sizes); two extracts (A’s016-main, 83 KB; the first 9 sessions of B’s014-test_h2h_delta, 158 KB, under the 256 KB file limit of pre-commit) live incpp/tests/data/goldens/hypergraph_batcheswith their SHA-256 and command inSUBSET.txt. They are kept out ofcbst_trace/because M8a’s tests replay every file there. The repository’s test data stays under 1 MB.Rule 3 in the allocator. The allocator implements the overflow segments of an append (rule 3, with B’s fill trigger point) although no hyperedge batch of 0.3 appends to H2V (incidence inserts are 0.4): the random allocator test exercises it against the host model, and M11’s
slack_csrreuses it. The removal of values from H2V rows (incidence deletions, 0.4) is not modelled: it changes no length or bump pointer, only tail occupancies.V2H has no payload limit on the host backends. The host store keeps V2H as sorted vectors (ADR 0041, section 5): no id depends on its layout. Under a fixed-capacity policy (
mosp_escher(extra)) the CUDA store’s V2H can therefore raisecapacity_errorwhere the host store does not; no parity set comes near it, and the random streams use small fixed payloads only with low-churn mixes.A’s initial capacity above 2^31 - 1 ends A’s process; dynG clamps the initial capacity to
max_payload(ADR 0041, section 6). No dataset of A or B comes near it.hyperedge_id_bound()is the store’s largest key, not A’s host vector size. A’snumHyperedgeKeysnever shrinks; dynG’s bound goes down after a surplus rebuild that drops dead slots at the top (documented inhypergraph.hpp). The harness compares rows up to the larger of the two (the extra ones are empty).line_graph_policy::automaticthrowsnot_supported_errorfromh2h()in this step (the line graph is step 2’s), andsorted_mergeis refused at construction until then.The internal applied record is a member of the container state, read through
hypergraph_access::applied();dyng::update()(step 4) will move it into its transaction.
Measurements#
Container-level id parity (
parity/results/M8b.md, section 1.3): every case of A’smake test,coauth50kandsyn1msets and of B’smake testanddblp50k_hyperedgesets oncuda,sequentialandopenmpwith 1 and 8 threads: 0 mismatches.Memory, early reading (not the gate, which is step 2’s with the line graph): in the scope the container has now (H2V and V2H trees and the H2V row view), the container’s
store_stats().device_bytesequals ESCHER-GPU’s bytes of the same trees and view after the build and every batch: 271 of 271 points, ratio 1.0000 (section 1.4).Tests:
dyng_hypergraph_tests47 (host),dyng_hypergraph_cuda_tests24,dyng_hypergraph_ids_tests2 anddyng_hypergraph_ids_cuda_tests2; compute-sanitizer memcheck clean on both CUDA executables (reduced sizes); the two mutations killed, the control passing; M8a’s store replays of the small sets unchanged (results, section 1.6).Checks:
ci/check.shpassed after the docs/provenance commit (its first run found the missing API page, two provenance lines and the harness’s mutation-config test); Clang 18 syntax and the fortified build clean; every commit of the step builds and passes its tests.
Notes for step 2 (sorted_merge and the memory gate)#
The device store keeps the H2V row view (
row_locator::build, keys1..bound) per commit; the H2H view and the delta merge go next to it inescher_store.cu(finish_apply()is the place forhypergraph.apply.line_graphbeforehypergraph.apply.views).store_stats().device_bytesequals A’s in the current scope at every point of016-mainandsyn1mandcoauth50k(section 1.4); adding A’s H2H tree and view should give A’s wholeDynamicHypergraph::deviceBytes().A’s merged trees (H2H) are sized exactly, B’s by its Caps factor;
initial_payload()has no “merged tree” variant yet.The id replay tool already reads the update records’
device_bytes(A’s whole scope) into its report.
Step 2: the line graph, the views, the memory gate (line-graph-views-memory)#
What was built#
Commit |
What |
|---|---|
|
ADR 0042 (before any code): the policies, the staged port, the line graph’s key count, the internal and public views, the order of an apply, the oracle, the parity method |
|
the transplant of ESCHER-GPU’s H2H merge ( |
|
the mechanical classes of the port: errors, buffers, streams, registration (the last removes the source rules’ staged exemption) |
|
|
|
|
|
the mutation |
|
the API page, the store README, the CHANGELOG |
(the records commit) |
|
Deviations from the plan and the step’s text (each with the same intent)#
The transplant commit also carries the infrastructure. The step asks to compare the line graph with A’s after every commit of the port, which needs the container wired from the first one; so
3b73335holds the transplant together with the host stores, the views, the oracle and the replay’s comparison. The transplanted files themselves are only the generated transplant (transplant.py --map ... --check --rev 3b73335: identical), and the mechanical classes are one commit each. Until the last of them the source rules ofcpp/src/hypergraphskipped the two transplanted files (a staged exemption, removed incdae5c6).The line graph’s key count is the largest bound so far, not the bound. A’s H2H has
numHyperedgeKeyskeys (its host vector never shrinks), dynG’s bound can go down after a surplus rebuild (ADR 0041, section 2). Keeping A’s count (max_bound) gives A’s layout, A’s memory and frontier flags that cover the deleted ids; the rows above the bound are empty and the public view stops at the bound (ADR 0042, section 3).syn1m is compared by its layout hashes only. The step asks for per-key hashes and the full rows of touched keys after every batch of make-test, coauth50k and syn1m. M8a’s syn1m set is a
batchestrace (no state digests), so it is compared by the hashes of A’s H2H rebuild inputs (the whole payload and the row offsets), which is stronger than per-key hashes but does not localize a difference; coauth50k gives per-key row hashes (its H2H rows are recorded as hashes only), make-test the full rows. No new golden set was exported.The merge’s errors raise the V2H tree’s device error word. The H2H tree’s own word is reset by its rebuild; the merge reads V2H and its errors (a broken V2H chain, a merged position outside its row) are read with V2H’s word, once at the end of the apply.
The H2H tree is sized exactly under every preset (A’s rule for merged trees, also under
mosp_escher()and the default); its limit is the policy’s payload limit.The
larger_thanmix of the random streams is capped. It tripled the hypergraph every batch (43K hyperedges on 1,200 vertices after 6 batches); with the line graph checked against the oracle and incheck_integrity()after every batch the two random tests took 92 s each in Debug. A batch inserts more hyperedges than the hypergraph has while it has fewer than 2,000 (m / 4after): 11 s. The testSpecialBatcheshas 1,100 rows on one hub (rows above 1,024 entries).h2h().weightsis host memory on every backend (the weights by id are a host array; anarray_viewcarries its own memory space), asweights().An empty batch still rewrites the line graph (A’s merge copies every row); kept for parity of the timing and memory, a hypergraph that never had a hyperedge skips the merge (no zero-block launch; tested).
The CTest entry of the new mutant came one commit early.
9c66eeeadded the CMake entry ofhypergraph.mutation.mirror_additions_dropped(the file was staged with the C12 test), its hook came withf561043; at9c66eeealone that CTest fails (the mutant equals the control).parity/mutate.py did not count the new mutation at first: the C12 test let an exception of a driving thread terminate the executable, and mutate.py counts only failed tests, never a crash;
cd9cc9ereports the exception as a failure. 3 / 3 hypergraph mutations detected after it.
Measurements#
H2H parity (results, section 2.2): every state of A’s make-test (255), coauth50k (4) and syn1m seeds 1-3 (12) equal to A’s on cuda after every commit of the port, and on sequential and openmp.
Memory gate: GO (results, section 2.3): byte for byte A’s
DynamicHypergraph::deviceBytes()after the build and every batch of coauth50k; nsys peaks 3,018.9 MiB (A, with counting) and 3,019.1 MiB (dynG), recorded.Maintain reading (results, section 2.4; reported, not gated; at the base clock lock, ADR 0021, because A’s process runs under the power cap at boost):
hypergraph.apply0.854x of A’supdate.maintainon coauth50k and 0.859x on syn1m (sums of the 3 batches; worst batch 1.026x); insert 0.68x / 0.87x, line graph 0.83x / 0.81x, views 0.30x / 0.22x; delete 1.72x / 2.96x (the host grouping of the deleted incidences is timed in dynG’s delete stage, in A’smaintain.prepare); host syncs per batch 21 (A: 76). A CPU profile of the first reading found the applied record’s sort of the touched vertices (about 16 ms per syn1m batch);16898f4makes it linear (the first reading: 0.893x and 1.120x).Sanitizers: memcheck (leak check) clean on
dyng_hypergraph_cuda_testsanddyng_hypergraph_ids_cuda_tests; racecheck and synccheck clean without the long random streams (results, section 2.6).Mutations:
parity/mutate.py run --suites hypergraph: control passes, 3 / 3 detected (the newhypergraph_mirror_additions_droppedby 10 host and 8 CUDA tests).
M9 risks#
The delete stage’s host grouping.
hypergraph.apply.deleteis 1.7-3.0x A’s delete timers because it times the single-threaded grouping of the deleted incidences by vertex (A times it inmaintain.prepare);maintainas a whole is 0.85x. If M9’supdatereading needs room, the grouping (and the insert’s) can run in parallel or on the device.The Nsight Systems peak of the container (3,019.1 MiB on coauth50k) is the merge’s: the new H2H payload exists next to the old one until the rebuild, as in A, whose peak (3,018.9 MiB) also lies in its H2H merge, not in its counting (corrected by the review; results 6.2). M9 gates the peak with the counting included; the counting adds to it only with workspace still allocated during the merge (headroom about 150 MiB under 1.05x).
The boost clock lock does not hold for A’s process on coauth-DBLP (power cap during its full count); M9’s
updategate is read at the base lock (ADR 0021) unless that changes.
Notes for M9 (triad_count)#
detail::hypergraph_access::require_line_graph(h)thenviews(h): A’sRowViews of H2V and H2H (outset) in the store’s memory space, keys = ids + 1, values + 1; the H2H view covers keys1..max_bound(empty rows above the bound). Count(-) must read them beforeapply()(the merge replaces the H2H payload, the H2V insert reuses dead slots); count(+) after.The frontier of A (
device_frontier.cuh, flags overnum_keys + 1) is ported inline_graph/; the inserted ids are on the device in the applied record.hypergraph.applyhas the stages of the toml; A’supdateregion adds subtract and add around it.
Open items#
Steps 3-5 of M8b.
Step 3: the update path, the kit’s hypergraph axis, the scaffold (update-kit-scaffold)#
What was built#
Commit |
What |
|---|---|
|
ADR 0043 (the update path and the kit’s container axis) and ADR 0044 (the hypergraph scaffold and the requirements of a hypergraph manifest), before any code; GOVERNANCE and index rows |
|
the host store’s lazy views and |
|
|
|
|
|
the aggregate-delta fakes on the hypergraph ( |
|
the kit’s container axis: |
|
|
|
|
|
|
|
a test fix found by memcheck (a hand-run participant’s workspace outlived its resources handle) |
(the records commit) |
|
Deviations from the plan and the step’s text (each with the same intent)#
The applied record stays container state (ADR 0041 deviation 12 said
dyng::updatewould move it into its transaction). The transaction exposes the record of its commit throughapplied(), which is the hypergraph’s last record; moving it out would take it fromhypergraph_access::applied(), which the parity tools and M11’s incidence-delta hook read after a plainapply()(ADR 0043, section 1).“Prepares the row views and h2h for participants that read them” is the line graph only: the internal row views are rebuilt by every commit already (ADR 0042), and the public views must not be built on the apply path.
reads_prepared_graph()of a hypergraph participant means “reads the line graph” (default false);run_update()makes sure of the line graph before the before-apply phase (on G_t, so count(-) reads it), not inside the commit.The planted bug’s “requested ids” are the ids a batch would get without reuse,
id_bound(G_t) + i(an inserted hyperedge has no requested id; these are the ids a caller who predicts ids would use). The kit catches it in C2, C5 and C7 (and C8 in budgets builds) on every backend.The test-only fakes are not registered algorithms. The kit gained two optional traits members,
registry_entry()(C0 and the backends read the entry the traits give) andtest_partners(C10’s partners besides the registered algorithms), so the hypergraph axis runs on every backend before the first hypergraph algorithm (M9’striad_count) exists. The fakes’ own suite is one test executable per backend kind (dyng_hypergraph_kit_tests,..._cuda_tests), not a<name>_conformance_testsof the registry.The kit’s graph axis keeps
graph_types; a hypergraph algorithm’s traits may saycontainer_typesinstead (both are read). The graph suites’ test lists are unchanged (compared with the binaries built before the change, every graph conformance executable, host and CUDA, and the registry test).Two template folders instead of
//@@ graph///@@ hypergraphblocks in one (ADR 0044): the two versions differ in almost every line.graph_requirementskeeps its name in a hypergraph manifest and names the requirements onhypergraph_properties(ADR 0044); a graph manifest’s table is unchanged.Recorded, not fixable in this step: the frozen
update.hpp’s static_assert text for a wrong batch names the graph’sedge_batch_viewalso for a hypergraph, and a graph result passed with a hypergraph stops in sssp.hpp’s frozenupdate_traits(no type named 'edge_type'); both headers must stay byte-identical (ADR 0043 findings 3 and 4). Compile-fail tests pin both.Two stale strings of criterion 21 fixed early:
update_dispatch_test’s “(the hypergraph of 0.2)” andnew_algorithm.py’s refusal message (gone with the refusal).The hypergraph stages of an update: Step 0 runs under
update.normalize(or<algo>.normalize) and the commit underupdate.commitaroundhypergraph.apply, so insidedyng::update()thehypergraph.applystage covers the commit only; a plainapply()keepshypergraph.applyaround validation and commit (the stages ofparity/timed_regions/ hypergraph.tomlare unchanged).
Measurements#
The kit’s hypergraph axis:
dyng_hypergraph_kit_tests28 tests (26 checks over the two hypergraph types, C4 and C12 skipped on the host: one engine, no CUDA) and the generator checks, 53 s in Debug;dyng_hypergraph_kit_cuda_tests28 tests (C4 skipped), 29 s on GPU 1. C8 holds with no allocation in the steady state and 0 (host) / 2 (CUDA) host synchronizations.The planted bug: in the budgets build (dev) the mutant fails 8 tests (C2, C5, C7, C8 of both types; the first C2 failure is the
mixedmix under erase_first, where reuse makes the predicted ids wrong);parity/mutate.py run --suites kit(parity-cuda, Release: no C8) detects it by 6 host and 6 CUDA tests (C2, C5, C7), control passing.Sanitizers: memcheck clean on the kit’s CUDA executable and the hypergraph CUDA tests after a test fix (
524d6c9: a hand-run participant’s workspace outlived its resources handle); racecheck and synccheck clean on the kit’s C1, C3, C11 and C12 and the update tests. The kit’s racecheck took 760 s, soci/gpu_local.shruns C1, C11 and C12 under racecheck and synccheck (memcheck covers every check).The CPU and GPU gates:
ci/check.sh(after the ruff fix),ctest -L gpu(670) andctest -L cpu(1,055) on dev-cuda, the Clang 18 syntax pass (328 + 710 files) and the fortified cpu-only build: clean.The scaffold check: 54 tests of the two hypergraph probes (13 checks x 2 types x 2 probes, two hand cases) passed, next to the graph probes (80) and
my_bfs(41).
Notes for M9 (triad_count)#
A hypergraph algorithm is a problem with
container_type = hypergraph<V, W>; itsrequestedisframework::hyper_requested_batch(normalized->deletions: the live deleted ids of G_t) and itsappliedisframework::hyper_applied_batch(summary.inserted_ids;record.inserted_ids_deviceon CUDA;record.deleted_members).framework/fake_hyper_problems.hppis the worked example of count(-) on the old row views and count(+) on the new ones, on every backend.reads_prepared_graph()returning true keeps the line graph (automaticbuilds it on G_t).The traits:
container_types,hypergraph_shape,require(hypergraph_properties&); the kit’s presets add bothid_reuseorders and the three capacity presets.scripts/new_algorithm.py triad_count --container hypergraph --family aggregate_deltagives the green start (itsplanned.tomlentry is removed by the scaffold).
Open items#
Hosted CI has not run on this branch (nothing is pushed).
Steps 4 and 5 of M8b (I/O, generators, Python; the freeze). The kit’s hypergraph mixes come from a kit-local generator; step 4 may switch them to the public hypergraph generators.
The author’s confirmations at the 0.3.0 release (the frozen hypergraph headers, the stable
.hg/.dgbformats) are still pending; the plan’s default applies until then.
Step 4: hypergraph files, batch files and generators (io-generators)#
What was built#
Commit |
What |
|---|---|
|
ADR 0045 ( |
|
|
|
|
|
the parity side: |
|
|
|
|
(the records commit) |
|
Deviations from the plan and the step’s text (each with the same intent)#
The originals’ preprocessing are option sets (
hypergraph_read_options::escher_gpu(),hsosp(seed, max_cardinality)), static factories like the capacity presets, on top of PLAN 5.7’s options. Two options were added:index_base(both originals’ files are 1-based; ids must not be guessed) andnum_vertices(ESCHER-GPU’s synthetic runs have isolated vertices above the largest id that a.hgfile cannot show; the converter records the count in its.txt). PLAN 5.7 namesrelabel = dense_sortedfor ESCHER: ESCHER-GPU’s loader keeps the ids (none);dense_sortedis its converter’s numbering, soescher_gpu()usesnone, which gives the same rows on coauth-DBLP (whose ids are already 1..V) and the right vertex count everywhere.weight_spec::hsosp_target_draw. H-SOSP’s weights are drawn after one draw for its virtual target vertex; the virtual hyperedges are M11’s (hyper_ssspadds them), but the draw changes every weight, so the option set reproduces the draw (and discards it). Documented in the header and file_formats.md.Weighted
.hgfiles end their lines with; w(as.dgt’s+h), an extension neither original writes; without itwrite_hypergraphcould not write a weighted list.Tokens. The
.hgreader takes ESCHER-GPU’s line rules (comments at the first character only; commas separate) and dynG’s tokens: MOSP_ESCHER’s loader also accepts a+sign and reads1-2as two ids; dynG rejects both (ADR 0010’s strict readers). No registered file has them..dgbcarries annotations. The flagsdeleted_membersandexpected_ids(andinsert_first) are the “semantics flags” of PLAN 5.7, chosen so that M9 replays A’s batches from.dgbfiles by content and checks the ids by key without the traces.The “ESCHER dump reader” is a decoder of the batch records of M8a’s CBST traces (the dumps of ADR 0036), on the parity side as the step allows (no public API; step 5’s review decides whether a resolver belongs in
dyng::testing).dyng_hypergraph_idsnow uses it and its resolver for the by-content replay (a refactor: the replays of steps 1-3 give the same records);trace_sourcemoved fromcbst_compareinto the trace library, which uses the standard library only, so thatdyng-compat-eschercan open traces without the test libraries. The converter compiles the two parity sources into the tool.“Test that by-content and by-key replay of coauth50k give the same rows” is the
--mode bothreplay ofcompare.py hypergraph_ids(A’s live member sets after every batch in both containers; A’s ids until the first ambiguous resolution, 824 on coauth50k) anddyng-compat-escher replayof the converted files (the live member sets of the by-id and the by-content containers equal after every batch).The kit’s generator switch changes the kit’s batches (the same twelve mixes and their promises, other draws).
heavy_deleteis a mix of the public generator that the kit does not use (no kit check names it); the kit’s twelve mixes map one to one. Test counts are unchanged.write_batches()now rejects negative ids (shared writer checks; such files did not read back): a behaviour change of a tracked function, in the CHANGELOG’s “Changed”.The fuzz workflow’s dispatch limit went from 1,000 to 750 s per target: eight targets must fit the job’s 120 minutes.
Reproducers without findings. The fuzzers found nothing in 30 minutes each; the
regressions/of the two new targets hold hand-made inputs of the class M6b found (size fields that lie), so that the class is replayed by every test build and bounded byfuzz_regression_test.hyperedge_batch_mix::limitsdeletesINT64_MIN, which no batch file can hold (the writers reject negative ids): batches of that mix are applied, not written.
Measurements#
Parity (
parity/results/M8b.mdsection 4):.hgwithescher_gpu()equals A’s 2,467,389 loaded hyperedges, withhsosp()B’s 2,466,792 (numbering and weights bit for bit);benson_to_hg.pywrites the pinned bytes in both variants, andwrite_hypergraphof A’s loaded hypergraph writes them too; the legacy generators give 20/20 synthetic hypergraphs and 180/180 batches ofmake test, 3/3 and 9/9 of syn1M (openmp and cuda), 3/3 coauth50k batches; by key and by content give the same member sets on coauth50k (824 ambiguous resolutions);dyng-compat-escherreplays its.dgbfiles of coauth50k and syn1M on openmp and cuda with the expected ids.Fuzzing:
fuzz_hg5.9 M,fuzz_batch_binary15.6 M andfuzz_batch_text11.2 M executions in 31 minutes each, no finding; peak RSS 126 / 450 / 124 MB..dgbsize: A’s coauth50k batches (3 x 50K operations, with the deleted member sets and the expected ids) take 4.3 MB; the reader allocates the largest record twice (pinned, CUDA) plus its device copy.Gates:
ci/check.shpassed (cpu-only 1,062 and dev 1,095 tests);ctest -L gpuof the step’s executables on dev-cuda 153/153; the Clang 18 syntax pass over 355 + 746 files and the fortified cpu-only build (250 tests of the step’s labels) clean; the intermediate commit12b0358built and passed in a worktree; compute-sanitizer memcheck, racecheck and synccheck ofdyng_io_cuda_testsclean.
Notes for M9 (triad_count)#
Replaying A’s batches.
dyng-compat-escher to-dgb <trace> <prefix>writes<prefix>.hg,.dgband.txt(A’s command line, vertex count, H2V payload capacity);replayshows the two ways: by id (the file’s deletions;expected_idsmust equalinserted_ids) and by content (deleted_membersthroughescher_dump::by_content_resolver). M9’s parity reads the.dgbwithio::batch_reader(res, path)(device views on CUDA) and the counts need no ids.Regenerating A’s runs without the traces:
generators::legacy::escher_hypergraphandescher_batch_generatorwith onestd::mt19937_64(seed), the hypergraph under {erase_first, escher_gpu(capacity)};escher_options::parse()turns A’s command line into the options.Datasets:
read_hypergraph(coauth.hg, escher_gpu())is A’s coauth-DBLP;benson_to_hg.py --variant escherre-derives the file.
Open items#
Hosted CI has not run on this branch (nothing is pushed); the fuzz workflow’s 60 s runs and the new CTests (
fuzz.*.hg,fuzz.*.batch_binary,compat_escher.*) run there first.Python bindings of the step’s functions (
dyng.io.read_hypergraph,.dgb, the generators and the legacy generators, Hypothesis strategies): step 5.The author’s confirmations at the 0.3.0 release (the frozen hypergraph headers,
.hgand.dgbstable at version 1) are still pending; the plan’s default applies until then.
Step 5: Python, the API review and freeze, the parity tooling, the records (python-freeze-records)#
What was built#
Commit |
What |
|---|---|
|
|
|
ADR 0046 (the hypergraph in Python), before the code; GOVERNANCE and index rows |
|
the bindings ( |
|
the Python tests (container, io, generators and Hypothesis properties, the dump replays, CUDA) |
|
the review’s additions ( |
|
the freeze: |
|
ADR 0047 (the review and the freeze, with the demonstration), GOVERNANCE rows (ADR 0047, O16 at the freeze, the open decision of the release), CHANGELOG |
|
the certificate’s hypergraph-id schema ( |
|
|
|
|
|
every replay of steps 1-4 again at the step’s code, the memory and maintain readings |
|
the author’s merge-method decision of 2026-10-14 (merge commits only; ADR 0019 amended; GOVERNANCE, |
|
|
(the records commit) |
|
Deviations from the plan and the step’s text (each with the same intent)#
mainwas merged with the strategyours. The author merged M8a as the squash commit2de6ebe, whose tree is byte-identical tob45e685, the M8a head this branch started from; a three-way merge from the old base30c3eefreports conflicts only where M8b changed M8a’s files. The merge (8278b89) keeps this branch’s tree, which is the correct result;git diff HEAD mainshows M8b’s changes only.The Python replay of the dumps reads the committed extracts with the repository’s trace reader (
parity/escher_trace.py) and reproduces the C++ replay’s decoding rules in the test, instead of committing.hg/.dgbexports of them: the repository’s test data was at its 1 MB budget (998,934 bytes incpp/tests/data), and the exports would have added 110 KB. The test skips where the parity harness is absent (an sdist).The views are host copies in Python (
Incidence,LineGraph), andBatchReadergives host batches: a zero-copy device view of a container needs the staleness ruledyng.Arrayhas for results (python_gaps.md, ADR 0046).int64 member arrays raise
NotSupportedErrorwithoutvertex_dtype="int32"(PLAN 5.4 rule 2: only int32 hypergraphs are compiled, and an int64 array never selects a narrower type on its own). Python lists take int32 when they fit.dyng.update()over hypergraphs is not bound before M9 (no hypergraph result exists to update); listed inpython_gaps.md.The review ADR (0047) follows the freeze commit, because it records the demonstration made on that commit;
aa7b679(the review’s additions) alone does not match the API baseline, which753449eupdates.O16 is recorded as the author’s decision, not as “pending the author’s confirmation” as the acceptance text says: the GOVERNANCE approvals log has the author’s O16 decision of 2026-10-07 (
erase_first, the plan’s default). What awaits the author at the 0.3.0 release is that the frozen headers (and.hg/.dgbat version 1) are stable (a new open decision).The pull request is not opened by this step: the implementer must not push, and a pull request needs the branch on GitHub. The description is prepared for the orchestrator in
$DYNG_SCRATCH/orchestration/M8b-pr-body.md(it says the pull request needs theapi-changelabel).The certificate is a dry run: the hypergraph-id records of every set, and the small CBST sets’ store records; the large CBST sets’ store replays, the performance suites and the release checks belong to the release (the dry run reports them missing).
dyng_hypergraph_ids --hgbuilds A’s by-id container from the file (after checking its rows and vertex count against A’s) instead of only comparing it, so the memory gate loads coauth-DBLP through the public reader; the replays without--hgare unchanged.The maintain reading is at the base clock lock (ADR 0021), as step 2’s.
Wheel sizes were measured once, before the freeze commit (
ci/plugin_wheels.shwith cu12 and cu13 and both Pythons); the later commits change headers by four inline accessors and comments.ci/gpu_local.sh’s plugin step rebuilt the cu13 wheel at the final code.ci/gpu_local.shrunsHypergraphCuda.TwoContainersOnTwoNonBlockingStreams(C12, two host threads on two non-blocking streams) under synccheck and memcheck, not racecheck (d077ccb). The step’s firstgpu_local.shrun died with the interrupted session after more than 70 minutes in racecheck ofdyng_hypergraph_cuda_tests, whose list already left out the long random streams. Probing each test: the other 44 tests take 51 s under racecheck andSpecialBatches(rows over 1,024 entries) 10 s, but the two-thread test took 59 s in two runs and more than 400 s in four others (two timed out at 400 s, two were stopped after 10 and 22 minutes, one of them withOMP_NUM_THREADS=8 OMP_WAIT_POLICY=passive). Without a sanitizer it is stable (50 repetitions, 9.2 s each at full size) and synccheck runs it in 3 s with 0 errors. Racecheck checks shared-memory hazards inside a kernel, and this test’s kernels are the ones the single-stream tests run under racecheck, so leaving it out of racecheck loses no kernel; the kit’s C12 (one host thread, two streams) stays under racecheck (43 s). The racecheck list is therefore bounded like the other suites’ sanitizer steps (the cbst step’s reduced sizes, the kit’s C1/C11/C12).The author’s merge-method decision of 2026-10-14 is recorded here (
7294720): M8a reachedmainas the squash commit2de6ebe, squash merging is off since, and ADR 0019 is amended (merge commits only). The M8b pull request is merged with a merge commit, which brings M8a’s cited commits back intomain’s history.ci/gpu_local.sh’sclang-tidystep ran overcpp/src/algorithms/_template_hypergraph, which is not compiled (step 3 added the template and its exclusion toci/check.sh’s tidy step only), so it failed on the template’s includes;3281f10excludes it asci/check.shdoes. No library source had a finding.
Measurements#
Python: 71 hypergraph tests (65 on the CPU: container, io, generators and Hypothesis properties, the dump replays; 6 CUDA tests with a plugin), the whole suite 626 passed, 72 skipped (CPU); with the cu13 plugin
pytest -m gpu61 passed (both Pythons), the suite withDYNG_CPU_ONLY=1624 passed.Wheels (
ci/plugin_wheels.sh,DYNG_PLUGINS="cu12 cu13", local toolkits CUDA 12.9 and 13.1):dyng1.99 MB (sdist 1.42 MB),dyng-cu128.27 MB,dyng-cu138.19 MB, all far below the 90 MB budget; growth since the local 0.2.0rc1 builds (R020: 1.70 MB, 0.99 MB, 6.0 MB and 6.2 MB with CUDA 12.9 and 13.4): +0.29 MB (CPU), +2.3 MB (cu12), +2.0 MB (cu13), the hypergraph container, its line graph and the store.Replays (results, section 5.3): every set on every backend, 0 mismatches.
Memory gate (5.4): 1.0000 at every point, through the public reader. Maintain (5.5): 0.893x (coauth50k), 0.859x (syn1m); delete 1.72x / 2.91x as in step 2.
Mutations (5.6): 11 / 11 detected, control passing.
Effort: M8b’s five steps ran from 2026-10-13 22:22 to 2026-10-14 about 14:30 (CDT; step 5 was interrupted by a usage limit at about 10:15 and resumed), under one day of wall-clock time for the whole milestone, against the plan’s re-baselined “M8b and M9 about 1-1.5 weeks”; the plan now expects M9 in 3-5 days plus the review and fix steps.
Checks of the step#
Check |
Result |
|---|---|
|
the first run passed every step but |
|
|
|
no breaking change of the frozen C++ API or of the Python API; 152 lines added to the frozen API (the six hypergraph sections, newly frozen) |
the freeze demonstration (ADR 0047, Decision 4) |
a removed default of |
|
the resumed run (11:30-14:05, records in |
wheels |
|
every replay on every backend |
results, section 5.3: 0 mismatches |
mutations |
11 / 11 detected (results, 5.6) |
fresh clone at |
cpu-only built, |
portability |
Clang 18 |
Python |
|
Notes for M9 (triad_count): the handoff#
What triad_count reads, and how (the fitness skeleton
cpp/tests/api/fitness/ m9_triad_count_fitness.cppcompiles all of it): a hypergraph problem whose participant (detail::hyper_update_participant<V, W>) returnsreads_prepared_graph() = true, sorun_update()makes sure the line graph exists on G_t;use_normalized()gives Step 0’s live deleted ids (the frontier of count(-)); inbefore_apply()count(-) reads the internal row views (hypergraph_access::views(h)afterrequire_line_graph(h): ESCHER-GPU’sRowViews of H2V and H2H, keys = id + 1, values + 1,outset; device memory on CUDA, the same layout on the host backends) or the public views (0-based,int64offsets,out); inafter_apply()count(+) reads the new views withinserted_ids(host) andhyper_applied:: inserted_ids_device(CUDA), plusdeleted_membersand the touched sets.framework/fake_hyper_problems.hpp(step 3) is the worked example;scripts/new_algorithm.py triad_count --container hypergraph --family aggregate_deltagives the green start.Parity inputs:
dyng-compat-escher to-dgbwrites A’s runs as.hg+.dgb(expected ids, deleted member sets); replay by key and by content throughio::batch_reader(the skeleton shows both with public API only);generators::legacyredraws A’s synthetic runs without traces.The maintain-stage risks:
hypergraph.apply.deleteis 1.7-2.9x A’s delete timers (the host grouping, timed there);maintainwhole is 0.79x (coauth50k) and 0.83x (syn1m) after the review (results 6.3: the 8 ms coauth drift of step 5 was the applied record’s touched sets, fixed); host syncs 19 per batch against A’s 76.The memory peak: both nsys peaks (dynG 3,019.1 MiB, A 3,018.9 MiB) lie in the H2H merge (results 6.2: A’s counting ranges peak at 1,837-1,853 MiB). M9 gates the peak with the counting included: the counting’s workspace must not stay allocated during the merge, or must fit the roughly 150 MiB of headroom that 1.05x of A’s peak leaves. The stream-ordered pool keeps about 1.27 GB of slack after the merge (3,104 MiB held against A’s 1,867 MiB between batches): M9’s memory reading should report the footprint too.
The layout-control decision: the maintain reading ran with one heap layout (ADR 0024’s protocol); the author’s decision of 2026-10-08 on layout control for the 0.2 gates (GOVERNANCE approvals log) applies to M9’s gated
updatereading too; no new decision is needed unless the M9 reading shows layout-dependent modes.Clocks: the boost lock does not hold for A’s process on coauth-DBLP (power cap); M9’s gate is read at the base lock (ADR 0021) unless that changes.
Open items#
Hosted CI has not run on this branch (nothing is pushed); the pull request is the orchestrator’s (deviation 8).
The long random streams and C12’s two-host-thread test are not run under racecheck (deviation 13; memcheck covers all of them, synccheck the two-thread test).
The review fixes (2026-10-14)#
Independent reviewers checked M8b at 6abdc7f through four lenses (the API freeze’s semantics,
the parity oracles, I/O robustness, GPU integration and memory); 39 confirmed findings (two of
them the same defect, the commit’s allocation after the version bump) were fixed on this branch
in small commits, each with its tests, and the affected parity and performance readings were run
again (results, section 6). None changes an author-approved rule; two change frozen-header text
(documentation only) and one adds lines to the API baseline’s frozen hypergraph.hpp section.
What was fixed#
Finding |
Fix |
Commit |
|---|---|---|
Step 0’s repeated-id removal was quadratic (one repeated id in a 400K batch: about 20 s) |
the (id, position) pairs sorted, the first position kept: O(k log k); a 200K shuffled batch with repeats on every backend |
|
the “no-throw” commit copied |
the summary is built inside the store phase; after the commit point only integer assignments, a swap and |
|
every apply paid a fixed O(|E| + |V|) host cost (about 9 ms per coauth batch) for the touched sets |
reusable marks in the container state: O(k log k) for few values, one scan for many; the random streams check both sets by brute force |
|
|
the store phase waits for the call’s stream (an event) and the call’s stream for the store’s work; ADR 0041 section 4, |
|
three back-to-back synchronizations read the three error words |
|
|
an OOM during the first |
the flag is set after the row view; the locator is left empty when its allocation fails (it freed first, as A); a fault-injection test fails every allocation of the build in turn |
|
|
|
|
the batch-size |
documented as strong (nothing changed) |
|
|
documented as every skipped operation (in 0.3 that sum); ADR 0041 section 8 records the 0.4 rule (B’s skips counted, an out-of-range incidence vertex skipped); ADR 0047 amendment |
|
|
“a surplus row inserted by a batch reserves its members rounded up to a multiple of this, plus one slot” |
|
a huge |
each term saturates at the payload limit; C++ and Python tests |
|
|
all six arrays |
|
|
the first offset is pushed inside the try; the allocation-failure executable tests it |
|
|
|
|
ADR 0047 froze the |
the snapshot lists |
|
ADR 0041’s table said the owning batch is “same” as the sketch; PLAN 5.3’s example does not compile |
the row records the default-only constructor and the |
|
the README listed the hypergraph container as planned |
its own row, working (M8a/M8b) |
|
by content, the id check could never fire (high) |
ids compared first (a mismatch unless the batch is ambiguous), |
|
no replay checked h2h against |
with |
|
“vertex-kind (not replayed)” counted stopped sessions; 97 hyperedge-kind batches uncounted |
|
|
B’s weight check shortened silently; the loaded weights unchecked |
sizes must agree, the loaded weights compared, the container built from B’s raw rows |
|
|
|
|
|
fail-stop: every later |
|
Python |
a mutex across |
|
a |
|
|
|
|
|
|
|
|
|
ADR 0045 records the shape, the reasons and the mapping of the sketch’s loop |
|
|
recorded as deliberate (header, ADR 0045, file_formats.md) with tests at the boundaries |
|
|
a custom mutator reseals the CRCs of 3 mutants in 4; a 31-minute run, results 6.4 |
|
K > 64 reported as “K = -1”; |
the value as written; the round-trip sentence names the limit |
|
Python options beyond int64 escaped as |
|
|
|
the deleted-member offsets’ rule and the flag rule |
|
|
|
|
no initcheck on the hypergraph executables; C12’s comment claimed racecheck |
initcheck added ( |
|
M8b’s compile-time growth unrecorded |
recorded below; the mutants’ module copies take one architecture |
|
the records said A’s peak included its counting; the pool slack was not reported |
|
|
Deviations and choices of the review fixes (each with the same intent)#
Stream ordering by events, not by refusing a mismatch. The reviewer offered both; refusing resources on another stream would have made the documented contract (“the CUDA backend orders work on the resources’ stream”) false for hypergraphs. The join costs nothing when the streams are the same; the views returned by
h2v()/v2h()/h2h()(which take no resources) stay on the hypergraph’s own stream, as documented (@sync).skipped_opsis reworded, not split. Anignored_incidence_editsfield would have been an addition to a frozen aggregate now for a feature of 0.4; the total is what C9 and B’sskippedOpsneed, and a per-kind field stays an additive change for M11..hgstays unversioned (ADR 0045 section 1): a version line breaks MOSP_ESCHER’s loader on dynG’s unweighted files. The author’s decision list names the alternative.escher_gpu()’s two extra inputs are recorded, not removed: the id 2^31 - 1 and an empty file are valid in dynG (the vertex type’s limit; 0 hyperedges is a valid hypergraph), and no dataset of A has either.The line-graph oracle runs on every set and backend of the line-graph replays, the large ones included: it took 142 s for coauth50k on cuda (62 s without), so no separate pass was needed.
--no-line-graph-oracleexists for runs that only want A’s comparison; a certificate refuses it.The graph’s
update_participantstays out of the API snapshot: ADR 0023 treats it as internal-stable and M8b leavesupdate.hppbyte-identical; only the hypergraph’s contract, which ADR 0047 froze, is listed.No test injects an allocation failure into the commit’s summary copy (the commit fix): the copy uses
std::allocator, and the structure (the summary built before the commit point, astatic_assertthat the applied record’s move isnoexcept) is what keeps the guarantee.The touched sets keep persistent host marks in the container state (one byte per id and per vertex, not counted in
device_bytes, not cloned): building the sets from the batch alone by sorting was the 16 ms of step 2.The mutants’ copies of the hypergraph module take one CUDA architecture (the build’s first, with PTX);
nativebuilds are unchanged.The two-stream test was fixed after
ci/gpu_local.sh’s memcheck (8bd6713): it copied G_t’s view on the legacy default stream, which memcheck rightly reports as unordered with the store’s stream-ordered frees on a non-blocking stream, and destroyed its streams before the hypergraph. The library was not at fault; the memcheck step of that run is therefore red fordyng_hypergraph_cuda_tests, and memcheck of the fixed executable, run as the step runs it, is clean (54 tests, 0 errors, 0 leaks;memcheck-hypergraph-rerun.log).
Compile time (recorded; the review’s reading)#
On the compile-only ci-cuda12 preset (7 release architectures, CUDA 12.9) the reviewer measured
main 2de6ebe at 2 min 54 s wall and 103 CPU-minutes, M8b at 3 min 58 s and 174 CPU-minutes
(libdyng 34,552 -> 38,404 KiB stripped, +11 %); 49.5 of the CPU-minutes were the 12 mutant copies
of the hypergraph module (8 CBST and 4 hypergraph mutants), against 3.9 for the library’s own
copy. After 5ef3f4f (one architecture per mutant copy), ci-cuda13 (CUDA 13.1) at HEAD:
197 s wall and 151 CPU-minutes for the whole preset (883 steps;
measured while the mutation suites ran on the same machine, so the times are upper bounds), libdyng
40.2 MB unstripped. The 12 mutant copies of the hypergraph module took 46 CPU-minutes in all; the
two units the reviewer counted, escher_store.cu and sorted_merge.cu, 13.7 (49.5 before); one
copy’s cbst_store.cu takes about 55 s against 164 s for the library’s seven-architecture one.
A unit’s time is mostly its host front end, so one architecture saves about two thirds of each
CUDA unit; the mutants stay in the compile-only presets (that they compile is checked there), and
the hosted cuda-build keeps its margin under timeout-minutes: 120 The wheels stay far below the 90 MB budget (step 5: dyng 1.99 MB,
dyng-cu12 8.27 MB, dyng-cu13 8.19 MB).
Checks of the review fixes#
Check |
Result |
|---|---|
|
every step passed: cpu-only 1,069 and dev 1,102 tests, harness 262, Python 636 passed / 72 skipped, scaffold 80 + 3 + 54 + 41, clang-format, clang-tidy, REUSE, provenance, docs (Doxygen with warnings as errors, the API snapshot equal to the baseline), pre-commit |
|
no breaking change of the frozen C++ API or of the Python API |
the detail-contract demonstration |
|
portability |
Clang 18 |
|
the full run at |
the replays, the memory and maintain readings, the fuzzer, the mutations, the dry-run certificate |
results, section 6 |