Retrospective: R020 (release preparation for 0.2.0rc1)#

Status: ready for the release candidate on the branch release-0.2.0 (from main at 8b9067e, the merge of pull request #10 with M6b). Release 0.2.0 is M7 + M6 (M6a, M6b; PLAN Appendix F); R020 makes its release candidate 0.2.0rc1: the mosp API review and freeze (mosp is stable from 0.2.0, the author’s decision of 2026-10-07), the mosp benchmark suite, the release changes and two informational investigations, then the release certificate and the distributions. Nothing is published by this milestone: the orchestrator pushes the branch; the author merges the release pull request; the AI assistant pushes the signed tag v0.2.0rc1 and creates the GitHub Release on the author’s behalf (GOVERNANCE.md, 2026-10-06 and 2026-10-07). Each of the five steps has its section here: step 5 fixed what the independent reviews of the branch found and measured the certificate again on the reviewed code; it ends with the readiness of 0.2.0rc1 and the open items.

Step 1: the mosp API review and freeze (mosp-freeze)#

What changed#

Commit

What

a1d2815

The review’s documentation amendments of <dyng/mosp.hpp> (no signature changed; the C++ baseline lists the same declarations): what each inherited update_stats counter means for mosp and how deterministic it is, the exceptions from_arrays() inherits from sssp’s import, clone() sizing the pooled workspace, path_costs() committed to host memory by default, max_objectives / max_preference_scale may be raised and never lowered; set_options() checked to be strong and allocation-free after its checks, pinned by the new dyng_mosp_allocation_failure_tests

5a71dcd

ci/api_snapshot.py marks dyng/<name>.hpp frozen exactly when the manifest says maturity stable (before, a hand-written list already labelled mosp.hpp frozen); tests that the label follows the manifest and that a changed mosp signature fails; ci/tests/test_api_check.py: griffe reports a renamed parameter of dyng.mosp.compute, a changed default of dyng.mosp.Options and a removed Result accessor as breaking

ad034bb

The mosp manifest says stable; scripts/regen.py outputs (registry, _algorithms.py, the tables); docs/algorithms/mosp.md, the README and the start page; dynamic_bfs and triangle_delta stay tutorial

a8abefb

ADR 0035 “The mosp API review and freeze (0.2)”: 20 findings in ADR 0023’s format, five documentation amendments, the rest justified; Accepted under delegation for the review, the maturity recorded as the author’s decision; the two demonstrations (removing the default of mosp::compute’s opt fails ci/docs.sh --doxygen-only with the baseline diff; renaming source of dyng.mosp.compute fails ci/api_check.sh)

Deviations (each with the same intent)#

  1. The frozen label of an algorithm header now comes from its manifest (ADR 0035, decision 2), instead of relabelling one entry of a hand-written list.

  2. The demonstrations ran once in a throwaway clone and are recorded in ADR 0035 (decision 3); the permanent tests are a synthetic-XML case and a griffe comparison of two copies of python/dyng, which need neither a build nor git.

Step 2: the mosp benchmark suite (mosp-suite)#

What changed#

Commit

What

df2d4e3

benchmarks/paper/ipdps25_dynamosp_mosp.yaml: the four graphs and three batches of M7’s gates (roadNet-PA, roadNet-CA, rgg_n_2_20_s0, road_usa; safe50k, unsafe50k, local10k, seed 777), K = 3, the sssp suite’s inputs and SHA-256 digests (re-hashed), OpenMP (28 pinned threads) against MOSP-OpenMP@c352151 and CUDA (GPU 0, boost lock, ADR 0018; the default-clock reading recorded, not gated) against MOSP-CUDA@e220ee2, “(a) compute” <= 1.05, “(b) end to end” <= 1.10 and device memory <= 1.05; parity/bench_suite.py runs mosp suites through perf_ab.py mosp and perf_ab.py memory --mosp; parity/certify.py requires the mosp suite from 0.2 (REQUIRED_SUITES) and a gated reading of every gated metric of every suite

d8f0602

benchmarks/README.md (= docs/developer/benchmarks.md), parity/README.md, release.md step 3, docs/algorithms/mosp.md names the suite record

e6f01ad

parity.json records required_suites; a 0.2 certificate without the mosp summary fails with “no performance suite of mosp”

1474a92

bench_suite.py --keep-contaminated: a diagnostic execution for a busy machine, always partial, refused as a gate reading anywhere else

Deviations (each with the same intent)#

  1. --keep-contaminated was added because another user’s long job (about 22 cores for 36 hours, at times on GPU 0) made the contamination monitor reject every round of the plumbing check; the gate protocol is unchanged (a record that kept contaminated rounds is refused).

  2. The suite is ipdps25_dynamosp_mosp.yaml (R010 split _sosp / _mosp); the TPDS K sweep is listed under deferred, as in M7.

  3. The mosp memory reading uses GPU 1, as the sssp suite’s does (M7 took it on GPU 0; the two A5000s are identical and the reading takes the exclusive lock).

Step 3: the release changes and two informational investigations (release-changes)#

This step was interrupted once by a usage limit (2026-10-07, about 16:20 UTC) and resumed the same day; the work in progress was checked against acceptance criterion 5 and kept.

What changed#

Commit

What

b9fdbb0

The approvals log of GOVERNANCE.md records the author’s decisions that main did not have: the AI assistant pushes the signed tags v0.2.0rc1 / v0.2.0 and creates the GitHub Release on the author’s behalf (2026-10-06); the author merges the release pull requests and approves pypi-cu12 and pypi-cu13 together, then pypi (2026-10-07); the three sanitizer checks are required checks of main (2026-10-07); the repository state of 2026-10-07 (M6b merged as 8b9067e, the first Pages deployment, the website field, 27 required checks). The plugins’ licence expression was already on main (2026-10-06). docs/developer/release.md has a 0.2.0 column in “who does what”; docs/developer/repository_settings.md lists the 27 required checks and closes step 13; the comments of release.yml and sanitizers.yml follow (comment-only changes; actionlint and zizmor pass)

5bf0a72

VERSION 0.2.0rc1; the CHANGELOG section [0.2.0rc1] - 2026-10-07 with the user-facing summary of 0.2.0 at its top, everything that was under Unreleased below it, an empty Unreleased and the link references; CITATION.cff 0.2.0rc1 / 2026-10-07 (cffconvert 2.0 --validate); the status blocks of the README and the start page, the release candidate’s install lines from TestPyPI with the plugins’ driver and GPU requirements (README, docs/getting_started), SECURITY.md, SUPPORT.md, CONTRIBUTING.md, the roadmap; the documentation banner names a release candidate as one

8bc3e61

This retrospective, the investigations’ record parity/results/R020-investigations.json, the layout-control proposal attached to the open decision of GOVERNANCE.md, docs/developer/plan.md

The CUDA toolkits of the plugins (criterion 4’s check). NVIDIA’s RHEL 8 repository (developer.download.nvidia.com/compute/cuda/repos/rhel8/x86_64, read 2026-10-07) lists nvcc up to cuda-nvcc-12-9 12.9.86 and cuda-nvcc-13-4 13.4.92, the pinned versions. A fresh ci/cuda_toolkit.py lock --plugin cu12 --release 12.9 and --plugin cu13 --release 13.4 into a copy reproduce ci/cuda_toolkits.toml byte for byte, and ci/cuda_toolkit.py verify --signatures passes for the cached RPMs of both. Nothing is re-pinned, so no plugin is rebuilt in this step and the documents naming 12.9 / 13.4 stay as they are.

Investigation (a): the cycle_count update of the CUDA 13.4 cu13 wheel#

M6a’s informational wheel-vs-parity row (open item 4 of its retrospective) read the DD k = 4 update (25K + 25K) of the cu13 wheel built with the pinned CUDA 13.4 at 1.088x the parity build (CUDA 13.1), while the wheel built with 13.1 read 0.988x.

The kernels are the same. The update kernels of the parity overlays built with 13.1 and 13.4 (parity/wheel_vs_parity.py build-parity, the same commit, sm_86, the same GCC 12.2 host compiler):

  • SASS (cuobjdump -sass, per kernel): count_owned_cycles_kernel<4, unsigned>, the kernel of this update, has the same 560 instructions and 20 registers instead of 22; the other count_owned_cycles_kernel widths differ by -64 to +8 instructions (fewer moves and IMADs under 13.4); item_counts_kernel and mark_owners_kernel are identical.

  • Nsight Compute 2025.4.1 (GPU 0, --clock-control base, the kernels inside the NVTX range cycle_count.update, 6 updates per side): the kernel time per update is 689.2 us (parity 13.1), 687.0 us (parity 13.4), 691.7 us (wheel 13.1) and 686.2 us (wheel 13.4). 13.4 is 0.3-0.8 % faster; build_next_rows_kernel (224 us) and count_owned_cycles_kernel<4> (2 x 180-183 us) dominate, and the CUB scan kernels of the newer CCCL have other policy names and 48 -> 58 registers but the same time.

The difference was a process mode of a host stage. About 70 % of the update is cycle_count.normalize, the framework’s normalization of the batch on the host (ADR 0020); it is compiled by the host compiler, not by nvcc. wheel_vs_parity.py keeps one long-lived process per side, so a side reads one process (one heap layout and process mode, parity/results/M3.md section 6.3) for a whole session. The attempt before the interruption read the update side by side in such sessions (GPU 0 and, while GPU 0 had a foreign job, GPU 1; the GPU 1 readings are informational only): the same build against itself read 3.73 against 6.39 ms (13.4 vs 13.4, GPU 0) and 4.01 against 5.73 ms (13.1 vs 13.1, GPU 1), and 13.4 against 13.1 read anywhere from 0.81x to 1.60x depending on the session. The final reading therefore runs every side in 8 separate processes (15 rounds each after a warm-up, the side order rotating, GPU 0, the exclusive lock, the boost clock lock; final_a.py, record in parity/results/R020-investigations.json):

Stage (device ms, median over 8 processes [min-max])

parity 13.1

parity 13.4

wheel 13.1

wheel 13.4

cycle_count.normalize (host)

2.209 [2.165-2.237]

2.103 [2.072-2.125]

2.151 [2.100-2.189]

2.126 [2.116-2.155]

cycle_count.count_minus

0.279

0.278

0.281

0.279

cycle_count.commit (graph.apply 0.287-0.289)

0.299

0.300

0.297

0.296

cycle_count.enact_fused (count_plus 0.210-0.215)

0.240

0.237

0.238

0.233

cycle_count.update

3.118 [3.085-3.156]

3.010 [2.965-3.037]

3.025 [2.961-3.055]

2.984 [2.960-3.016]

13.4 / 13.1: 0.965 for the parity builds, 0.986 for the wheels; every device stage is within 1.5 % (13.4 slightly faster), and the spread is in the host stage. (The wheels are M6a’s, 476ab72; since then the cycle_count sources changed only in one error message.)

Recommendation. Keep CUDA 13.4 for the cu13 wheel: there is no 13.4 slowdown to fix, so no library change and no ADR. A future wheel-vs-parity row of a short, host-heavy update should be read over several processes per side, as here, not from one long-lived worker per side (a note for parity/wheel_vs_parity.py; the row is informational, PLAN 7.7, and no gate reads it).

Investigation (b): layout control for the 0.2 gates (a measured proposal)#

The open decision “Layout control for the 0.2 gates” (GOVERNANCE.md; parity/results/M3.md sections 5.4 and 6.3) asks whether the gates against the originals should control code placement. Measured on a representative subset of the gate readings, at the release library code (sources of 1474a92 = the release commits’), GPU 0 / the Xeon, the exclusive lock, the contamination monitor (at most 1 round repeated per reading), outputs byte-identical / histograms equal to the goldens in every reading. Variants of the host code, built from the parity / parity-cuda presets with: L0 no flag (the accepted presets); F -falign-functions=64; FB F plus -falign-loops=32 and the assembler’s -mbranches-within-32B-boundaries (the mitigation of the Skylake-derived cores’ jump-conditional-code erratum). “A” is the original: A0 the unpatched build of the harness, AF / AFB the same unpatched sources rebuilt with the same flags in a separate work area (an experiment, never a gate reference). B0-BF and B0-BFB are dynG against dynG (sssp over 5 heap layouts, ADR 0024). Ratios dynG / original (or variant / L0), medians:

Reading

A0-B0 (the protocol)

A0-BF

AF-BF

A0-BFB

AFB-BFB

B0-BF

B0-BFB

sssp OpenMP roadNet-CA local10k, sosp_update obj0 / obj1 / obj2

0.741 / 0.730 / 0.854

0.755 / 0.748 / 0.855

0.640 / 0.611 / 0.659

0.733 / 0.700 / 0.796

0.716 / 0.677 / 0.730

0.992 / 1.010 / 1.006

0.976 / 0.983 / 0.992

… apply / end to end

0.888 / 0.880

0.872 / 0.907

0.869 / 0.880

0.860 / 0.668

0.842 / 0.734

0.974 / 0.983

0.948 / 0.789

sssp OpenMP roadNet-CA safe50k, sosp_update obj0 / obj1 / obj2

0.819 / 0.915 / 0.926

0.787 / 0.918 / 0.933

0.834 / 0.898 / 0.901

0.819 / 0.904 / 0.917

0.798 / 0.896 / 0.897

1.015 / 1.003 / 0.999

0.986 / 0.989 / 0.993

… apply / end to end

0.865 / 0.887

0.911 / 0.855

0.892 / 0.812

0.851 / 0.727

0.861 / 0.738

1.019 / 0.992

0.997 / 0.809

sssp OpenMP rgg local10k, sosp_update obj0 / obj1 / obj2

0.756 / 0.733 / 0.763

0.707 / 0.671 / 0.710

0.753 / 0.736 / 0.751

0.717 / 0.684 / 0.720

0.713 / 0.685 / 0.715

1.010 / 1.004 / 1.005

1.006 / 0.994 / 1.000

… apply / end to end

0.768 / 0.893

0.789 / 0.910

0.797 / 0.831

0.783 / 0.705

0.786 / 0.744

0.986 / 0.998

1.024 / 0.791

cycle_count CUDA DD 25K update, original scope / end to end / resident

0.683 / 0.966 / 0.621

0.681 / 0.969 / 0.613

0.673 / 0.949 / 0.614

0.662 / 0.958 / 0.601

0.669 / 0.966 / 0.604

0.993 / 1.000 / 1.011

0.981 / 1.001 / 0.963

cycle_count OpenMP github 25K update / end to end

0.447 / 0.573

0.441 / 0.568

0.443 / 0.531

0.404 / 0.558

0.398 / 0.626

0.999 / 1.002

0.899 / 0.982

(Compact records in parity/results/R020-investigations.json; the full records in $DYNG_SCRATCH/r020/inv-b/records-final.)

What the readings say:

  1. dynG against dynG, F changes nothing beyond noise: every region of B0-BF is within 0.974-1.019 (ADR 0024’s reading over heap layouts). Function alignment alone is not a speed-up; it would only fix where the hot functions fall.

  2. FB is a real speed-up of dynG’s text I/O: dynG’s sssp end to end is 0.79-0.81x of L0 in every round (21 / 21, unimodal; the compute regions within 0.976-1.024), and the cycle_count OpenMP update is 0.90x. The branch-boundary mitigation helps the branchy readers and loops on this Cascade Lake CPU.

  3. The gate ratios move with the originals’ layout as much as with dynG’s: rebuilding the original with F moves the roadNet-CA local10k per-objective ratios from 0.73-0.85 (A0-B0) to 0.61-0.66 (AF-BF), i.e. the original’s short updates got 15-25 % slower from its own placement. Controlling only dynG’s layout (A0-BF) moves the ratios by up to 0.06 either way; controlling both sides is a change to the originals’ build, which the parity rules pin (unpatched, as the original’s build files build it).

  4. Every gated reading passes in every variant with a wide margin: the largest gated ratio in the table is 0.969 (cycle_count CUDA end to end, gate 1.10) and the largest per-objective one 0.933 (gate 1.05). Layout control is not needed for the 0.2 gates to hold, and no variant comes near a gate.

The proposal for the author (attached to the open decision; the decision stays open and criterion 3’s gates use the accepted one-layout protocol of ADRs 0018 / 0021):

  • For the gates against the originals: keep the one-layout protocol (the default), with the originals built exactly as their build files build them. Reason: reading 3; a flag in the parity presets alone would shift the ratios by about as much as the noise it is meant to remove, and aligning the originals changes the pinned baselines. The layout spread (about ±0.06 on the short OpenMP sssp updates, ±0.02 elsewhere) is the known uncertainty of a single reading and is far inside every gate (reading 4).

  • For dynG-against-dynG bars (refactors): keep ADR 0024 (reading over heap layouts and modes).

  • Separately, as a performance option for the release builds (not a protocol change): FB in the release presets and the wheels would make dynG’s end-to-end runs about 20 % faster on this CPU family with no regression elsewhere; it needs its own ADR, a parity run of the release builds, a check with the wheels’ toolchain (gcc-toolset 14 in manylinux_2_28) and a reading on a CPU without the erratum. Recommended for 0.3, not 0.2.0 (it would change the measured library after the certificate’s protocol was fixed).

Deviations and decisions (each with the same intent)#

  1. (a) is read over processes, not with the M6a harness. The M6a row’s tool reads one long-lived process per side; its result for the DD update depends on the process mode of a host stage (above), so the final reading uses 8 processes per side with the same batch, plus Nsight Compute on the kernels. The harness itself is unchanged.

  2. (b)’s aligned originals are rebuilt from the unpatched sources in a separate work area ($DYNG_SCRATCH/r020/inv-b/scratch-F, scratch-FB), never in $DYNG_SCRATCH/ref, so no gate reference changed.

  3. No library change came out of either investigation, so the certificate measures the release code as committed here.

  4. The GPU 1 readings of the interrupted attempt are kept only as the evidence of the process-mode spread, labelled informational; every reading of the tables above is on GPU 0.

Results#

Check

Result

python ci/wheel_check.py --release-metadata

VERSION, CHANGELOG.md and CITATION.cff agree (0.2.0rc1)

cffconvert --validate (cffconvert 2.0)

valid (schema 1.2.0)

scripts/regen.py --check

7 generated files up to date (5 algorithms)

actionlint, zizmor (the two workflows)

passed

ci/check.sh (log $DYNG_SCRATCH/runs/r020-release-changes-check.log)

all checks passed: cpu-only 835/835 and dev 864/864 (ctest -L cpu), clang-format, clang-tidy naming rules, reuse, provenance, regen, the harness and CI-script tests (215 passed), ci/python.sh (stubs, mypy, pytest 544 passed / 66 skipped, installed 0.2.0rc1), ci/api_check.sh against origin/main (no breaking change), the scaffold and tutorial check, ci/docs.sh (Doxygen with the API snapshot of 719 declarations, Sphinx -W, links), pre-commit

Portability pre-checks

not applicable: no C++ or CUDA file changed in this step

Step 4: the release certificate and the distributions (certify)#

What was done#

Commit

What

00f4889

parity/certify.py: the generated-metadata exception (METADATA_PATHS) covers the manifest of every algorithm, derived from cpp/src/algorithms/*/manifest.toml (before: sssp and cycle_count only; open item of step 2), with a test; benchmarks/README.md

bc1304f

CHANGELOG: the section “R020: the release certificate”

dc2979e

This section and the status of this retrospective; the final newline of parity/results/R020-investigations.json (deviation 4)

6c53336

.pre-commit-config.yaml: the certificate’s generated files are exempt from two lint hooks (deviation 6)

afcfb0f

The wording of deviation 6 and of that comment (codespell, deviation 6)

(this commit)

The storm window checked against every gate reading; the three mosp CUDA readings at the boost lock that ran in its tail were taken again (deviation 1); the certificate summary, the readiness and the open items

(the next commit)

The certificate benchmarks/results/0.2.0rc1/: parity.json, README.md, the suite summaries and compacted records, the golden replays, mutation-goldens.json, checks.json and the check excerpts under checks/

How it was measured. Every measured build comes from a clean git clone of the branch. The gates, the golden replays and the golden mutations were measured at 8bc3e61 (clone $DYNG_SCRATCH/r020/cert/clone-a, presets parity and parity-cuda), except the three mosp CUDA readings at the boost lock of roadNet-CA, rgg and road_usa, which were taken again at afcfb0f (clone $DYNG_SCRATCH/r020/cert/clone-r, preset parity-cuda; deviation 1); the commits after 8bc3e61 change no path of the library scope (parity/, CHANGELOG.md, documents), which certify.py write checks. The C++ test suites (the sanitizer presets, the mutation CTests), the portability pre-checks and the distributions ran at bc1304f, whose tests and packaging paths are the release’s; ci/check.sh --parity and ci/gpu_local.sh (the repo scope, every tracked file but benchmarks/results/) ran at this commit, the last one outside benchmarks/results/, from fresh clones. Logs: $DYNG_SCRATCH/runs/r020-certify-phase-a.log (the suites, replays and mutations), r020-certify-mosp-retake.log, r020-certify-mosp-retake2.log and r020-certify-mosp-retake3.log (the readings taken again), $DYNG_SCRATCH/runs/r020-cert/ (the checks); each check names its log with its SHA-256 in checks.json.

The first certificate (f90fb9c; superseded by step 5)#

Golden parity (byte equality with the originals; CUDA on GPU 1):

Golden set

Original

Cases

Configurations

Result

sssp (the whole MOSP update with the combined graph, through dyng-compat-mosp)

MOSP-OpenMP@c352151 (= MOSP-CUDA@e220ee2)

495

sequential, openmp:1 / 4 / 16 / 28 / 56, sequential/int64, openmp:4/int64; cuda, cuda-fused, cuda-operators, cuda/int64, cuda-operators/int64

6435 / 6435 equal

cycle_count (--full)

CycleEnumeration-GPU@0a976ad

24

sequential, openmp:4, openmp:56

72 / 72 equal

cycle_count_cuda

CycleEnumeration-GPU@0a976ad

24

cuda, cuda:resident, cuda:int64

72 / 72 equal

mosp_scale (paper scale, every output file’s SHA-256 and the counters)

MOSP-OpenMP@c352151 (= MOSP-CUDA@e220ee2)

20

sequential, openmp:28; cuda, cuda-operators

80 / 80 equal

Performance gates (PLAN 8.6 against the unpatched originals; the exclusive lock; OpenMP sssp and mosp with 28 pinned threads, cycle_count with 56; CUDA on GPU 0 at the boost clock lock, the COLLAB update at the base lock (ADR 0021), the default-clock readings recorded and not gated (ADR 0018); device memory on GPU 1). Every gated region of the three suites is within its gate: 270 / 270. The worst (largest) ratio port / original per algorithm, backend and gate kind:

Algorithm (suite)

Backend (clocks)

Compute (gate)

End to end (gate 1.10)

Device memory (gate 1.05)

sssp (ipdps25_dynamosp_sosp, 132 regions)

OpenMP

0.991 (1.05)

0.976

-

CUDA (boost)

1.009 (1.10, region under 10 ms)

0.975

0.858

CUDA (default clocks, not gated)

1.062

0.966

-

cycle_count (ieee_tc_dyntrucy, 78 regions)

OpenMP (incl. COLLAB)

0.906 (1.05)

0.855

-

CUDA (boost)

1.003 (1.05)

0.982

1.001

CUDA (base, COLLAB update)

0.999 (1.05)

0.984

-

CUDA (default clocks, not gated)

1.001

0.974

-

mosp (ipdps25_dynamosp_mosp, 60 regions)

OpenMP

0.926 (1.05)

0.955

-

CUDA (boost)

1.005 (1.05)

0.946

1.019

CUDA (default clocks, not gated)

1.009

0.970

-

The best ratios (where dynG is fastest): sssp OpenMP 0.643, cycle_count CUDA 0.223 (the COLLAB k = 3 count, resident), mosp OpenMP 0.648; the full tables are generated at the end of the certificate’s README.md. No reading has a flagged or kept contaminated round, and every record’s inputs were hashed by the harness when its measurement started and equal the suite’s digests. The one default-clock region above 1.05 (sssp CUDA, road_usa local10k, objective 2, 1.062) is the DVFS effect ADR 0018 describes (the original read 20.8 ms there, 22.2 ms at the lock); at the locked clocks the same region reads 0.995.

Checks (checks.json):

Check

Scope

Commit

Result

mutation-goldens (parity/mutate.py run, every mutation of sssp, cycle_count and mosp)

library

8bc3e61

the control passes; 11 / 11 mutations detected (sssp: the skipped subtree invalidation on sequential, OpenMP and CUDA; cycle_count: the double-counted 5-cycles and the weakened ownership rule, host and CUDA; mosp: the combined-edge weight from one tree on each backend, the path costs from one objective’s weights)

asan (ci/sanitizers.sh asan, GCC 12)

tests

bc1304f

835 / 835 tests passed, no report

tsan (ci/sanitizers.sh tsan, GCC 12, OpenMP off)

tests

bc1304f

742 / 742

tsan-openmp (ci/sanitizers.sh tsan-openmp, Clang 18.1.8, Archer)

tests

bc1304f

835 / 835

mutation-ctests (ctest --preset dev-cuda -L mutation, GPU 1)

tests

bc1304f

7 / 7

distributions (below)

packaging

bc1304f

passed

check-parity (ci/check.sh --parity)

repo

this commit

all checks passed: cpu-only 835 / 835 and dev 864 / 864 (ctest -L cpu), clang-format, the clang-tidy naming rules, reuse, provenance, scripts/regen.py --check, the harness and CI-script tests (216 passed), ci/python.sh (stubs, mypy, pytest 544 passed / 66 skipped), ci/api_check.sh, the scaffold and tutorial check, ci/docs.sh (Doxygen with the API snapshot of 719 declarations, Sphinx -W, the links), pre-commit, the parity preset’s golden replay 4 / 4

gpu_local (ci/gpu_local.sh, every step: the CUDA tests, the CUDA goldens, compute-sanitizer memcheck, synccheck and racecheck, the plugin wheel, clang-tidy)

repo

this commit

every step passed: ctest -L gpu 465 / 465, ctest -L cpu in the CUDA build 864 / 864, the sssp corpus on cuda and cuda-operators (990 / 990) and the cycle_count CUDA corpus (48 / 48), memcheck, synccheck (sssp, mosp, cycle_count) and racecheck with 0 errors, the cu13 plugin wheel (pytest -m gpu 55 passed), clang-tidy

The distributions of v0.2.0rc1, built locally as release.yml would build them for that tag (docs/developer/release.md, “Building a release locally”; no tag was created):

  • select (its run block extracted from release.yml, GITHUB_REF_NAME=v0.2.0rc1): kind=package, version=0.2.0rc1, prerelease=true, plugins=true, the distributions dyng, dyng-cu12 (-cu12) and dyng-cu13 (-cu13); v0.2.0 and v0.2.0-rc.1 refused. ci/wheel_check.py --release-metadata passes.

  • The toolkits: step 3 checked on 2026-10-07 that CUDA 12.9 (nvcc 12.9.86) and 13.4 (nvcc 13.4.92) are still the newest 12.x and 13.x of NVIDIA’s RHEL 8 repository (a fresh lock reproduces ci/cuda_toolkits.toml; verify --signatures passes), so nothing was re-pinned.

  • ci/wheels.sh: dyng-0.2.0rc1.tar.gz (0.99 MB) and dyng-0.2.0rc1-cp312-abi3-manylinux_2_28_x86_64.whl (1.70 MB), the whole Python suite in fresh 3.12 and 3.13 venvs (543 passed, 67 skipped).

  • ci/plugin_wheels.sh with DYNG_PLUGINS="cu12 cu13" from that sdist, with CI’s toolkits (the pinned RPMs of ci/cuda_toolkits.toml unpacked by ci/cuda_toolkit.py extract: CUDA 12.9 and 13.4; Wheels, the sdist and the release workflow, “The CI toolkit locally”): dyng_cu12-0.2.0rc1-...whl (6.0 MB) and dyng_cu13-0.2.0rc1-...whl (6.2 MB), the SASS / PTX check of the code objects, and its install tests on GPU 1 (the GPU smoke test, the fallbacks without a device and without a driver, pytest -m gpu 55 passed, the suite with DYNG_CPU_ONLY=1 543 passed) on 3.12 and 3.13.

  • The collect job: twine check --strict on the four files; ci/wheel_check.py on each (--platform manylinux_2_28_x86_64, and --require-libgomp for the core files), and --release-set --split on all four (exactly the release’s distributions, one directory each).

  • Installed into fresh Python 3.12.14 and 3.13.15 venvs with no development environment on PATH (env -i PATH=/usr/bin:/bin), from an empty directory: the core wheel alone, core + dyng-cu12, core + dyng-cu13, and pip install "dyng[cu13]==0.2.0rc1" resolved from a local simple index laid out like TestPyPI (PEP 503 pages with SHA-256 fragments, served on 127.0.0.1) with --extra-index-url https://pypi.org/simple/ (NumPy 2.5.3 from PyPI; pip chose dyng and dyng-cu13 0.2.0rc1 from the local index). In each: the wheel’s test subset (cibuildwheel’s test-command: 269 passed / 2 skipped for the core, 268 / 3 with a plugin), the README quickstart (its output equal to the README’s), dyng --version, and with a plugin ci/plugin_smoke.py --expect cuda and pytest -m gpu with DYNG_REQUIRE_CUDA=1 on GPU 1 (55 passed). SHA-256 of the files: sdist b68a063e..., CPU wheel df96e3d8..., cu12 1a0de18c..., cu13 ff465da0... (built locally; the tag’s files are CI’s builds).

Portability pre-checks (at bc1304f, /home/sskg8/Projects/dyng-work/ci-local/README.txt): the Clang 18 syntax pass over the dev build’s 211 translation units and over the DYNG_BUILD_PYTHON=ON database’s 228: 0 failures; the cpu-only build with -D_FORTIFY_SOURCE=3 and the bindings: clean. No C++ changed in this step.

Deviations and decisions (each with the same intent)#

  1. Five mosp readings were taken again; no gate reading comes from the storm window. From about 13:49 CDT (18:49 UTC) on 2026-10-07 a process outside this work triggered the automounting of network shares (about 7,100 CIFS mounts appeared under another user’s /dfs/<user>/users/), and the mount daemons of every desktop session (udisksd, gvfs-udisks2-volume-monitor, tracker) kept about 15 cores busy. The contamination monitor rejected every round, and the harness stopped the OpenMP reading of road_usa (13:48:56-13:55:09 CDT, 12 rejected rounds, foreign load 2.1-17.9 cores) and the CUDA reading of roadNet-PA at the boost lock (13:55:09-13:57:38, 22 rejected rounds, the foreign load falling from 17.6 to 2.5 cores) without writing their records, as designed. Both were taken again from the same clone and builds after the sssp and cycle_count suites, with parity/bench_suite.py run ... --skip-existing into the same records directory (16:57-17:42 CDT, a quiet machine, no round rejected).

    The storm’s window was then checked against every gate reading of the certificate (the start and end of each perf_ab.py command in the logs). Its CPU load ended with the last rejected round at 13:57:38 CDT, but the next three mosp readings, CUDA at the boost lock of roadNet-CA (13:57:38-14:00:57), rgg (14:00:57-14:05:04) and road_usa (14:05:04-14:21:37), ran in its tail: every round passed the monitor (foreign load at most 1.99 cores), but the load average reached 27.0 (rgg) and 29.9 (road_usa) from tasks waiting on the mounts (uninterruptible, not running: the foreign runnable count stayed at 1-2), against 2-6 in the readings after 14:21. So that the certificate holds no reading from the storm or its tail, these three were taken again on 2026-10-08 between 03:59 and 04:25 UTC (22:59-23:25 CDT), from a fresh clone of afcfb0f (the same library paths as 8bc3e61) built with the parity-cuda preset, under the exclusive lock at the boost lock on GPU 0, with --skip-existing after the three records were moved out of the records directory (kept in $DYNG_SCRATCH/runs/bench/r020-superseded-storm-tail/); one round was rejected and repeated, the foreign load stayed at or below 1.82 cores. The new readings agree with the replaced ones: compute 0.991-1.005 (before 0.992-1.005), end to end 0.755-0.946 (before 0.778-0.967). The other mosp readings ran before the storm (OpenMP roadNet-PA, roadNet-CA and rgg, 13:41:19-13:48:55, foreign load at most 1.89 cores) or after 14:21 (the default-clock readings, not gated; the memory readings, 14:44-14:48; the two readings of the first paragraph); the sssp and cycle_count suites ran from 14:48 to 16:57 CDT. The summary ipdps25_dynamosp_mosp.json covers the whole suite and names both measured commits. No gate was loosened and --keep-contaminated was not used.

  2. tsan-openmp locally needs a wrapper of the conda Clang 18. The development machine has no system Clang; the M6b wrapper was in a scratch directory that no longer exists, so a new one (clang++-18 --gcc-install-dir=/usr/lib/gcc/x86_64-linux-gnu/12 --sysroot=/, the rpath of the conda libomp at link time) was written, plus -Wno-error=deprecated-declarations: GCC 12’s libstdc++ calls std::get_temporary_buffer in std::stable_sort, which Clang 18 reports as deprecated in C++17 (the same diagnostic clang_syntax.py filters; the hosted job uses Ubuntu’s Clang with its own libstdc++). Archer is the conda env’s libarcher.so.bak, passed with DYNG_ARCHER_LIBRARY. Nothing of the sanitizer configuration changed; the hosted tsan-openmp / clang-18 job, a required check, remains the reference.

  3. The checks ran from several clones of the same commits (one per stream: the sanitizers; the mutation CTests, the pre-checks and the distributions; ci/check.sh --parity; ci/gpu_local.sh), concurrently under the shared lock and never during a timing run. ci/check.sh --parity and ci/gpu_local.sh ran at bc1304f, at dc2979e, at afcfb0f (after deviation 6) and, after the readings of deviation 1, again at this commit, whose runs are the records of the certificate; the counts above are the same in each run.

  4. A missing final newline. The first ci/check.sh --parity at bc1304f failed only its pre-commit step: parity/results/R020-investigations.json (step 3) had no newline at its end (the end-of-file-fixer hook). This commit adds it; no other file changed between the two runs but this retrospective. Beyond ci/check.sh, ci/api_check.sh 8b9067e (against main as pushed; a clone’s origin/main is the development repository’s stale local main) reports no breaking change.

  5. The golden mutations are recorded as one check, mutation-goldens (all eleven mutations of the three suites; 0.1.0 recorded mutation-sssp).

  6. Two lint hooks exempt the certificate’s generated files. Staging benchmarks/results/0.2.0rc1/ showed two pre-commit failures that ci/check.sh (which lints the tracked files) could not see before the directory was committed: parity.json is 314 KB, above the 256 KB of check-added-large-files (0.1.0rc1’s was 245 KB; 0.2 adds mosp’s suite, the mosp_scale digests and more replay configurations), and codespell flags fragments of wrapped tool output (paths cut mid-word by twine’s line wrapping) in the verbatim log excerpts under checks/. .pre-commit-config.yaml now excludes benchmarks/results/<version>/parity.json from the size hook and benchmarks/results/<version>/checks/ from codespell; both are generated, committed as evidence and never edited, and every other file keeps both hooks. The file is not in the sdist, so the distributions are unchanged; the repo checks ran again. (At 6c53336, ci/check.sh failed once more, on codespell: this paragraph quoted such a fragment. It was reworded, the run of ci/gpu_local.sh at 6c53336 was stopped, and both ran at this commit.)

Step 5: the review fixes and the certificate measured again (review-fixes)#

Independent reviewers checked the branch at f90fb9c through four lenses (release readiness, the certificate, the mosp API freeze, packaging and the user’s view); a skeptic confirmed 26 findings (two of them the same gap, the packaging scope, seen by two lenses). All are fixed; of the two that needed the author, layout control was decided on 2026-10-08 and the PyPI names of the plugins are an open decision in GOVERNANCE.md (below). One finding, the end-to-end slowdown since 0.1.0, led to a library change (the CSR-triplet reader), so the whole certificate was measured again on the final code; the orchestrator wanted the release candidate’s code to be the final 0.2.0’s.

What changed#

Commit

What (the findings it fixes)

e320582

<dyng/mosp.hpp>: options::compute_path_costs takes effect at the next compute() / update() (the field comment contradicted path_costs()’s @throws and the code; no longer “a tunable”), set_options() says when its options take effect, update()’s @throws invalid_argument_error adds the backend-mismatch and copy-policy cases of sssp::update(); the Python Options, Result.set_options, module (the determinism exception of non-canonical imported trees) and Stats (converged, fallback_used, engine_used) docstrings and the option table of docs/algorithms/mosp.md; the tests MospBackend.ComputePathCostsTakesEffectAtTheNextUpdate and test_compute_path_costs_takes_effect_at_the_next_update. Comments only: the API baseline is unchanged

0b6de64

find_package(dyng 0.2 REQUIRED) in README.md and the install guide (0.1 refused 0.2.0rc1: SameMinorVersion before 1.0), the comment of dyng-config.cmake.in without a number

5a6cd66

Both suite files and benchmarks/README.md: rgg’s unsafe50k batch is byte-identical to safe50k (the original’s --safe filter drops only the deletions that disconnect a vertex from the source, and none of rgg’s does), so its readings are not independent coverage

10dc59d

parity/certify.py: REQUIRED_CHECKS by release series with the narrowest scope of each record and gpu_local’s compute-sanitizer steps; the golden-mutation record read against parity/mutate.py list; every gated row recomputed from the compacted record and the suite’s tolerance (recheck_gates); measured_commits full SHAs, once each; the packaging scope covers the plugins’ build inputs and every sdist member (sdist_members()), tests covers cpp/fuzz; tests for each (parity/tests/test_release_cert.py)

878c122

The C++ freeze is enforced: ci/api_snapshot.py --against <base> in ci/api_check.sh (api-check.yml, ci/check.sh) fails on a breaking change of a frozen section without the api-change label and a CHANGELOG entry; lint.yml’s harness job and api-check.yml install griffe and set DYNG_REQUIRE_GRIFFE=1, so ci/tests/test_api_check.py no longer skips itself in CI; ADR 0035 amendments A1-A3; docs/developer/api_review_checklist.md

8c06a99

python/README.md (the PyPI description) names mosp as stable and runs it in its snippet; dyng mosp update in its CLI block

148c252

dyng.show_config() prints the pip hint only when no plugin is installed; the selection reason says whether a plugin of the driver’s major is installed

02ee6ad

The dyng command shows warnings as dyng: warning: ... (not the console-script wrapper’s line)

2306207

The getting-started next steps name mosp and the tutorial algorithms; the wheel’s test subset runs test_mosp.py

9fa9f0a

ci/cmake_consumer.sh: the install guide’s CMake block builds a consumer against an installed build (the consumer step of ci/check.sh, the cpu-only jobs of cpu.yml)

1a689cf

The release candidate’s plugin install from TestPyPI alone (--no-deps, NumPy from PyPI first) in README.md, the install guide, the CHANGELOG and the roadmap; docs/developer/release.md (every packaging- and repo-scope check, api-check included, re-run on a later tag day and in the final-release carry-over; step 3: how the hosted sanitizers.yml jobs of the release pull request are recorded; step 4: wheels.yml with the plugins green on the pull request’s head before the merge; step 8 runs mosp; the final-release checklist in version-neutral terms with docs/getting_started/index.md and the DOI sentence of docs/citing.md); GOVERNANCE.md: the plugins’ PyPI names a new open decision; the roadmap’s sentence on who merged 0.1.0’s pull requests

aafb8dc, 1731a87

CHANGELOG: the section “R020: review fixes”; the release date 2026-10-08 (CHANGELOG heading, CITATION.cff; cffconvert --validate passes)

882c31a

GOVERNANCE.md: the author’s decision on layout control (2026-10-08, below) in the approvals log; the open decision removed

b1a4869

io::read_csr_triplet(): the end-to-end slowdown since 0.1.0 recovered (below); the tests TextScanner.NumbersMatchTheGeneralPath and CsrTriplet.ReadsEveryLayoutOfTheTextFormat; CHANGELOG

(this commit)

This section; the first certificate’s section marked superseded

(the next commit)

The certificate benchmarks/results/0.2.0rc1/, replaced

The findings and where each went#

#

Finding (lens)

Fixed in

1

The tag-day procedure forgot the api-check record (release readiness)

1a689cf (release.md, both places); readiness item 5 below

2, 14

The packaging scope missed the plugins’ build inputs and sdist members (release readiness, certificate)

10dc59d (with a test that every sdist member is in the scope and that a plugin input invalidates distributions)

3

Step 3’s hosted sanitizers.yml run is not in the certificate (release readiness)

1a689cf (release.md step 3); deviation 2 and readiness item 3 below

4

The layout-control decision was due before v0.2.0 without a record (release readiness)

the author decided it on 2026-10-08; 882c31a

5

Stale orchestrator items (27 required checks; ADR 0035 vs M8) (release readiness)

this section: both settled (the ruleset changed on 2026-10-07; M8’s ADRs start at 0036)

6

The final-release checklist named 0.1’s texts (release readiness)

1a689cf

7

Step 8’s CPU smoke test without mosp (release readiness)

1a689cf

8

The roadmap said the author merged 0.1.0’s pull requests (release readiness)

1a689cf

9

wheels.yml missing from the readiness (release readiness)

1a689cf (release.md step 4); readiness item 2 below

10

certify.py did not require the release checks (certificate)

10dc59d

11

certify.py trusted the summaries’ within_gate (certificate)

10dc59d (recheck_gates: ratio, gate and verdict recomputed from the compacted records)

12

dynG’s end to end 5-12 % slower than 0.1.0, unreported (certificate)

b1a4869 and the next section; the certificate README

13

rgg’s unsafe50k batch equals safe50k (certificate)

5a6cd66; the certificate README

15

The README’s “CUDA (fused)” for mosp_scale; duplicate SHAs in measured_commits (certificate)

10dc59d (full SHAs, once each); the new certificate’s README says “the automatic engine”

16

compute_path_costs’s comment contradicted path_costs() (mosp freeze)

e320582

17

The C++ freeze was only a label (mosp freeze)

878c122

18

The Python freeze test skipped itself in CI (mosp freeze)

878c122

19

update()’s @throws incomplete (mosp freeze)

e320582

20

Python determinism and Stats docs (mosp freeze)

e320582

21

find_package(dyng 0.1) refuses 0.2 (packaging)

0b6de64, 9fa9f0a

22

The PyPI description without mosp (packaging)

8c06a99

23

The rc’s install mixed TestPyPI with PyPI for unregistered plugin names (packaging)

1a689cf (the install from TestPyPI alone); reserving the names is the author’s: open decision

24

show_config()’s pip hint for an installed plugin (packaging)

148c252

25

The CLI’s BackendWarning named the wrapper’s line (packaging)

02ee6ad

26

Getting-started next steps and the wheel’s test comment (packaging)

2306207

The end-to-end slowdown since 0.1.0 (finding 12)#

Attribution. A dynG-against-dynG stage A/B (the dyng-compat-mosp report lines and profiler stages of 0.1.0’s d13d393 against 5a6cd66, sssp OpenMP 28 threads, safe50k, 11 alternated runs per side under the exclusive lock; $DYNG_SCRATCH/runs/r020-e2e-ab/stages.log) put all of the growth in one stage, [graph] read graph, the call of io::read_csr_triplet(): rgg +104.2 ms of +103.7 ms end to end (583.2 -> 687.4 ms), roadNet-PA +27.5 of +19.4; every compute stage, the batch read and the tree import were unchanged (within 1-7 ms, mostly faster). The ADR 0024 reading over 17 heap layouts (sssp-openmp-<graph>.json there) agrees: end to end 1.109-1.116 (rgg) and 1.070-1.079 (roadNet-PA) for 0.99-1.01 on sosp_total.

The reviewer suspected 0b388f7 (M6b’s bounded reservations: read_values counts the line breaks of the whole Values file). That count runs only on the strict path, which re-reads a file after an error: the concurrent pass, the only one a valid graph takes, already counted them in 0.1.0 (its m is unknown there). The success path of the reader is the same source in 0.1.0 and here (git diff d13d393 -- cpp/src/io/csr_triplet.cpp cpp/src/util touches only the strict path), so the 18 % was, most likely, code placement: the text reader’s hot loop moved with the library around it (the layout sensitivity of the text readers that investigation (b) measured, about 20 % from function and branch alignment on this CPU, which has the JCC erratum). Reading the three files alone (a driver linking each build’s libdyng.so, 15 alternated reads each, exclusive lock): 0.1.0 578 ms, 5a6cd66 692 ms on rgg; 143 and 167 ms on roadNet-PA. The Values file (121 MB, 41 million weights) is the critical one of the three concurrent reads (about 525 of the 530 ms on rgg).

The fix (b1a4869). text_scanner::next_number() / next_number_in_line() scan a token and, when it is 1 to 18 decimal digits, return its value on the way, so its characters are read once; detail::integer_value() applies the range check and sends every other token (a sign, 19 or more digits, anything else) through parse_integer(), so values, error messages and locations are exactly the general path’s (pinned by TextScanner.NumbersMatchTheGeneralPath over the token classes and three ranges). read_values() writes each weight straight into its objective’s column once the columns exist, instead of through a per-line vector. Nothing of the fuzz guards changed: the reservations stay bounded by what the file can hold (0b388f7), the strict re-read on any error is the same, the corpus and regression replays pass (ctest -L fuzz, 14 tests), and ci/fuzz.sh --time 120 csr_triplet ran 37,318 inputs without a finding. Matrix Market, the batch and result readers keep the general path (they are not on a gated path; the edge-list reader of cycle_count has its own parser, unchanged since 0.1.0, whose static_read moved -2 % to +4.5 % between the two certificates).

Before and after (the reader alone, as above): rgg 578 ms (0.1.0) / 692 ms (5a6cd66) / 507 ms (b1a4869), roadNet-PA 143 / 167 / 121 ms. End to end, the stage A/B of 0.1.0 against b1a4869 ($DYNG_SCRATCH/runs/r020-e2e-ab2/stages.log): rgg safe50k end to end 1025.1 ms (0.1.0) -> 931.9 ms (-9.1 %), of which [graph] read graph 625.6 -> 527.5 ms; roadNet-PA 312.2 -> 289.7 ms (-7.2 %), read graph 133.7 -> 116.0 ms (11 alternated runs per side, medians; the other stages as before). In the certificate, against 0.1.0’s certificate (the same suite file and protocol), over the 24 sssp end-to-end readings, dynG’s median moved -3.6 % (from -11.8 to -1.0 %; the first certificate of 0.2.0rc1: +8.2 %, from +5.3 to +12.2 %), the originals’ -0.6 % (first certificate: -3.4 %), and the stages the fix does not touch did not move (sosp_total median 0.0 %, apply -0.8 %). mosp’s 24 end-to-end readings moved -9.2 % against the first certificate (from -17.8 to -4.5 %) and -4.4 % against M7’s (from -11.4 to -0.8 %), its compute 0.0 %. Examples (dynG’s median end to end, ms: 0.1.0’s certificate or M7 / the first certificate / this one):

Reading

Before (0.1.0 or M7)

First certificate (8bc3e61)

This certificate (b1a4869)

sssp OpenMP, rgg safe50k

967.5

1079.9

890.4

sssp OpenMP, roadNet-PA safe50k

304.7

332.9

283.9

sssp CUDA, road_usa safe50k

5203.6

5661.7

5042.2

mosp OpenMP, rgg safe50k (M7)

1249

1359.7

1135.8

mosp OpenMP, roadNet-CA safe50k (M7)

807

855.7

757.6

mosp CUDA, rgg safe50k (M7)

1275

1382.4

1246.9

The author’s decision on layout control (2026-10-08)#

The author accepted investigation (b)’s proposal: the gates against the originals keep the one-layout protocol of ADRs 0018 / 0021 (the originals built exactly as their build files build them), dynG-against-dynG bars keep ADR 0024, and the alignment flags (-falign-functions=64, -falign-loops=32, -Wa,-mbranches-within-32B-boundaries) are not in 0.2.0 but a performance option for 0.3 with their own ADR, a parity run of the release builds, a check with the wheels’ toolchain and a reading on a CPU without the erratum. Recorded in GOVERNANCE.md’s approvals log (882c31a); the open decision is closed, so step 10’s due decisions no longer include it.

How the certificate was measured again#

Every measured build comes from a clean git clone of the branch. The gates, the golden replays and the golden mutations (phase A) ran at b1a4869, the last commit of the library scope (clone $DYNG_SCRATCH/r020/cert3/clone-a, presets parity and parity-cuda; log $DYNG_SCRATCH/runs/r020-cert3-phase-a.log, records $DYNG_SCRATCH/runs/bench/0.2.0rc1; the first certificate’s records moved to runs/bench/0.2.0rc1-cert2). The sanitizer presets, the mutation CTests, the portability pre-checks and the distributions ran at b1a4869 too, whose tests and packaging paths are the release’s (runs/r020-cert3-stream1.log, r020-cert3-stream2.log); ci/check.sh --parity, ci/api_check.sh 8b9067e and ci/gpu_local.sh (the repo scope) at this commit, the last one outside benchmarks/results/. The CUDA readings at the boost lock on GPU 0, the memory readings and the replays on GPU 1; the orchestrator had reset both GPUs’ clocks after the interruption.

One reading taken again. Phase A ran from 12:41 to 20:01 UTC on 2026-10-08. Its first reading, mosp OpenMP on roadNet-PA, was stopped by the contamination monitor without a record (22 rejected rounds, foreign load 2.0-24.9 cores; the harness stopped at 12:55:53 UTC). It had waited for the shared lock behind this step’s own checks and, most likely, took the exclusive lock when their last locked step ended (12:53:36) and then ran alongside the unlocked part of the distributions check (the installs and test runs in fresh venvs, 12:53:37-12:56:28 UTC, which the scratch script dist.sh runs outside the lock). The harness behaved as designed: the reading wrote no record and the suite’s summary failed with “missing”. The reading was taken again on its own, on a quiet machine, from the same clone and build (parity/bench_suite.py run benchmarks/paper/ipdps25_dynamosp_mosp.yaml --skip-existing, 20:04-20:06 UTC; log $DYNG_SCRATCH/runs/r020-cert3-rerun-pa.log). No round was rejected and the foreign load was at most 1.23 cores. The summary then covers the whole suite (60 / 60 gated regions). Every other reading of the three suites completed in its first run (the few rounds the monitor rejected were repeated, as the protocol says), and the readings waited for the lock behind ci/check.sh --parity and ci/gpu_local.sh, which hold the shared lock for their steps.

Golden parity (byte equality with the originals; CUDA on GPU 1; at b1a4869):

Golden set

Original

Cases

Configurations

Result

sssp (the whole MOSP update with the combined graph, through dyng-compat-mosp)

MOSP-OpenMP@c352151 (= MOSP-CUDA@e220ee2)

495

sequential, openmp:1 / 4 / 16 / 28 / 56, sequential/int64, openmp:4/int64; cuda, cuda-fused, cuda-operators, cuda/int64, cuda-operators/int64

6435 / 6435 equal

cycle_count (--full)

CycleEnumeration-GPU@0a976ad

24

sequential, openmp:4, openmp:56

72 / 72 equal

cycle_count_cuda

CycleEnumeration-GPU@0a976ad

24

cuda, cuda:resident, cuda:int64

72 / 72 equal

mosp_scale (paper scale, every output file’s SHA-256 and the counters)

MOSP-OpenMP@c352151 (= MOSP-CUDA@e220ee2)

20

sequential, openmp:28; cuda (the automatic engine), cuda-operators

80 / 80 equal

Performance gates (the protocol of step 4: PLAN 8.6 against the unpatched originals, the exclusive lock, OpenMP sssp and mosp with 28 pinned threads, cycle_count with 56, CUDA on GPU 0 at the boost clock lock, the COLLAB update at the base lock (ADR 0021), the default-clock readings recorded and not gated (ADR 0018), device memory on GPU 1). Every gated region of the three suites is within its gate: 270 / 270 (sssp 132, cycle_count 78, mosp 60). The worst (largest) ratio port / original per algorithm, backend and gate kind:

Algorithm (suite)

Backend (clocks)

Compute (gate)

End to end (gate 1.10)

Device memory (gate 1.05)

sssp (ipdps25_dynamosp_sosp, 132 regions)

OpenMP

0.989 (1.05)

1.036 (apply, roadNet-PA unsafe50k; 0.1.0: 1.046)

-

CUDA (boost)

1.007 (1.10, region under 10 ms)

0.947

0.858

CUDA (default clocks, not gated)

1.061

0.960

-

cycle_count (ieee_tc_dyntrucy, 78 regions)

OpenMP (incl. COLLAB)

0.951 (1.05)

0.814

-

CUDA (boost)

1.004 (1.05)

0.980

1.001

CUDA (base, COLLAB update)

0.999 (1.05)

0.987

-

CUDA (default clocks, not gated)

1.003

0.980

-

mosp (ipdps25_dynamosp_mosp, 60 regions)

OpenMP

0.927 (1.05)

0.859

-

CUDA (boost)

1.005 (1.05)

0.880

1.019

CUDA (default clocks, not gated)

1.008

0.895

-

The best ratios: sssp 0.643 (CUDA, apply of roadNet-PA local10k), cycle_count 0.220 (CUDA, the steady update chain of twitch), mosp 0.597 (OpenMP compute, roadNet-PA local10k). No accepted reading has a flagged or kept contaminated round, and every record’s inputs were hashed by the harness when its measurement started and equal the suite’s digests. The one default-clock region above 1.05 (sssp CUDA, road_usa local10k, objective 0, 1.061) is the DVFS effect of ADR 0018, as in the first certificate.

Checks (checks.json):

Check

Scope

Commit

Result

mutation-goldens (parity/mutate.py run)

library

b1a4869

the control passes; 11 / 11 mutations detected (as in step 4)

asan (ci/sanitizers.sh asan, GCC 12)

tests

b1a4869

839 / 839 tests passed, no report

tsan (ci/sanitizers.sh tsan, GCC 12, OpenMP off)

tests

b1a4869

745 / 745

tsan-openmp (ci/sanitizers.sh tsan-openmp, Clang 18.1.8, Archer; step 4, deviation 2)

tests

b1a4869

839 / 839

mutation-ctests (ctest --preset dev-cuda -L mutation, GPU 1)

tests

b1a4869

7 / 7

distributions (as in step 4, plus the documented --no-deps install form)

packaging

b1a4869

passed: the four files, 12 venvs

check-parity (ci/check.sh --parity)

repo

this commit

every step passed (counts in checks.json)

api-check (ci/api_check.sh 8b9067e: the frozen C++ API and the Python API)

repo

this commit

no breaking change

gpu_local (ci/gpu_local.sh, every step)

repo

this commit

every step passed, compute-sanitizer memcheck, synccheck (sssp, mosp, cycle_count) and racecheck with 0 errors

The portability pre-checks at b1a4869: the Clang 18 syntax pass over the dev build’s 211 translation units and the bindings’ 228, 0 failures; the cpu-only build with -D_FORTIFY_SOURCE=3 and the bindings, clean. The distributions are those of step 4 rebuilt at b1a4869 (the same toolkits, still the newest 12.x and 13.x of NVIDIA’s RHEL 8 repository on 2026-10-08: $DYNG_SCRATCH/runs/r020-cert2-toolkits.log, verify --signatures passed); in the fresh 3.12 and 3.13 venvs, the wheel’s test subset (now with test_mosp.py) 295 passed / 2 skipped for the core and 294 / 3 with a plugin, pytest -m gpu 55 passed, and the documented form (NumPy from PyPI, then --no-deps dyng and dyng-cu13 from the local index) passed ci/plugin_smoke.py --expect cuda.

Deviations and decisions (each with the same intent)#

  1. A library change after the review. The review found the end-to-end slowdown on a stable path; the orchestrator asked for it to be fixed in this step so that 0.2.0rc1’s code is the final 0.2.0’s. The change is confined to the CSR-triplet reader’s success path and keeps its values and errors (above); every part of the certificate was measured again on it.

  2. The hosted sanitizers.yml jobs are not in this certificate (release step 3 from 0.2.0). They exist only once the release pull request is open, and the branch has never been pushed; the local asan, tsan and tsan-openmp records are in it (the last with the local Clang 18 wrapper of step 4’s deviation 2, so the hosted tsan-openmp / clang-18 job stays the reference). The hosted jobs of the pull request’s head are recorded with parity/certify.py check --name sanitizers-hosted in a later commit of that pull request or in the tag-day pull request (release.md step 3; readiness item 3). They are required checks of main, so the pull request cannot be merged without them green.

  3. The clones’ origin/main is main as pushed. A clone of the development repository has the repository’s stale local main (7d64c90, before M6b) as origin/main; since 878c122, ci/check.sh’s API step also compares the frozen C++ API with it, and a provisional run at b1a4869 failed there only: against 7d64c90 the enumerator tutorial of M6b is new. Every check clone of this step therefore sets origin/main to 8b9067e (git update-ref, the merge of M6b as pushed) before it runs; nothing in the repository changed, and against 8b9067e neither the C++ nor the Python API has a breaking change.

  4. The informational driver. The before/after reading of the reader alone uses a small driver in the scratch directory linking each build’s libdyng.so (not a harness of the repository); it is informational, as finding 12 is, and no gate rests on it.

  5. One gate reading was taken again (mosp OpenMP roadNet-PA, above): the contamination monitor stopped it while this step’s own distributions check ran its unlocked venv tests. It was taken again alone with --skip-existing from the same clone and build; no gate was loosened and --keep-contaminated was not used. Future check scripts should keep their venv test runs under the shared lock too (open item 7 below).

Results#

The review’s 26 confirmed findings are fixed (the table above); the one that needs the author is open item 1 below. The second certificate of 0.2.0rc1 (benchmarks/results/0.2.0rc1/, the next commit) replaces the first: every part was measured at b1a4869 (the library, tests and packaging scopes) or at this commit (the repo scope), all on the final code, and parity/certify.py write and check pass with the new required checks and the recomputed gates. Golden parity holds on every backend (sssp 6435 / 6435, cycle_count 72 + 72, mosp_scale 80 / 80), the 270 gated regions are within their gates, all eleven golden mutations and the seven mutation CTests are detected, and the sanitizer presets, the distributions, ci/check.sh --parity, ci/api_check.sh and ci/gpu_local.sh pass. dynG’s end-to-end readings are faster than 0.1.0’s again (sssp median -3.6 %), so the slowdown of finding 12 is gone; compute is unchanged.

Readiness of 0.2.0rc1#

Ready for the release-candidate tag, from the AI assistant’s side. Done: criteria 1-7 of the milestone (steps 1-5) and every confirmed review finding; the certificate states all parts passed (parity.json, written on the committed tree). Waiting for the author or the orchestrator, in order:

  1. The orchestrator pushes release-0.2.0 and opens the release pull request; hosted CI runs its 27 required checks (the three sanitizer jobs among them since 2026-10-07, GOVERNANCE.md).

  2. wheels.yml with the plugins (manual dispatch on the pull request’s head, or its run on the pull request, which touches VERSION) is green before the author merges, and the run is linked in the pull request (release.md step 4; not a required check, and the only build of the published setup).

  3. The three hosted sanitizers.yml jobs of the pull request’s head are recorded with parity/certify.py check --name sanitizers-hosted --scope tests --commit <head> --result passed --evidence <the job logs> and parity.json written again, in a later commit of the pull request or in the tag-day pull request (deviation 2).

  4. The author merges the pull request (2026-10-07).

  5. The tag day: the tag is pushed on 2026-10-09 (UTC), so the CHANGELOG heading and CITATION.cff were moved from 2026-10-08 to the tag day on this branch, before the release pull request (A tag day later than the final-release pull request; cebbd29), and distributions, check-parity, api-check (against 8b9067e) and gpu_local were run again from fresh clones of the next commit and recorded, then parity/certify.py write --version 0.2.0rc1 (the last commit of the branch).

  6. The AI assistant tags v0.2.0rc1 on the merge commit and pushes it (2026-10-06); release.yml uploads the plugins to TestPyPI (testpypi-cu12, testpypi-cu13), then dyng (testpypi); a pre-release stops there. Then step 8: the smoke install from TestPyPI (mosp included), both plugins as CI built them on the GPU machine (The GPU tests of the published plugin wheels (steps 8 and 10)).

  7. The GitHub pre-release (the AI assistant, 2026-10-06).

Open items#

For the author:

  1. The open decision on the PyPI names dyng-cu12 / dyng-cu13 (GOVERNANCE.md, R020 review): reserve them, for example with a 0.0.1 placeholder through pypi-cu12 / pypi-cu13, before the release candidate is announced, or accept the window until 0.2.0. Until then the documented commands install the plugins from TestPyPI alone.

  2. Zenodo is still not connected, so 0.2.0 gets no DOI unless it is connected before the GitHub Release of 0.2.0 (release.md step 12).

For the orchestrator:

  1. The record of the hosted sanitizer jobs (readiness item 3); the tag date and its re-run checks are done (item 5).

  2. The local branch main of the development clone is stale (7d64c90); ci/api_check.sh was run against 8b9067e (main as pushed) and is unaffected.

  3. Settled since step 4 (no action): the main ruleset requires the 27 checks since 2026-10-07 (GOVERNANCE.md), and M8 (m8a-escher-store) numbers its ADRs from 0036, so ADR 0035 does not clash.

Carried over, not blocking 0.2.0rc1:

  1. update_traits::make_participant of sssp, cycle_count and mosp reads container_t::edge_type (detail, not public API); M8b’s hypergraph composition must say why it does not compile (ADR 0035).

  2. The io/*, testing/* and generators/* headers that hold the mosp helpers are still tracked, not frozen (their review, deferred by ADR 0023, is not recorded yet).

  3. A future wheel-vs-parity row of a short host-heavy update should be read over several processes per side (step 3, investigation (a)).

  4. The local tsan-openmp toolchain (step 4, deviation 2) could be made reproducible, for example a ci/local_clang.sh wrapper or a conda environment file with a matching libstdc++; the hosted job is unaffected.

  5. The text readers stay sensitive to code placement on this CPU (finding 12, investigation (b)): the 0.3 performance option of the author’s decision would address it; the reader’s remaining cost on rgg is about half reading and allocating (the whole-file read zero-fills its buffer, the line count), half parsing.

  6. ci/api_snapshot.py --against compares declarations line by line, so an enumerator appended to a frozen enumeration (maturity_level::tutorial of M6b, seen against the stale 7d64c90, deviation 3) counts as a breaking change; it fails safe (the api-change label and a CHANGELOG entry pass it), but 0.3’s new maturity or backend values will need the label until the tool treats an appended enumerator as an addition.

  7. The local distribution check of this step (a scratch script modelled on release.md, “Building a release locally”) ran its venv installs and tests outside the shared perf lock, so it could overlap a timing run (deviation 5 of step 5); the monitor caught it. A repository script for the local release build (for example ci/release_local.sh) should run every heavy step under flock -s.