Retrospective: M1b (CUDA sssp, CUDA core, performance harness and gates)#
Status: complete (2026-09-28) with one open gate decision; reviewed and fixed. Steps 1-4 each appended their section; Step 5 (finish) added the documentation, the Doxygen synchronization rule, the final verification from a fresh clone of every preset, the milestone summary, the acceptance record, the consolidated deviations, the lessons and the re-estimate. An independent review then found 19 problems; all are fixed and the gates were re-measured on the fixed code (section “Review and fix step” at the end). One acceptance reading is still not met as written and waits for the author (criterion 3a, CUDA road_usa local 10K batch, 1.054-1.060x as measured, ADR 0018).
Step 1: the OpenMP gates and the M1a carry-over (cpu-gates, 2026-09-27)#
Goal: close the M1a carry-over (retrospective M1a, “Open items carried forward” 1 and 2; ADR 0013 “Open M1b blocker”) so that the OpenMP backend meets the PLAN 8.6 gates against MOSP-OpenMP@c352151 on the four graphs of PLAN 6.4.2, before the CUDA gate is read.
Done#
Workspace sharing (ADR 0015).
detail::workspace_pool(cpp/src/framework/workspace.hpp) owned by everyresourceshandle and shared by its copies.compute(),from_arrays(),clone()and each result’s part ofupdate()lease the pooledsssp_workspace, size it once and return it: the K objectives ofupdate_each()share one workspace, asmospUpdate()shares itsSospWorkspace; a steady-state update allocates no scratch memory; concurrent calls on copies of a handle lease distinct workspaces; a run that throws discards its workspace (a failed run may leavein_farflags set). The per-objective change lists moved into the workspace. Public additions:resources::release_workspaces(),resources::workspace_bytes();set_memory_resource()releases the idle workspaces. The M1a pre-touch (sssp.workspace.pretouch) and the “first touch counted” reading of the A/B are gone: both sides now pay objective 0’s first touch inside its region.Graph construction without a transposition. The in-edges are built on first use and cached per graph state (thread-safe);
run_update()builds them inside the commit for the updated graph only.from_csr()checks the CSR in one parallel pass and takes a CSR that already has the requested form as it is; a new rvalue overload moves the arrays. The M1a load-time transposition (213 ms on roadNet-CA) is gone.Parallel where the original is, identical results. On the OpenMP backend: the CSR assembly of
apply(degrees, offsets, block-wise copies), the import and thevalidate_inputschecks offrom_arrays()(a chain walk like the engine’s invalidation; any problem re-runs the sequential checks, so the message is unchanged). The transposition’s scratch arrays are no longer zero-filled by one thread.Reading.
read_file()reads a file with one allocation and onefread(the 64 KB-chunk growth copied large files several times); the distance and tree files are parsed in one pass withstd::from_chars(falling back to the strict reader on anything irregular); the Values file is parsed straight into the objective-major columns (no conversion pass).Harness.
dyng-compat-mospbuilds the K results one after the other on its own handle and splits the input phase in its report;timed_regions/sssp.tomlcompares every objective as measured and gatesapplyandend_to_endat 1.10x;perf_ab.pyreads theend_to_endgate, knows the local-batch radius of each graph (bench/prepare.sh: 110, 160, 110, 200) and writes schema 3. Benchmark inputs for roadNet-PA, rgg_n_2_20_s0 (rgg) and road_usa (road_usa_g) prepared with the unpatched original’smospPrep(2.4 GB under$DYNG_SCRATCH/bench/mosp).Tests (227 on
dev, all green; alsocpu-only,asan,tsan): the pool (reuse, distinct overlapping leases, discard on failure, 8 threads x 2000 leases), sssp on the pool (one workspace for K objectives, constant pool bytes over repeated updates, two graphs and two handles, failed update, concurrent computes, parallel validation messages), lazy in-edges,from_csrequal tofrom_edgesfor every property combination (copy and move, both backends), OpenMP apply assembly equal to sequential on 80K-vertex graphs, fast and strict readers agree. Every intermediate commit of the step builds with-Werrorand passes its tests.Parity: the golden replay (495 cases x sequential / OpenMP 1, 4, 16) is byte-identical after the workspace change, after the I/O and apply changes (
ci/check.sh --parity) and on the committed code in both theparityand thedevpreset (JSON records); the A/B checks byte-identical outputs on all 12 graph x batch combinations and equalinvalidatedcounters in every round.
Measured (details and every number: parity/results/M1b.md)#
OpenMP A/B at 79400d2 against the unpatched MOSP-OpenMP@c352151, 28 threads pinned, medians of
21 alternating rounds, all three batches (50K safe, 50K unsafe, 10K local); ratio dynG / original:
Graph |
SOSP region per objective (gate 1.05x) |
apply (gate 1.10x) |
end to end (gate 1.10x) |
|---|---|---|---|
roadNet-PA |
0.78-0.95x |
0.78-0.91x |
0.80-0.83x |
roadNet-CA |
0.81-0.99x |
0.84-0.88x |
0.76x |
rgg_n_2_20_s0 |
0.82-1.01x |
0.77-0.80x |
0.83-0.85x |
road_usa |
0.84-0.96x |
0.79x |
0.77-0.81x |
Every gate is met (36 per-objective, 12 apply and 12 end-to-end readings). In M1a, on
roadNet-CA alone, objective 0 was 1.07-1.44x with its moved first-touch cost counted and end to
end 1.30-1.41x. Golden replay: 495/495 byte-identical on sequential and OpenMP 1/4/16 threads in
the parity and dev presets.
Deviations from the plan (pragmatic choices, same intent)#
Plan |
What was done |
Why |
|---|---|---|
PLAN 4.7.2 / 5.1: workspaces live inside |
the |
MOSP shares one workspace across the objectives; per-result workspaces cost K times the memory and made the per-objective gate unmeetable without moving cost out of the region (M1a) |
PLAN 4.7.1 |
+ |
the cache needs a release and a memory report (PLAN 8.6 memory gate) |
PLAN 5.2 |
+ |
taking the arrays over avoids a copy of the whole graph (about 1 GB on road_usa) |
|
the in-edges are built on first use and cached per graph state; |
a graph built only to receive a batch never needs its own transposition (MOSP builds the reverse graph once, of the updated graph) |
PLAN 8.6 gates: compute regions and end to end |
also the |
the draft map of M1a announced it; it keeps the non-kernel work honest |
— |
parallel CSR assembly in |
the end-to-end gap was in the surroundings, not the kernel (see Lessons); outputs and messages unchanged |
Lessons#
The end-to-end gap was I/O plumbing. The kernels were at parity since M1a; the 1.3-1.4x end-to-end ratio came from a load-time transposition nobody used, file reads that copied every file several times, a two-pass token parser, a sequential weight-layout conversion and sequential validation. Profile the whole run stage by stage (the compat tool’s split report) before touching an engine.
Scratch memory belongs to the executor, not to the value. Once the workspace moved to
resources, the “first touch counted” bookkeeping, the moved-cost caveat of ADR 0013 and the K-fold memory all disappeared together.The original’s own variance on this machine is large (roadNet-PA end to end 306-418 ms between rounds; road_usa’s
read_graph2.1-3.9 s): only medians of 21 alternating rounds are readable; 5-7-round probes during development gave misleading single ratios.
Open items#
The host
graph::apply()still builds a new CSR per batch (the straight port ofapplyChangeBatch, exempt from I9 until the resident apply), and the OpenMP engine’s per-thread gather lists allocate per round (the straight port ofListGather).The
OMP_WAIT_POLICY=activeA/B of M1a was not re-recorded (the gates are read with the default policy, as in M1a).Next steps of M1b: CUDA core, resident device graph, the fused kernel, the CUDA gates, the
edge_tbenchmark (ADR 0009),ci/gpu_local.sh,cuda-build.yml.
Step 2: CUDA core (cuda-core, 2026-09-27)#
Goal: the CUDA foundation the fused sssp kernel and the resident device graph build on (PLAN
4.6, 4.7.1-4.7.3, 7.1-7.4, 7.8, 8.8): the CUDA build, resources::cuda(), streams, the
CCCL-shaped memory resources, device buffers, errors, device error flags, warm_up(), device
workspaces, the GPU gate and the compile-only CI.
Done#
Build.
DYNG_ENABLE_CUDA(ON when a CUDA compiler is found; the presets pin it),cmake/cuda_architectures.cmake(native,releaseper toolkit, or an explicit list; CUDA >= 12.4 enforced; set beforeenable_language(CUDA)and any target), nvcc using the same host compiler, shared cudart,cuda_std_17,--extended-lambda --expt-relaxed-constexpr,-lineinfoin RelWithDebInfo and withDYNG_CUDA_LINEINFO,DYNG_CUDA_DEBUG_SYNC(Debug)..cufiles enter a module throughCUDA_SOURCES; host files that call the runtime are ordinary sources guarded by#if DYNG_HAS_CUDA, so the CPU builds and clang-tidy see them. Host code keeps-Wall -Wextra -Wpedantic -Werror; nvcc runs with--Werror=all-warningsand-Xcompiler=-Wall,-Wextra,-Werror. Public headers stay host-only (the self-containment test still compiles them with g++ in the CUDA builds).Presets:
dev-cuda(native),release-cuda(release list),parity-cuda(sm_86,-O3 -lineinfo -fmad=true, MOSP-CUDA’s flags),sanitize-cuda,ci-cuda12,ci-cuda13; the CPU presets pinDYNG_ENABLE_CUDA=OFF.Core (ADR 0016).
resources::cuda(device, stream)(device validated, properties recorded once incl. cooperative launch and SM count, a test hook to force the cooperative flag off),scoped_deviceon every enqueuing call,synchronize(),warm_up()through a kernel registry (DYNG_REGISTER_KERNEL);backend_available(cuda)checks for a visible device.cuda_async_memory_resource(owncudaMemPool_t, unlimited release threshold, 256-byte alignment,used_bytes()/reserved_bytes()),default_device_memory_resource(device)(per device, never destroyed),pinned_host_memory_resource; copies between every pair of spaces throughcudaMemcpyAsyncon the handle’s stream and device;DYNG_CUDA_TRY,DYNG_CUDA_TRY_NO_THROW,DYNG_CHECK_KERNEL(stream);device_error_flags(+.cuhraise);fill_asynckernels (7 explicit instantiations);scratch_buffer<T>for device workspaces; CCCL 3.x adapters in both directions (ADR 0003).Tests (
dyng_cuda_tests, labelgpu, 27 tests): resources and device record, invalid devices, user streams, current-device preservation across calls and copies (two GPUs), memory resource rules, warm-up of every registered kernel, pools (stream-ordered reuse without new reservations, OOM recovery), pinned memory, device buffers (round trips, resize, move, pinned staging, kernel writes), every fill instantiation, error macros and their messages, launch failures, the device error word and its exception mapping, the CCCL adapters, device workspaces (reuse, zero allocations in steady state counted through a counting resource, growth, release byrelease_workspaces(),set_memory_resource()and the last handle). The CPU tests adapt to CUDA builds (the default backend becomescuda).Gates.
ci/gpu_local.sh(dev-cuda build,ctest -L gpuon GPU 1,ctest -L cpuof the CUDA build, compute-sanitizer memcheck with leak check, clang-tidy with the CUDA build’s compile database, a Markdown summary, optional PR comment).ci/build_cuda.shand.github/workflows/cuda-build.yml(compile-only innvidia/cuda13.1.1, 13.3.1 and 12.9.2 devel containers; per-kernel registers / stack / shared memory per architecture and the library size in the job summary).
Deviations from the plan (pragmatic choices, same intent)#
Plan |
What was done |
Why |
|---|---|---|
PLAN 7.4: |
|
the CPU gate ( |
PLAN 4.7.3: |
|
the debug synchronization must be per stream; the stream-less form would need |
PLAN 4.2: |
|
they need only the runtime API, so by the naming table ( |
PLAN 4.7.2: built-in resources |
+ |
a default resource must outlive every buffer (process-wide, never destroyed); the memory report and the I9 tests need the pool counters; the CCCL concept requires equality |
PLAN 4.7.1: “host staging is owned by resources” |
the handle carries a staging resource (pinned for CUDA); staging buffers live in workspaces |
pinned allocation is slow and synchronous: buffers must be allocated once, which the workspace pool already guarantees |
PLAN 8.8 |
+ CUDA 13.1.1, the development toolkit |
the only toolkit that is also built and tested on the development machine |
Lessons#
Test the CUDA 12 path locally even without a CUDA 12 machine. A conda toolkit (CUDA 12.9 in an existing environment, used read-only with
CUDAHOSTCXX=/usr/bin/g++) showed that the CCCL 2.x of CUDA 12 lacks the 3.x memory-resource concept; the adapters are now guarded, andci-cuda12builds with-Werrorand its GPU tests pass on the CUDA 12.9 runtime.compute-sanitizer counts failed API calls as errors. Tests that make calls fail on purpose are grouped in the suite
CudaApiErrors, which the gate runs without API-error reporting; every other test must be free of API errors too.CUDA 13 reports an oversized block as
cudaErrorInvalidValue(CUDA 12:cudaErrorInvalidConfiguration); tests should not pin launch-error codes.
Open items#
The profiler’s
device_msfrom CUDA events (PLAN 4.7.5) is not wired yet; the CUDA gate step needs it (orperf_ab.py’s own event timing) for regions under 10 ms.engine::automatic’s use of the recorded cooperative-launch flag, the co-resident block count (per kernel, from the occupancy API) and the fused kernel are the next step; so are the resident device graph, the CUDA A/B, theedge_tbenchmark (ADR 0009) and the sssp page.cuda-build.ymlhas not run on GitHub yet (the repository is not pushed); CUDA 13.3.1 was not built locally.
Step 3: the CUDA sssp engine (sssp-cuda, 2026-09-27)#
Goal: port MOSP-CUDA@e220ee2’s SOSP engine verbatim behind the fused engine of the CUDA backend,
with the resident device graph (the batch still applied on the host), compute / from_arrays /
device results, engine selection, generators::legacy for MOSP’s change generator, and the CUDA
side of the harness (dyng-compat-mosp, compare.py, perf_ab.py) (PLAN 4.5.4, 5.1, 6.3,
6.4.1, 6.4.2; ADR 0017).
Done#
Fused engine (
cpp/src/algorithms/sssp/fused.cuh,cuda.cu):sospPersistentKerneland the host side ofsospUpdateGpu()/sospFromScratchGpu()with mechanical changes only (names, templates on the index types, dyng streams, scratch buffers from the handle’s pool, exceptions). The int32 instantiation compiles to the original’s 59 registers and 616 bytes of parameters (cuobjdump --dump-resource-usageofbin/mospand of the parity-cudalibdyng.so), so the occupancy-derived grid is the same (256 x 256 threads). Two additions outside the search: the deterministicaffectedcounter in the unpack pass (224 more SASS instructions, all in the unpack pass; without it the SASS is byte-identical to the original’s; the pass writes only changed pairs, so it moves no more bytes than the original’s) and a host-side parent-cycle check on the control block.Engine selection before anything is changed:
automatic/fusedneed the recorded cooperative-launch flag, elsenot_supported_errornaming the host backends;operatorsthrows (0.2). Tested with the forced flag (resources_access::force_cooperative_launch).Resident device graph (
cpp/src/graph/device_graph.{hpp,cu}, fromuploadDeviceGraph): a graph built with CUDA resources keeps its host CSR and a device copy per state (out- and in-edges, objective-major columns), uploaded on first use and inside the commit (graph.upload); placement by backend class withclone(res)to move graphs and results.sssp on CUDA: compute, update (the per-objective change lists from the commit’s per-(insertion, objective) byte classification, uploaded in
sssp.changes),from_arrays(host or device arrays, checked on the host, uploaded),cloneacross host and device, device result arrays; stats as in PLAN 5.1 (engine_used = fused).Profiler:
profiler_options::cuda_eventsfillsdevice_msfrom CUDA events.generators::legacy::mosp_changes() (MOSP-OpenMP’s generator, the same code as MOSP-CUDA’s): 15 committed fixture cases made by both originals’
mospPrep changesare reproduced byte for byte for every index type;dyng-compat-mosp changesreproduces all twelve benchmark batches of the four gate graphs (seed 777) byte for byte.Harness:
dyng-compat-mosp --backend cuda(warm-up first, CUDA-event rows in the timing CSV, download before writing),compare.py --configs cuda, CTestparity.sssp.mosp_cuda_e220ee2,perf_ab.py run --backend cuda, the completed[reference.mosp_cuda]map inparity/timed_regions/sssp.toml, and a golden-corpus step inci/gpu_local.sh.Tests:
dyng_sssp_cuda_tests(labelgpu): the shared suitessssp_test.cpp,sssp_random_test.cppandsssp_fixture_test.cppcompiled withDYNG_TEST_CUDA=1(the hand cases and the 34 MOSP fixtures byte-exact on cuda; randomized cross-backend equality cuda = openmp = sequential against Dijkstra andcheck_sssp_tree(require_canonical), for all three index types, canonical and perturbed input trees), plussssp_cuda_test.cpp: the forced no-cooperative-launch path,engine::operators, placement and clones, the device copy per state, device and host inputs offrom_arrays, an overflowing imported tree (poisoned result), the packing boundary n = 2^17 - 1 (pull, push and compute), the 320 x 320 large-weight fallback (weights 2 * 10^9 with ties, and random up to 2^31 - 1), steady-state allocations (an update allocates exactly what the upload of the new graph state allocates), profiler device times, kernel registration.dyng_generators_tests(labelcpu).
Measured#
Golden corpus on cuda (
compare.py --configs cuda, parity-cuda preset): 495 / 495 byte-identical, every group,invalidatedequal.A first CUDA A/B (roadNet-PA, 5 alternating runs, GPU 0, not the gate record): SOSP region per objective 0.98-1.00x of MOSP-CUDA, apply 0.55-0.60x, end to end 0.82-0.84x; outputs byte-identical,
invalidatedequal in every run. The gate record (four graphs, >= 20 runs) is the next step’s.road_usa (5 runs): 50K safe 1.00-1.01x per objective, apply 0.71x, end to end 0.73x; local 10K 1.05-1.09x per objective. That gap is the GPU boost clock (1.69 vs 1.90 GHz during the kernels, nsys GPU metrics; equal kernel times under Nsight Compute’s locked clocks), after the unpack’s
affectedcount was made free (parity/results/M1b.md6.4).
Deviations from the plan (pragmatic choices, same intent)#
Plan |
What was done |
Why |
|---|---|---|
PLAN 5.1: |
|
the workspace’s stamp generation, which the next run must continue from, is known only after the kernel (as in |
PLAN 4.2 file names ( |
|
the device graph is a single port of |
PLAN 4.7.5 stage names |
on cuda one stage, |
Tier B replaces those hooks by one launch; |
PLAN 8.6: regions < 10 ms “timed with CUDA events or nsys kernel sums” |
the CUDA gate compares host times of the same scope (the original’s timer is host time up to its synchronization); dynG’s CUDA-event time is recorded next to it |
the original has no device timer; comparing dynG’s device time with the original’s host time would favour dynG |
PLAN 4.6 rule 5 (placement) |
enforced by backend class: sequential and openmp share host graphs; a CUDA graph keeps a host CSR as well |
the host apply is the straight port (device apply later); the CPU backends have always shared the host storage |
— |
the kernel counts |
|
PLAN 5.8 |
|
MOSP’s generator reads a CSR; a view needs no resources and works for any placement ( |
— |
|
a drop-in check of the generator on the benchmark batches |
Lessons#
A template over the index types costs nothing when the int32 instantiation is the original’s code: 59 registers on both sides. Additions belong outside the hot loops (the
affectedcount lives in the unpack pass only).Report lines are an interface. The harness regex expects
(invalidated N,first in the per-objective line; the first CUDA replay “failed” 495 cases on a reordered line while every file was equal. New fields go at the end.Compile shared suites twice instead of parameterizing across labels. One source per suite, a
DYNG_TEST_CUDAbuild of it in thegpuexecutable, keeps thecpulabel free of GPU work and runs every hand case on cuda. Give the second build’s cases a CTest prefix: CTest merged the same names from two executables and mixed their labels.Measure the clock before blaming the code. With byte-identical SASS the port was 7 % slower on one workload; Nsight Compute (locked clocks) and nsys GPU metrics showed a boost-clock difference caused by what ran on the GPU before the timed region. Host-timed GPU regions of a few tens of ms need the clock state recorded next to them.
An added counter can be free: comparing before writing (write only changed pairs) moves no more bytes than the original’s unconditional write, where “read, then write everything” cost 2 %.
Open items#
The CUDA performance gates (four graphs, three batches, >= 20 runs,
parity/results/M1b.md), theedge_tbenchmark with ADR 0009, and thecuda-build.ymlcheck on GitHub are the next steps. The GPU boost-clock effect (parity/results/M1b.md6.4) makes host-timed per-objective regions depend on the GPU lead-in; the gate protocol must control for it (locked clocks need root; otherwise record the GPC clock during the kernels, e.g. nsys--gpu-metrics-devices, and compare at equal clocks or with an equal lead-in), and must not move work to hide it. If the gate needs device-side times of the original, nsys kernel sums ofsospPersistentKernelare the only source (not wired intoperf_ab.py).A CUDA graph keeps a host copy of its CSR (memory) and uploads each new state; the device apply (PLAN 6.4.1) replaces both later.
compute()on cuda is synchronous (ADR 0017 item 7).
Step 4: parity and performance of the CUDA backend (cuda-parity-perf, 2026-09-27/28)#
Goal: prove M1b parity and performance (PLAN 6.4.2, 8.3, 8.5, 8.6; acceptance criteria 2-4 and
7): byte parity of every backend with both originals on the whole corpus, the CUDA and OpenMP
gates on the four graphs, the fused kernel’s resources, and the edge_t benchmark with ADR 0009.
The step was interrupted once by a usage limit; the second session verified the uncommitted work
of the first, kept it, and finished the step.
Done#
The corpus from MOSP-CUDA’s own tools (
711ad76,2b39fac):export_goldens.py --reference MOSP-CUDA --compare-tore-exports all 495 cases with MOSP-CUDA@e220ee2’s tools (itsbin/maintest cases,mospTest,stressTest/parallelStressTest,mospPrep,bin/mosp --validate,sospUpdateGputhrough the export tool) and requires the committed corpus file for file: identical.compare.pyconfigurations take/int32or/int64.Byte parity on the final code (
0f0fba9): 495 / 495 on sequential, OpenMP 1/4/16/28, sequential and OpenMP with int64 offsets, CUDA and CUDA with int64 offsets; the paper-scale outputs of both A/Bs byte-identical;invalidatedequal in every timed round.Harness (
0ecc062,49943ac):perf_ab.py kernels(both programs under Nsight Compute, clocks locked to base: per-objective kernel time, DRAM bytes, registers, grid, occupancy limits) andperf_ab.py edge-type(int32 against int64 offsets);dyng-compat-mosp --edge-type.The
edge_tdefault (988a113, ADR 0009): int32, withdetail::checked_edge_countin every construction path (capacity_errornaming the int64 instantiation); tested at the boundary.The GPU clock state (ADR 0018, proposed): the as-measured and the controlled-clock readings are recorded side by side; nothing is moved to change the clock state.
OpenMP engine (
9439733,0f0fba9): the per-thread lists live in the workspace on their own cache lines (util/thread_list.hpp); the near-far rounds pass three barriers instead of six (list_gather::gather_pair(),nowaitloops), which fixed the road_usa local-batch reading (section “Measured”) and makes every local batch 0.63-0.89x of the original.Records:
parity/results/M1b.mdsections 7-12 (the certificate), the JSON records next to it, ADR 0009’s table on the final code, the sssp page’s performance section.Tests:
list_gather(gather_pair()equals twogather()calls, in thread order, after anomp for nowait),thread_list(alignment, no shared lines, capacity kept),checked_edge_countat the int32 boundary, the harness smoke tests forkernelsandedge-type.ci/check.shandci/gpu_local.sh(build,ctest -L gpuand-L cpuof the CUDA build, the CUDA golden replay, compute-sanitizer memcheck, clang-tidy) green on the final code.
Measured (every number: parity/results/M1b.md sections 7-12)#
All on port 0f0fba9 (clean), parity presets, exclusive perf lock, GPU 0, 21 alternating
rounds per batch (11 for edge_t), medians:
Byte parity: 495 / 495 in 9 configurations (sequential, OpenMP 1/4/16/28, sequential and OpenMP 4 with int64 offsets, CUDA, CUDA with int64 offsets); 24 paper-scale output checks byte-identical;
invalidatedequal in every timed round.CUDA, as measured: 34 / 36 per-objective readings within the gate (0.98-1.01x); road_usa’s local 10K batch exceeds on objectives 0 and 1 (1.065x, 1.063x; objective 2 1.037x): FAIL under the strict reading. apply 0.55-0.74x, end to end 0.83-0.92x.
CUDA, locked clocks (Nsight Compute): 36 / 36 kernels within 0.992-1.014x (road_usa local: 0.992-0.995x), DRAM bytes 0.99-1.03x, 59 registers and a 256 x 256 grid on both sides. nsys GPU metrics: at the same clock (1.694 GHz) both kernels take 22.5 ms; the original’s usually run at 1.89 GHz after its 470 ms upload stage, dynG’s at 1.69 GHz.
OpenMP: 36 / 36 within the gate (0.63-0.99x; local batches 0.63-0.89x), apply 0.78-1.02x, end to end 0.75-0.91x. Before the barrier change the straight port read 1.12x on road_usa’s local batch (objectives 1 and 2) in two campaigns, 0.96x in step 1’s.
edge_t: int64 costs 3-4.5 % on the CUDA SOSP region of the 50K batches (1.5-2 % on road_usa), -0.6 to +4.4 % on OpenMP; default int32 (ADR 0009).
Deviations from the plan (pragmatic choices, same intent)#
Plan |
What was done |
Why |
|---|---|---|
PLAN 6.4.2 / 8.6: the CUDA per-objective gate as host-time medians |
recorded as measured and at locked clocks (Nsight Compute |
the RTX A5000’s DVFS state during the timed kernels depends on the GPU work that precedes them in a one-shot process, not on the kernel; locking the clocks for whole runs needs root |
MOSP-OpenMP’s |
|
the straight port missed the local-batch gate on road_usa in two campaigns (barrier latency); outputs unchanged; reported separately from the straight port (PLAN 8.6) |
MOSP’s per-region |
per-thread |
no per-round allocations, no shared cache lines; same lists and order |
PLAN 8.3 |
the certificate is |
|
PLAN 8.6: >= 5 runs |
21 alternating rounds per batch (11 for the |
every per-objective region is gated, and the regions under 10 ms need >= 20 |
Lessons#
A 5 % gate on a barrier-bound loop measures the machine as much as the code. The same binary read 0.96x in one campaign and 1.12x in the next; the original has the same two modes. Fixing the cause (six barriers per round where three suffice) removed the mode instead of averaging over it. Instrumenting the loop per thread found the cause in one run after hours of excluding the usual suspects (layout, huge pages, power, false sharing).
Record the controlled reading next to the gate reading, never instead of it. The locked-clock kernel table shows that the CUDA kernel is the original’s (0.99-1.01x everywhere); the as-measured table shows what a user of a one-shot process sees. Both are true, and only the author can decide which one the gate means (ADR 0018).
Re-exporting the corpus with the second original is cheap insurance. “Byte-identical to the goldens” now means both originals’ tools produce the same 11,799 files.
Keep the tree clean during a measurement campaign. The harness stamps every record with
HEADand a dirty flag at write time; editing a tracked file while it ran would have marked the records dirty. Docs and name fixes were written after the last record.Interrupted sessions need a written state. The first session left uncommitted work and JSON records from several commits; the second had to re-derive which record belonged to which code. The final campaign re-measured everything on one clean commit.
Open items#
The CUDA per-objective gate on road_usa’s local 10K batch (objectives 0 and 1, 1.065x and 1.063x as measured) needs the author’s decision (ADR 0018, “Open decision”): (A) one A/B with the GPU clocks locked by root (
nvidia-smi -lgc/-lmcon GPU 0, then reset), (B) accept the proposed verdict rule (different P-states recorded, locked-clock reading within the gate, end to end within the gate: all three hold), or (C) keep it as a known M1b gate miss. Until then acceptance criterion 3a is not met as written. Nothing was moved in dynG to change the clock state.The PLAN 8.3
parity/certify.py/benchmarks/results/<version>/parity.jsontooling is not written;parity/results/M1b.mdand its JSON records carry the same content for M1b.The OpenMP near-far loop could drop to two barriers per round (one gather barrier, the region end) with a lock-free prefix sum; not needed for the gate, left for the operators engine of 0.2.
perf_ab.py’s load-average guard counts kernel threads in uninterruptible sleep (a CIFS mount on the lab machine pushed it to 244 without CPU load); a run-queue sample would be a better contamination monitor (PLAN 8.5).As in step 3: a CUDA graph keeps a host copy of its CSR;
compute()on cuda synchronizes.
Step 5: finish (2026-09-28)#
Goal: close M1b (the step “finish” of the milestone): the sssp page, README, CHANGELOG, Doxygen
clean with @sync / @async on CUDA-capable functions, REUSE and provenance, both local gates
green from a clean tree, and this close-out. The step was interrupted once by a usage limit; the
second session found the tree clean at 0fef14f (nothing of the first session was left
uncommitted) and did the whole step.
Done#
Doxygen synchronization rule (
3f683fb,e330e9e). The public functions that take astream_refor amemory_resource_ref, and the members that do stream-ordered work with their own stream (buffer’s destructor,resize()and move assignment;resources:: release_workspaces()), now carry@syncor@asyncwith a sentence saying what is ordered on which stream: the memory resources’allocate/deallocate, thebufferconstructors,resources::cuda(),set_memory_resource(),release_workspaces().ci/doxygen_coverage.pyenforces it (before, only functions takingresourceswere checked); the rule was checked to fail on four removed tags.Example on the GPU (
10805b4):sssp_update ... cudaruns the update with the fused engine and writes the same file; CTestexample.sssp_update.cuda(labelgpu) compares it with MOSP’s output and is skipped (77) without a device; unknown backends are rejected.sssp page (
73f7f05): the CUDA engine table (automatic/fused/operatorswith and without cooperative launch), the determinism level, a CUDA API snippet (compiled against the headers), absolute times next to the ratios, the gate thresholds of the table corrected (1.10x under 10 ms), and “Paper vs fixed code” (PLAN 6.3 step 10): the correctness fixes between the papers’ code (baseline-2026-09) and the pinned originals, and the per-objective times of the papers’ code, of the fixed code (its own record and the M1b A/B) and of dynG.README and CHANGELOG (
fbb2508): the CUDA build (toolkit, presets, architectures), the local GPU gate and its knobs (DYNG_TEST_GPU, the perf lock,DYNG_GPU_SKIP), the example on cuda, the parity check with MOSP-CUDA, the status table.Found by building every preset (the first full run, on
fbb2508):SsspWorkspace.TheObjectivesShareOneWorkspaceAndASteadyStateAllocatesNone/openmpfailed in therelwithdebinfopreset (workspace 157,584 -> 169,872 bytes after the warm-up; about 1 in 180 runs indevtoo). Since Step 4 the OpenMP per-thread lists live in the workspace, and a list grows when its thread takes a larger share of a round than before, which the dynamic schedule decides.sssp_workspace::thread_list_bytes()now separates them; the test requires the rest of the workspace to stay exactly constant, the lists never to shrink, and the whole workspace to stay constant on the sequential backend (5fbf149; 320 repetitions under parallel load without a failure). The sssp page,thread_list.hpp, the engine header and ADR 0015 state the exception to I9 instead of “allocates nothing” (eff3684,0086ee7).GCC 12 at
-O2warned (-Wstringop-overflow, not an error inrelwithdebinfo) on the sequential engine’s colour reset; it now fills the whole state array (4609c94).
Verification of every preset from a fresh clone of the final code (section “Final verification”).
Deviations from the plan (pragmatic choices, same intent)#
Plan |
What was done |
Why |
|---|---|---|
PLAN 9.1: |
the check defines CUDA-capable mechanically (a |
the XML has no notion of “CUDA-capable”; the list is short and lives next to the rule |
PLAN 8.5 / 9.5: “paper vs fixed code” against the papers’ numbers |
against the papers’ code ( |
neither original re-derived the papers’ run times (MOSP-CUDA |
PLAN 9.6: README quickstarts extracted and run by |
not yet; the CUDA snippet of the sssp page was compiled by hand against the headers |
the extraction step belongs to the docs tooling of M5 |
invariant I9 (no allocation in a steady-state update) |
holds except for the OpenMP per-thread lists, which grow geometrically to the largest share a thread has taken (ADR 0015 update) |
bounding them up front costs a round’s size per thread; MOSP allocates them in every region |
Milestone summary#
M1b was carried out in five implementation steps between 2026-09-27 15:40 and 2026-09-28 04:20
(CDT): the OpenMP gates and the M1a carry-over (Step 1), the CUDA core (Step 2), the CUDA sssp
engine (Step 3), parity and performance (Step 4), and this finish (Step 5); two steps were
interrupted by a usage limit and resumed. 46 commits on main since the M1a close-out
(c54eb6a); nothing was pushed by a step. M2a (m2-cycle) and M4 (m4-infra) ran in parallel on
their own branches and worktrees and are not part of this record.
Area |
Size after M1b (lines, tracked) |
After M1a |
|---|---|---|
Library: public headers and sources ( |
16,000 |
9,900 |
Tests ( |
7,000 |
4,000 |
Parity harness, compat driver, CI, scripts, CMake, examples (without the result records) |
7,300 |
3,800 |
Documentation (Markdown, ADRs, algorithm and API pages) |
4,700 |
1,900 |
The tracked tree is 2.5 MB, of which 720 KB are the JSON records of parity/results/ and 209 KB
the committed test inputs (limit 1 MB).
What M1b delivered, in one list:
CUDA foundation (ADRs 0003, 0016): the CUDA build and presets,
resources::cuda(),stream_ref, the CCCL-shapedmemory_resource_refwithcuda_async_memory_resourceas the default, pinned host memory, devicebuffer, device error flags,warm_up(), device workspaces in the handle’s pool, profiler device times from CUDA events.CUDA sssp (ADR 0017): the resident device graph (uploaded per state, in-edges built on the device), MOSP-CUDA@e220ee2’s persistent cooperative kernel ported verbatim behind the fused engine (the same 59 registers, 616 parameter bytes and 256 x 256 grid), engine selection with the forced no-cooperative-launch path, device results,
from_arrays()/clone()across spaces.OpenMP at the gates (ADR 0015): workspace sharing through the pool, lazy in-edges, parallel checks and assembly, one-pass readers, per-thread lists in the workspace, three barriers per near-far round.
generators::legacy::mosp_changes(), bit-exact with MOSP’smospPrep changes.Harness: CUDA in
dyng-compat-mosp,compare.py,perf_ab.py(run,kernels,edge-type), the corpus re-exported by MOSP-CUDA’s own tools,ci/gpu_local.sh,ci/build_cuda.shandcuda-build.yml.Decisions: ADR 0009 (int32 offsets, checked), ADR 0015 (workspace sharing), ADR 0018 (the GPU clock state in the CUDA gate; open).
Acceptance record#
# |
Criterion |
Evidence |
Status |
|---|---|---|---|
1 |
A fresh clone configures and builds every preset (host |
section “Final verification”: all 13 configure presets from one fresh clone of |
met |
2 |
CUDA byte-identical to MOSP-CUDA e220ee2 on the 495-case corpus plus the M1b cases; CUDA = OpenMP = sequential on the corpus and in randomized differential tests against |
495/495 in 9 configurations incl. cuda and cuda with int64 offsets ( |
met |
3a |
CUDA per-objective SOSP region <= 1.05x (<= 1.10x under 10 ms) of MOSP-CUDA |
34/36 readings within the gate (0.98-1.01x); road_usa local 10K objectives 0 and 1 read 1.065x and 1.063x as measured (objective 2 1.037x); at locked clocks all 36 kernels read 0.992-1.014x (ADR 0018). Re-measured after the review on |
not met as written; the author’s decision is open (ADR 0018: A, B or C) |
3b |
CUDA end to end <= 1.10x |
0.83-0.92x (apply 0.55-0.74x) |
met |
3c |
OpenMP per-objective region and end to end within the same gates against MOSP-OpenMP c352151 (28 threads pinned); the M1a carry-over closed |
36/36 per objective (0.63-0.99x), end to end 0.75-0.91x, apply 0.78-1.02x; workspace sharing (ADR 0015), lazy in-edges in |
met |
4 |
Fused kernel registers and occupancy no worse than the original’s |
|
met |
5 |
|
engine selection before any change, message naming the host backends ( |
met, with the graph upload exempt until the device apply (as the host apply’s I9 exemption of M1a) |
6 |
|
15 committed fixtures from both originals’ |
met |
7 |
|
int64 offsets cost 3-4.5 % on the CUDA 50K batches, -0.6 to +4.4 % on OpenMP: int32 with checked construction (ADR 0009, accepted); ADR 0015 |
met |
8 |
|
|
met (the hosted run waits for the push) |
Final verification (Step 5)#
All from one fresh git clone of 0086ee7 (the code as committed; this retrospective was
added after it and re-checked with the lint steps), on the development machine (GCC 12.2, CUDA
13.1, RTX A5000; GPU tests on GPU 1; every heavy step under the shared perf lock). Logs:
$DYNG_SCRATCH/runs/m1b-finish/final/.
Command (preset) |
Result |
|---|---|
|
all steps passed: clang-format; |
|
build; |
|
build without warnings; |
|
|
|
|
|
build without warnings; |
|
build; |
|
|
|
compile-only release build, |
|
compile-only release build, |
The first full run (on fbb2508) failed in relwithdebinfo on the flaky steady-state test and
showed the -Wstringop-overflow warning; both are fixed (Step 5, “Found by building every
preset”), and the run above is the rerun on the fixed code.
Deviations, consolidated#
Every deviation is in the table of the step that made it; the ones that matter beyond M1b:
Scratch memory belongs to
resources(ADR 0015): a workspace pool per handle, leased by runs, instead of workspaces inside results (PLAN 4.7.2, 5.1);release_workspaces(),workspace_bytes().Graph construction: in-edges built on first use;
from_csr(res, csr&&, props); theapplyregion gated at 1.10x in addition to PLAN 8.6’s regions.Presets:
devstays CPU-only; the CUDA builds are separate presets (dev-cuda,release-cuda,parity-cuda,sanitize-cuda,ci-cuda12/13) (ADR 0016).CUDA placement: a graph belongs to its backend class; a CUDA graph keeps a host CSR and a device copy per state;
compute()on cuda synchronizes once (ADR 0017).Stage names on cuda: one
sssp.enact_fusedstage replaces the four hooks (Tier B).The CUDA gate: host times of the same scope on both sides (the original has no device timer), recorded as measured and at locked clocks, never merged (ADR 0018, open).
OpenMP engine beyond the straight port: three barriers per round (
gather_pair(),nowait) and per-thread lists in the workspace; the straight port’s records kept separately.Additions to the ported kernel: the deterministic
affectedcount in the unpack pass and a host-side parent-cycle check;stats::packed_parentsmay differ between cuda and the host engines right at the packing limit.Records instead of tools:
parity/results/M1b.mdwith JSON records instead ofcertify.py/benchmarks/results/<version>/parity.json; 21 alternating rounds instead of 5.Documentation checks:
@sync/@asyncenforced on a mechanical definition of “CUDA-capable”; “paper vs fixed code” against the papers’ code rather than the papers’ numbers.Invariant I9 exceptions: the OpenMP per-thread lists (schedule-dependent growth, ADR 0015) and, on CUDA, the per-batch upload of the new graph state (until the device apply).
Lessons#
Machine time for performance gates is the scarce resource. The final campaign (4 graphs x 3 batches x 21 rounds for both backends, the locked-clock kernels and the
edge_tbenchmark) held the exclusive lock for about 2 h 40 min; every gate iteration before it cost 30-60 min. Parallel milestones (M2, M4) share that lock: schedule GPU campaigns, do not assume them.A gate is only as good as its control of the machine. Two causes of false readings were the machine, not the code: DVFS performance states on the GPU (ADR 0018) and barrier-latency modes on the CPU (Step 4). Record the machine state (clocks, P-state, run queue) next to every timing, and decide in advance, in an ADR, what the gate compares when the state differs.
Straight ports first, measured changes after, both kept on record. Every change beyond the straight port (workspace sharing, parallel assembly, three-barrier rounds) came with a measurement and left byte parity intact; the straight port’s numbers stay in the certificate.
Byte parity scales when the originals are cross-checked first. Both originals’ tools produce the same 11,799 files; every CUDA mismatch during the port was dynG’s by construction, and the corpus replays in under a minute per configuration.
Mechanical checks beat review for conventions. Extending the
@sync/@asyncrule fromresourcesparameters to streams and memory resources found twelve public functions without the tag, all written in Steps 2-3 and unnoticed since.Build every preset from a fresh clone before closing a milestone.
ci/check.shandci/gpu_local.shcover four presets; the other nine found a schedule-dependent test (a 1-in-180 flake that the gates had never hit) and an optimizer-only warning.Interrupted sessions need a clean tree or a written state. Step 4’s first session left uncommitted work and mixed records; Step 5’s interruption left nothing, because every piece was committed as soon as it was checked.
Re-estimate of the roadmap#
Measured: the M1a retrospective re-estimated M1b at 4-6 days of focused work and 1.5-2 weeks of calendar time (plan: 2 weeks). M1b took five sessions over about 13 hours of wall-clock time; the final measurement campaign alone held the exclusive lock for about 2 h 40 min, and the earlier A/B runs of Steps 1, 3 and 4 added to that. The independent review and its fix step (M1a’s found 25 defects) are still to come. M2a (the CPU half of M2) and M4 were completed in parallel on their own branches in the same period.
What this changes. The coding of a port with a good original is faster than the M1a re-estimate assumed; the remaining time is dominated by (i) measurement campaigns that need the exclusive lock of one shared machine, (ii) reviews and their fix steps, (iii) the author’s decisions and account actions, and (iv) merges of parallel branches. The re-estimate therefore lowers the focused effort and keeps most of the calendar time.
Milestone |
Plan (working weeks) |
M1a re-estimate (focused) |
Now: focused AI effort |
Calendar incl. author gates |
Main risk |
|---|---|---|---|---|---|
M1b CUDA |
2 |
4-6 days |
done (about 13 h), plus a review-and-fix step of 1-2 days |
ADR 0018 decision; review |
the open gate reading; review findings |
M2 |
2-3 |
4-6 days |
M2a done; M2b (CUDA) 3-5 days after the M1b merge |
1-1.5 weeks |
two kernel families, device batch apply, GPU campaigns competing for the lock |
M3 framework + conformance kit + 0.1 API freeze |
2-3 |
5-8 days |
4-7 days |
2-3 weeks |
re-running parity and both GPU/CPU gates per refactor commit (lock time); the author’s API sign-off |
M4 GitHub repository and infrastructure |
1-2 |
1-3 days |
done on |
author settings actions |
first hosted runs of the CUDA compile job |
M5 Python CPU wheel, CLI, docs |
2-3 |
5-8 days |
4-7 days |
2-3 weeks |
nanobind + scikit-build-core, Sphinx |
0.1.0 |
12-17 (from M0) |
4-6 weeks |
about 3-4 weeks from now |
about 6-8 weeks |
merges of three branches; reviews |
0.1.x (M6) |
4-6 |
1.5-2 weeks |
1-2 weeks |
3-4 weeks |
GPU runner decision (O11); wheel sizes |
0.2.0 (M7-M9) |
8-12 |
3-4 weeks |
2.5-3.5 weeks |
6-8 weeks |
the operators engine must stay within 1.05x of the fused one (a long GPU campaign per refactor); the CBST merge |
0.3.0 (M10-M11) |
8-12 |
3-4 weeks |
2.5-3.5 weeks |
6-8 weeks |
DynLP float tolerances; conda-forge review |
Up to 0.3.0 |
about 32-47 |
12-16 weeks |
about 9-13 weeks |
about 20-27 weeks |
Recommended order now: the author decides ADR 0018 (option A needs one sudo command pair and
one 30-minute A/B on GPU 0); an independent review of M1b; merge M1b, then M2a and M4 into main
(M2b needs M1b’s CUDA core); M2b; then M3. Measurement campaigns of parallel milestones should be
scheduled one after the other on the exclusive lock.
Open items carried forward#
For the author:
ADR 0018 (acceptance criterion 3a): (A) one A/B with root-locked clocks on GPU 0 (
nvidia-smi -lgc/-lmc, then reset), (B) accept the proposed verdict rule (its three conditions hold for road_usa’s local batch), or (C) keep the reading as a known M1b gate miss.Review M1b; ADRs 0015-0017 are proposed and are accepted with the 0.1 API freeze (M3).
For M1b’s review-and-fix step / M2b / M3:
A CUDA graph keeps a host copy of its CSR and uploads each new state (allocating from the pool); the device apply (PLAN 6.4.1) removes both.
compute()on cuda synchronizes (ADR 0017 item 7).perf_ab.py’s load-average guard counts kernel threads in uninterruptible sleep (CIFS); a run-queue sample would be a better contamination monitor (PLAN 8.5).The OpenMP near-far round could drop to two barriers with a lock-free prefix sum (not needed for the gate; the operators engine of 0.2 is the place).
parity/certify.pyandbenchmarks/results/<version>/parity.json(PLAN 8.3) are release tooling for 0.1.The README quickstart extraction (
ci/docs.sh, PLAN 9.6) comes with the docs tooling of M5.
For M4 / the first push:
cuda-build.ymlhas not run on GitHub yet (the CUDA 13.3.1 container was never built locally; 13.1 and 12.9 were).
Review and fix step (M1b review, 2026-09-28)#
An independent review of M1b (at 6ab4f25) found 19 problems, each confirmed by a second
reviewer. All 19 are fixed in this step, in small commits 043fcae..674fc3b (plus the records
and this section), each checked by the tests it touches; nothing was deferred. The step was
interrupted once by a usage limit before any change was made; the resumed session found the tree
clean at 6ab4f25 and did the whole step.
Findings and fixes#
# |
Finding (severity) |
Fix |
Commit |
Checked by |
|---|---|---|---|---|
1 |
The fused kernel’s |
per-thread counts summed per warp before the barrier ( |
|
the CUDA sssp suite under |
2 |
|
|
|
|
3 |
|
|
|
Doxygen check |
4 |
No contamination monitor or per-round GPU clock record in the gate records (medium) |
|
|
harness tests (a spinning process is detected); the re-measured gate records below |
5 |
The CUDA |
added to |
|
harness test; re-measured records |
6 |
Default-stream handles are per-thread streams, so |
workspace fences ( |
|
|
7 |
|
|
|
|
8 |
|
staging (7) plus |
|
each batch array in device memory in turn, on cuda and the host backends |
9 |
|
|
|
|
10 |
|
|
|
|
11 |
|
|
|
|
12 |
Doxygen gaps: |
tags added; |
|
the check fails on a removed tag (tried) and passes |
13 |
|
the matrix builds 13.4.1 |
|
|
14 |
The local equivalent never used the containers’ GCC 13; GCC 14 failed with |
the test’s per-insertion vector (a GCC 13/14 |
|
|
15 |
|
|
|
configure with |
16 |
|
ccache installed, launchers set, cache per toolkit; container paths for the caches |
|
YAML check (the hosted run waits for the push) |
17 |
The |
exit code 77 mapped in |
|
|
18 |
clang-tidy and all of |
|
|
both gates run under the lock |
19 |
|
the option (CUDA::nvtx3, build interface only) and ranges per profiler stage |
|
|
Verification of the final code (674fc3b)#
All on the development machine (GCC 12.2, CUDA 13.1, RTX A5000; GPU tests on GPU 1; every
heavy step under the shared perf lock, the timing under the exclusive one). Logs:
$DYNG_SCRATCH/runs/m1b-review/.
Command (preset, code) |
Result |
|---|---|
|
all steps passed: clang-format, |
|
build; |
golden replay records ( |
495/495 in 9 configurations ( |
|
|
|
compile-only release builds with |
fresh clone of |
each builds without a warning; |
performance campaign ( |
below |
Performance, re-measured with the contamination monitor#
parity/results/M1b.md section 13 has every number; medians of 21 alternating rounds per
batch, parity presets, GPU 0, with the contamination monitor (one round rejected in the whole
campaign, none of the CUDA ones; foreign load of accepted rounds at most 1.46 cores; no foreign
GPU process).
Reading |
Result |
|---|---|
CUDA per-objective SOSP region, as measured |
33 / 36 within the gate (0.979-1.010x); road_usa local 10K 1.060 / 1.060 / 1.054x: still a FAIL as written (step 4: 1.065 / 1.063 / 1.037x) |
CUDA fused kernels at locked clocks (road_usa) |
0.989-0.991x (local 10K), 1.009-1.011x (50K); 59 registers, grid 256 x 256 on both sides |
CUDA apply / end to end |
0.66-0.96x / 0.80-0.90x (apply now includes |
OpenMP per-objective / apply / end to end |
36 / 36 (0.63-0.99x) / 0.79-1.03x / 0.77-0.90x |
|
equal in every timed round, byte-identical outputs |
The monitor settles what the road_usa miss is not: it is not contamination. It does not settle ADR 0018’s condition (a): sampled every 50 ms over each process, both sides show the same clock states, and the 65 ms of kernels per round cannot be separated from the surrounding work. ADR 0018 now says so; the author’s decision stays open, and option A (locked clocks for the whole A/B) is the one that would measure the gate as written.
Deviations from the plan (pragmatic choices, same intent)#
Plan |
What was done |
Why |
|---|---|---|
MOSP-CUDA’s kernel ported verbatim (PLAN 6.3 step 5) |
|
the original races on that read; the public contract calls the counter deterministic and the parity checks compare it exactly; trees and SASS of the search unchanged |
PLAN 4.7.4: a copy of |
true for explicit streams; the default |
the per-thread stream has no handle that another thread could use; pinning handles to a thread would forbid the concurrent read-only calls of 4.7.4 |
PLAN 4.7.1: |
+ |
the host-side work of a CUDA call runs on OpenMP threads, whose count belongs to the handle (PLAN 4.6 rule 6, 4.7.4) |
PLAN 4.7.1: “if [an input’s space] does not match the backend, the library copies once” |
the match is against where the input is read: the host, for every backend in this release; host inputs of CUDA calls are not implicit copies (ADR 0016 item 11) |
the batch is applied, graphs are built and trees are checked on the host (the straight ports); calling host arrays “misplaced” would make the fastest path warn |
PLAN 5.2: |
both, identical in this release |
a CUDA graph keeps its authoritative CSR on the host until the device apply |
PLAN 8.5 item 2: “the contamination monitor” |
foreign CPU load from |
the originals’ |
Lessons#
A verbatim port inherits the original’s races. The golden corpus could not show the
invalidatedrace because the original’s plain runs never expose it; a sanitizer’s scheduling did. Run the ported kernels undersynccheck/racecheckonce, and keep the one that finds nothing false in the gate.“Per-thread default stream” is a different stream per thread. Every stream-ordered free and every “same stream” assumption has to name the thread; the library’s own reuse needs events, and the documentation has to say what
synchronize()really waits for.Documented options must do something.
copy_policyandprofiler_options::nvtxwere stored and documented but read nowhere; a grep for the readers of every public setting is a cheap review step.Test the CI’s host compiler, not the development machine’s. GCC 13/14 disagreed with GCC 12 on one test; conda-forge compilers (read-only environments in
$DYNG_SCRATCH/tools) make the container toolchains checkable locally.Record the machine next to every timing. The per-round monitor turns ADR 0018’s rule (a) from a claim backed by three profiled runs into a property of the gated samples.
Open items after the review#
ADR 0018 (acceptance criterion 3a) is still the author’s decision; the monitored re-measurement shows the miss is not contamination and that the gated samples cannot establish rule 3’s condition (a), which leaves option A (locked clocks for the whole A/B, a root action) as the way to read the gate as written.
cuda-build.yml(now CUDA 13.4.1, 13.1.1 and 12.9.2, with ccache) runs on GitHub only after the push; its three toolkits were built locally with the containers’ host compiler generation (GCC 13) from conda-forge environments in$DYNG_SCRATCH/tools(13.1 with the system GCC 12).The implicit copies of device inputs are host staging copies until the device apply (PLAN 6.4.1) makes device batches the matching space (ADR 0016 item 11).
The monitor’s GPU samples cover whole processes; clock readings inside the timed kernels would need profiler metrics on every gated round (ADR 0018 update).
The records of this step add about 840 KB of JSON to
parity/results/(the per-round monitor data); the tracked tree is about 3.4 MB. A compact record format (or moving the per-round data out of the repository) is worth deciding before 0.1.
Acceptance fix step (M1b acceptance, 2026-09-28)#
The independent acceptance verifier (at a304d90) passed criteria 1, 2 and 4-8 and failed 3(a):
its own default-clock A/B read road_usa’s local 10K batch at 1.061 / 1.059x on objectives 0 and 1
(gate 1.05x), as the committed records did, and PLAN 8.6 has no clock-state exception. It named
option A of ADR 0018 (locked clocks for the whole A/B) as the way forward, which was believed to
need root. The step was interrupted once by a usage limit before any change was made; the resumed
session found the tree clean at a304d90.
Done#
Item |
Commit |
|---|---|
Locked clocks without root: Nsight Compute holds a clock lock for every process on the GPU while the process it profiles lives; |
|
|
|
The CUDA gate at locked clocks on all four graphs, plus road_usa at base clocks and a fresh default-clock reading of road_usa’s local batch |
|
ADR 0018 update, |
this commit and |
Measured (parity/results/M1b.md section 14)#
Reading |
Result |
|---|---|
CUDA per-objective SOSP region, clocks locked (boost: SM 1695 / memory 7601 MHz), 4 graphs x 3 batches, 21 rounds |
36 / 36 within the gate, 0.977-1.028x; road_usa local 10K 0.993 / 0.994 / 0.995x; unimodal per-round times |
CUDA apply / end to end, locked |
0.67-0.87x / 0.76-0.90x |
road_usa at base clocks (SM 1170 MHz) |
0.993-1.002x per objective |
road_usa local 10K at default clocks (ungated) |
1.063 / 1.060 / 1.062x, bimodal as before; the original’s kernels reached P0 more often (118 of 480 busy samples against 68 of 525) |
Outputs / |
byte-identical, equal in every round |
|
all checks passed (harness 20 tests) |
fresh clone of |
all checks passed ( |
|
release architecture list (sm_75..sm_120), |
The OpenMP gate and the other criteria were not re-measured: no library or kernel code changed in this step (the changes are the harness, the NVTX detection of the build and documentation).
Deviations from the plan (pragmatic choices, same intent)#
Plan |
What was done |
Why |
|---|---|---|
PLAN 8.6: the CUDA per-objective gate, protocol without a clock rule |
the gate is read with the GPU clocks locked for the whole A/B (ADR 0018 rule 4, |
the default-clock reading measures the GPU’s DVFS response to the GPU work each program does before its kernels (the original’s upload stage vs dynG’s earlier tree uploads), not the code; locked clocks are the standard control, the plan’s gate threshold is unchanged, and neither program is altered or profiled |
ADR 0018 option A: |
the same lock through Nsight Compute’s clock control, no root |
root is not available; the lock is verified in every sample |
Lessons#
Look for an unprivileged path before calling a step blocked on root. The profiler’s clock control, already used by
perf_ab.py kernels, was the tool; what was missing was the observation that its lock applies to the whole GPU while the profiled process lives.A killed profiler session leaves the clocks locked, for everyone on that GPU, until
ncu --clock-control reset. The harness resets unconditionally and unwinds on SIGTERM / SIGHUP.Never walk
/on this machine. Afind /issued to locate a header descended into the automounted/dfsnetwork share and mounted thousands of CIFS sub-shares; the desktop daemons of every logged-in user then loaded the CPUs (load average above 50) for about half an hour. Search the toolkit directories by name instead.
Open items#
ADR 0018: the author may still decide that the default-clock reading should be gated too (today it is recorded and ungated); ADRs 0015-0018 are accepted with the 0.1 API freeze.
The records of this step add about 720 KB of JSON (per-round monitor data); the compact record format of the review’s open items is still to be decided.
ci/perf_gate.sh(PLAN 8.6) should pass--lock-clocks boostfor the CUDA gates (it is the default ofperf_ab.py run).