Repository navigation
Vulkan: one submission per batch of dispatches, 30% to 68% less run time on an M1 with the same logits (stacked on #2015) - #2016
Conversation
|
Thanks @joshuaswarren. An independent review on current main (d8b80bd): What the 45 commits areCommits 1-41 are #2015's series, patch-identical (checked with
The commits apply as described under our earlier review. Lean is 465 at the head, with none added. The author is @joshuaswarren's The change
Change 1The posted contract
Small points (not blocking)
What the device runs need
|
|
Every Vulkan contract runner and stage probe now has a GPU run at #2015's head and at this head, and the lines are equal. Setup. 13-inch M1 (Apple M1, G13G B1) on the Honeykrisp Vulkan driver, Linux 7.1.12-2. No model. Ten aarch64 ReleaseSafe runners built per head with
Compared with the elapsed and gate milliseconds removed, 10 of 10 runners print the same lines at both heads. What this does not cover.
On the two small points: I will add the [Joshua agent] |
|
The batched build also has a run on a 16-inch M1 Max (Apple M1 Max, G13C C0), the second Apple GPU, on the Honeykrisp driver, Linux 7.1.12-2. No model. Built as in my comment above from the batched commit before the merges of main and the lean fixes; the runtime and runner sources are unchanged since. Both exit 0. Contract runner sha256 The stage 1 probe reports [Joshua agent] |
…rted preservation
… -Dcontract-root option The contract runner built only the Q4 projection's shaders. Each later operator registers in the same runner, so it now imports the stage 1 to 5 shader modules (norm, rope, attention, GDN, greedy and the data-movement kernels) that the operator probes already build, and the stage modules are hoisted out of their probe blocks so both can share them. -Dcontract-root points the runner at another root file, so one operator group can be built and run on its own.
…nel contract The stage 4 GatedDeltaNet images (conv_dw1d, gdn_conv_decode, gdn_split, gated_delta_prefill_ash and gated_delta_decode in its ASH_QWEN35 raw-gates mode) register as eight kernels through the production Gdn wrapper, so the wrapper's binding tables — the read-only g slot and the single-chunk snapshot dummy among them — are the ones under test. f32 state flows in and out of place, the interior chunk snapshot is bound exactly when chunks exceed one, and the decode loop chains the carried state. References are the stage 4 fixture's f64 walkers made parametric in tokens; every depth a spec states is a function of the token count (one ~130-deep state update per token, the fixture's stated derivation). The f32 state parts prove fp32 sums with an engineered stress fill whose kv partials overflow fp16; the bf16 parts declare bf16=0 with the reason the output's own rounding hides the sum error, as HIP's mamba and CUDA's gdn_prep do. The host suite cross-checks the walker against the fixture's bit for bit and proves the generated fills against the references without a GPU. contract_gdn_main.zig runs the group alone via -Dcontract-root and prints the same LIMITS lines, records audit and runAll tally as contract_main, plus live local-size and shared-memory evidence for all seven pipelines. Proven: zig build vulkan-host-test (20 gdn contract tests, walker parity with fx.RecurWalk/fx.Decode, stress fp16 margins >= 100x); both contract runners build; the runner refuses the software Vulkan device fail-closed. GPU gates 1-4 pending on the M1/M2 devices (none reachable from this host: llvmpipe only).
…e run Three fixture bugs the M1 Max's first run surfaced, none of them shader bugs: the split parts uploaded one combined output buffer where the wrapper binds soa, g and beta separately (OutOfRange on every gate); the decode conv's unchanged-check listed the host chain scratch as an input while indexing one device buffer fewer, so input_now's tail was never filled (a false 'a run changed its inputs'); and the prefill recurrence's random fill used the stage 4 fixture's ranges, which only ever ran at 8 tokens - at 130 the walk outgrew f32. The random fill now decays (g, beta and the whole soa at 2^-3 scale or less), so any token count stays bounded. Also the sums replay the gates ran: every dot re-summed in the requested accumulator and order, the conv chains replayed from the uploaded initial state, exactly as computeWant walks them. Proven: vulkan-host-test green; runAll on Apple M1 Max (G13C C0) Honeykrisp - prefill conv, decode conv and decode state pass gates 1-4 fully; prefill y, hf+snap and split classes settle with the fill fix pending the next device run.
…the soa sums proof The M1 Max run showed the split g shapes gate failing where a random a_log above ~2.4 drove g into f32 subnormals the device flushes - the exact case the stage 4 fixture's census deferred to Main. The served random fill now draws a_log in [-6, 0] (g >= e^-4.5, normal), with the census case named in the fill; the subnormal-flush decision stays with Main. The split soa sums gate moves to f16=0 with the measured reason: the fixture budget's magnitude term at the output's scale is wider than any fp16 miss the stress can engineer, and gate 3's shapes already bound the norm's sum to f32 quality (a bf16-summed ss misses ~2^-8 relative against an observed worst of 0.037 of bound). Proven: vulkan-host-test green.
…tes 1-4 cast_f32_bf16, cast_bf16_f32, copy_u32, copy_u16, gather_u32, gather_u16 and dequant_bf16 register with the kernel contract, bit for bit (a NaN keeps its class; a non-finite dequant scale or bias is checked by class; every gather run includes an out-of-range id whose stated rule is the documented zero row). tf-stage1-datamove runs the group on a real GPU (zig build stage1-datamove) and refuses CPU devices; the stage 1 imports hoist so the probe and this runner share them. Ported from w7p/native-vulkan's a3f040c7 onto the engine-pr2 tree: the files needed no source change; the wiring reuses this tree's hoisted stage1_shaders module. Proven: zig build -Dvulkan=true stage1-gpu-build and vulkan-host-test pass on this box (llvmpipe only); tf-stage1-datamove --help exits 0 and the runner fails closed (exit 1) with only the software device present.
rope_bf16 and sdpa_rows_bf16 against stage3_fixture's f64 references generalized past the fixture's 8-key score buffer to the served shapes (decode rows 1, prefill rows <= 32, visible keys 1..7168 on the 16-key tile, 256-lane scan and 1024-thread stride edges and the kStreamKeys bound). The sdpa bound is derived from the shader text -- denominator depth ceil(n/256)+8, the 256-lane strided chains plus the 8-level tree, NOT the plain n-term sum -- and pinned to the source by a host test. Single-key rows carry the V bits exactly; multi-row prefill rows decode alone bit-identical. Ported from w7p/native-vulkan-model's 60a22e77 onto the engine-pr2 tree: the files needed no source change; the wiring reuses this tree's hoisted stage1/stage3 shader modules. Proven: zig build -Dvulkan=true stage3-gpu-build, stage3-host-test and vulkan-host-test pass on this box; tf-attention-contract --help exits 0 and honestly refuses this box's software llvmpipe (exit 1). Real-GPU run needs the Apple Vulkan rig.
The greedy draw — finite_logits, argreduce_chunk and argreduce_combine behind one submission, three launches inside the run hook — registers through gates 1-4, plus the planted control contract_controls_greedy.comp (the mirror of HIP's tf_ctl_fmax_max: a row max that drops a NaN), registered with control = .nonfinite and required to go red. The acceptance is exact: the token equals the host greedyRef (max wins, ties to the lowest index, +0/-0 equal), a planted non-finite row refuses naming its first offender, repeats are byte-identical, inputs and guards untouched. Ported from the greedy agent's files on w7p/native-vulkan-model onto the engine-pr2 tree: the files needed no source change; the wiring compiles the control beside the stage 5 variants and reuses this tree's hoisted stage5_shaders module. Proven: zig build -Dvulkan=true stage5-gpu-build, stage5-host-test and vulkan-host-test pass on this box; tf-vulkan-greedy-contract --help exits 0 and the runner fails closed (exit 1) with only the software llvmpipe present.
…ion shapes contract_pointwise.zig states and checks the six pointwise closures of stages 2-4 on the host: the stage 2 trio reuses stage2_fixture, the f32-preserving residual add claims the exact f32 sum bit for bit (a flush-to-zero device fails HONESTLY), the attention output gate uses boundExact with ops 4 mids 1 and demands signed-exact zeros, and the gated RMSNorm uses the rows family bound plus ops 10 mids 12 with nan/inf/±0/zero-row/nan-row controls class-checked. Every checker also refuses a corrupted output, a sign-flipped zero and a planted nonfinite. contract_pointwise_main.zig drives all six on a real GPU through the wrappers' exact dispatch shapes (NormMlp for stage 2; the preserve add, gate and gated norm at attention.gatePlan's and gdn.gatedNormPlan's geometry), three byte-identical repeats per case. Ported from w7p/native-vulkan-stage2 (c0ee7bf + eb72415) onto the engine-pr2 tree. One source adaptation: this tree's NormMlp.Shaders gained the add_preserve_bf16 field, so the runner now serves it (the image this tree's stage 2 closure already compiles); the shader sources themselves landed with this tree's stage 3/4 closures. Proven: zig build -Dvulkan=true contract-pointwise-build, vulkan-host-test pass on this box, and zig-out/stage{2,3,4} carries the six images; tf-contract-pointwise --help exits 0 and the runner fails closed (exit 1) with only the software llvmpipe present.
… cross target The datamove test module was built for -Dtarget, so a cross build (aarch64 on the x86 build host) failed with 'unable to spawn foreign binary' in vulkan-host-test. It now gets its own host-target module like every other host suite. Proved by: vulkan-host-test and all stage and runner build steps pass with -Dtarget=aarch64-linux-gnu (224/224 steps).
… and u16 copy On a real device (Apple M2 Max, Honeykrisp) the shapes gate failed three kernels with the Zig error Misaligned: the gate's edge shapes include odd element counts, and a 16-bit range of an odd count is not bindable (dispatch.bind needs whole 32-bit words; View.init refuses the same ragged tail), so the fixture's view call raised an error that the gate scored as FAIL. The kernel refuses that edge, so prepare now returns false for it (an edge refused by the kernel, as the gates count them); served shapes (146x1, 3x700, 300x1, 2x520) are all even. Proved by: vulkan-host-test and stage1-gpu-build pass for aarch64 (81/81 steps, 210/212 tests, 2 skipped). Not proved: no device rerun yet.
The stage 4 fixture (1035 lines) and probe (950 lines) each broke the 600-line lean rule, and the fixture's two-line header was a multi-line comment. The fixture is now four files (the shared core, the split fixtures, the state fixtures, and the root that keeps the public surface and the host tests) and the probe three (the shared helpers, the gates, and the root), all under 580 lines. Each file reaches the others through @import inside the same module, so zig/build/vulkan.zig is unchanged: the stage 4 host test still runs stage4_fixture.zig as its root, and the probe executable still roots at stage4_probe.zig. Every line of code is the original line, moved. The only new tokens are the import aliases, the pub on the helpers the parts share (violated, poisonedShell, f32Of, the gate and buffer helpers), and the one-line file headers. The fixture root re-exports the whole public surface (fx.Split, fx.SplitOne, fx.Conv, fx.RecurWalk, fx.Recur, fx.Decode, fx.Norm, fx.Model, fx.c, fx.budget, fx.gateRefAsh, fx.sampleDyadic, fx.tokens), so the probe and contract_gdn.zig are untouched; the probe root calls the gates through the gates module. Proven: tools/zig/lean_check.py reports no problem for any stage4_*.zig (the repo total falls from 466 to 463). `zig build stage4-host-test -Dvulkan=true` passes with the same 60 + 9 tests, `zig build stage4-gpu-build -Dvulkan=true` compiles tf-stage4-gpu, and `zig build test -Dvulkan=true` gives the identical summary before and after (543/549 steps, 2273/2313 tests; the 37 failures are the pre-existing originals_test ones, the same at the base commit). zig fmt --check is clean.
contract_gdn.zig was 1611 lines, over the 600-line lean limit, so no case could be added without splitting it. The served shapes, the specs and the f64 references move unchanged into contract_gdn_ref.zig, the fixture's buffers and launches into contract_gdn_ops.zig, and the host fills, references and upload order into contract_gdn_host.zig; the root re-exports them, so every caller keeps the same API. The tests stay in the root file, so every artifact that imports the fixture still runs them and the test counts do not move.
…activation loads The new image computes the same function as qmm_bf16 with the same arithmetic and order: one fp32 accumulator per output, k ascending, one lane at a time. Only the load width changes: a uvec4 of w is 32 codes and a uvec4 of x is eight bf16. ops_q4 picks it when the x and w windows are 16-byte aligned and keeps the scalar image otherwise; projectAs forces either one. stage1_probe now runs both images on every projection case and requires byte-identical output. The cases include the served shapes (k 2048 and 6144, m up to 32) and one wide enough to take the chunked path. Proved by: stage1 host tests 162 of 164 pass (2 skipped); stage1 probe 17 of 17 on a software Vulkan device. Not measured on a GPU yet.
…tes as qmm_bf16 The new image gets a launch record and a second fixture in the Vulkan contract runner. It runs the same served shapes against the same float64 reference and bound, with the same guards and input checks, as qmm_bf16. The fixture takes a name so the two kernels print as two kernels. Proved by: stage1 and the other Vulkan host suites pass (148 of 148 build steps, 155 of 157 tests, 2 skipped). The contract's SUMMARY lines on the two Apple GPUs are in the receipts commit.
…elog line The receipts folder holds the identity probe, the pre-registration, a summary generated from the raw run files, and the SHA-256 of each raw file. On Qwen3.5-2B 4-bit, a run took 40% to 52% less time on three inputs, with the same logits digest in all six processes of each case. The contract runner's output on the GPUs follows in a later commit.
tf-vulkan-kernel-contract on the Apple M1 (G13G B1) under Honeykrisp: 2 kernels pass gates 1 to 4, 0 fail, 0 known findings, 8 of 8 planted controls caught, 0 limit failures. qmm_q4_vec4 and qmm_bf16 show the same worst shapes value (0.990 of the bound).
…to 32-column tiles A decode step reads each weight row k/2 bytes apart, so every 16-byte load of a 32-lane group touches 32 cache lines. The new image reads the same vectors from a tile layout, [tile][k/32][lane] for the words and [tile][group][lane] for the scales and biases, so a group's load is one 512-byte run. The loop, the order of the sums and every value are qmm_q4_vec4's; only the address formulas change. vk.tile repacks on the host (pure moves of 16-byte vectors and 2-byte scales). The Qwen3.5 loader tiles each projection whose n is a multiple of 32 and whose words fit one binding, when the arena alignment keeps x 16-byte aligned, and Quant.tiled sends it to ops_q4.projectTiled. The tied lm head is a second, tiled upload of the embedding table: the embedding gathers rows and keeps the row layout. Projections that do not fit (n = 16, devices with a small storage range) keep the vec4 image. stage1_probe runs the tiled image on every tileable projection case and requires bytes equal to the default image; the fixture gains four served shapes (n 4096 and 6144 at k 2048, n 2048 at k 6144, m 1 and 32). Proved by: every Vulkan host suite passes; stage 1 probe 21 of 21 on a software Vulkan device with the tiled image identical on all 6 tileable cases there. Not measured on a GPU yet.
…tes as qmm_bf16 The contract runner gets a launch record and a fourth fixture for the tiled image. Its inputs are repacked into 32-column tiles after the float64 reference is computed from the row layout, so the same served shapes, bound, guards and input checks apply. Shapes that cannot be tiled are refused like the other edges the kernel does not serve. The fixture's kernels table is now 10 entries. Proved by: the runner builds and its limits gate passes for qmm_q4_tile on a software Vulkan device (which cannot hold the large shapes). The SUMMARY lines on the Apple GPUs are in the receipts commit.
…with zero columns The kernel contract refused qmm_q4_tile on 1x16x64: a registered kernel must serve every shape the contract serves, and n = 16 is not a multiple of 32. The repack now stores n rounded up to whole tiles and fills the extra columns with zeros. The kernel never computes a column past n, so no sum changes. The loader tiles every projection whose padded words fit one binding, including the n = 16 ones, and projectTiled checks the padded geometry. The contract fixture and the stage 1 probe size their buffers the same way. Proved by: every Vulkan host suite passes; stage 1 probe 21 of 21 on a software Vulkan device with the tiled image identical to the default image on all 11 cases that fit one binding there (n 8, 16 and 257 included).
…-inch M1 The receipts folder holds the identity probe (21 of 21, the tiled image byte-identical on all 12 projection cases), the contract runner's output (3 kernels pass gates 1 to 4, 8 of 8 controls caught), the pre-registration with its addendum, a summary generated from the raw run files, and the SHA-256 of each raw file. Against the vec4 build a run took 19%, 50% and 63% less time on three inputs, with the same logits digest in all six processes of each case.
…ce per batch Every Pipeline.run was its own submission with a fence wait. On a 13-inch M1 the 31,104 dispatches of a 64-step Qwen3.5-2B run (486 a step) cost about 90 us each in submit and wait alone, about 40 percent of the run. Run now records into the open command buffer and Commands submits when 128 dispatches are waiting, or when the next upload, download or fill needs the queue, so a readback still sees every earlier dispatch and no kernel's arithmetic changes. A pipeline holds a ring of 256 descriptor sets: bind fills the next one and record binds the current one, so a set is never rewritten while a recorded dispatch still uses it (a flush at 128 keeps the ring from wrapping). Commands.begin returns the open buffer instead of resetting it; submit with nothing recorded is a no-op. Proved by: every Vulkan host suite passes; stage 1 to 5 probes on a software Vulkan device: 21 of 21, 9 of 9, 4 of 4, 6 of 6 and 7 of 7. Not measured on a GPU yet.
…on a 13-inch M1 The receipts folder holds the stage 1 probe on the GPU (21 of 21, the tiled image equal to the default image on all 12 projection cases), the contract runner's output (3 kernels pass gates 1 to 4, 8 of 8 controls caught), the pre-registration, a summary generated from the raw run files, and the SHA-256 of each raw file. Against the tile build a run took 68%, 49% and 30% less time on three inputs, with the same logits digest in all six processes of each case.
…nd that the fence timeout covers a batch
f73e550 to
9d13b72
Compare
|
Batched submission on the Intel Arc Pro B60, the first measurement of this lever on a second vendor's driver: 65.0% at unicode-24.
Median 65.02% (range 64.5 to 65.0). All six exit 0, My forecast was 15 to 30 percent and it missed by more than a factor of two, in the direction that matters: on the Apple GPU batching is worth about 40 percent (40.5% and 40.6% at 24 tokens), and on the Intel driver it is worth about 65 percent. The pairs are tight, so this is not noise - it is the driver's per-submit and fence cost, which is higher on the B60 and which batching removes. That also explains the end-to-end gap between the two devices: the B60's stack-over-vec4 figure is 68.75% against the M1's 78.5%, and roughly 25 points of the B60's remaining headroom sit in this one lever. Any per-device default should account for it. [Joshua agent] |
|
Batched submission on top of vec4 at document-256 on the 13-inch M1: 19.29%, inside my 15 to 35 percent forecast (45%).
All six exit 0, [Joshua agent] |
|
Thanks @joshuaswarren. The series checks out against the one we reviewed. All ten runners matched with batching on, and the B60 timing shows the lever is bigger on Intel's driver (65%) than on the M1 (about 40%), with the same logits. This lands right after #2015. |
|
Correction: my earlier reply recalculated the displayed table, but those displayed rows do not match the retained timing file. The retained B60 run-2 pairs are 13536→4735 ms (65.019%), 13533→4731 ms (65.041%), and 13351→4738 ms (64.512%). Their median is 65.019%. Each retained timing record says |
…ontracts gates.runAll converts every kernel error, Timeout and InFlight included, into a FAIL verdict. An unguarded GPU call let a fence timeout unwind into the rig's frees and vkDestroyDevice with work possibly in flight, instead of the tree's exit-124 and exit-1 doctrine (commands.zig:104,122). Guard the datamove rig's upload and download and the four fixture launches with vk.orExit. In the greedy contract the draw arm returned select()'s Timeout and InFlight past the fixture boundary and the control arm submitted raw; guard both. A NonFinite refusal still reaches the refused marker. Host tests lock the swallowing channel and the refusal.
…t call fails A host allocation can fail between a recorded dispatch and the submission that drains it. gates.runAll catches the error and the next prepare frees buffers the still-open command buffer references, so a later submit would dispatch on freed device memory. Commands.discard resets the open command buffer and clears the batch state. The qmm fixture and control, the datamove rig, the greedy rig and its device teardown call it before they free anything. Two failures left the stream usable after it should be terminal, and a gate wrapper that catches and continues could reuse it. A vkResetFences failure after a signaled wait left the fence signaled, and a failed vkEndCommandBuffer left an invalid open buffer for the next begin. Both now store a terminal error in Commands: begin and every later submit refuse without recording or queueing, and vk.orExit ends the process. Host tests drive discard through a stubbed vkResetCommandBuffer and force each failure. A further test plants a failed host allocation on the qmm fixture's and the control's allocator, then resets each with a dispatch still recorded. Without discard in those resets the dispatch stays open and the test fails.
|
[Joshua's agent] Note 🤖 Claude Sonnet 5.5 responding on behalf of joshuawarren Following @ashhart's request on #2038, the fix for a control that records a dispatch and then fails a host allocation is now in this PR, as two commits on top of
Regression test, in Host tests ( #2017 and #2038 are rebased onto this head with their own commits unchanged. The only difference in #2038 is that |
Fix for the failed host allocation after a recorded dispatch. Head is e0cffaa, no new commit in this comment. What changed
Test: "a failed host allocation after a recorded dispatch is dropped before the fixture and control free their buffers" (
Scope
Test change
Review: a second-model review is GREEN at e0cffaa. The reviewer did not run the suite. The RED and GREEN logs above come from the implementer. [Joshua's agent] |
Comment-only change: each two-line comment in the Vulkan sources becomes one line of at most 120 columns. Lean check drops the 13 multi-line comment problems this branch adds versus main; no code changed. Checked: lean_check adds zero problems versus main; vulkan-host-test 75/75 steps, 228/230 tests, 2 skipped.
|
Thanks @joshuaswarren, the discard fix with its RED/GREEN test is where we hoped it would land. |
Pushed
[Joshua's agent] |
Stacked on #2015 (tile), which is stacked on #1990 (vec4) and #1544. Review only
530b5b73(the change),63dc7a21(receipts and changelog),46fc1e9aand1eae3a42(comment style). The commits below them are #2015's, #1990's and #1544's.On an Apple M1 with Honeykrisp, a Qwen3.5-2B 4-bit run took 30% to 68% less time than the tile build on three inputs. The logits are byte-identical. No kernel changes.
e5447d9a3ab5147a2d2e02327f9d8e65e90eRun 2 is the prefill plus 63 decode steps plus the per-step logits readback. It is not decode-only. The digests equal the ones the scalar kernel,
qmm_q4_vec4and the tile build gave on the same inputs.Across the three stacked changes the same three runs went from 15.6, 40.0 and 122.8 s (the #1544 scalar kernel) to 2.3, 5.2 and 15.7 s. Those are three paired comparisons measured in separate sessions, not one paired run.
What changes
Every
Pipeline.runwas its own submission with a fence wait. A 64-step run is 31,104 dispatches (486 a step). A profile of the tile build showed the small kernels (copy, gather, norm, add) at 89 to 119 us of host wall time each, which is almost all submit and wait, and the GPU idle between them.Pipeline.runrecords into the open command buffer.Commandssubmits when 128 dispatches are waiting, or when the next upload, download or fill needs the queue, so a readback still sees every earlier dispatch.Commands.beginreturns the open buffer instead of resetting it.submitwith nothing recorded does nothing.bindfills the next one andrecordbinds the current one, so a set is never rewritten while a recorded dispatch uses it. The flush at 128 keeps the ring from wrapping.What callers can rely on:
runno longer waits for its dispatch. Results are visible after any later download, upload or fill, or aftersubmit. The callers ofbeginandsubmitarefill,upload,download,Greedyand the greedy contract fixture; they append to the open buffer and submit it. A fence timeout now covers a whole batch (up to 128 dispatches) instead of one dispatch.Setup
a50413a1; its ELF was built from an identical tree,6a158edc). New build:530b5b73(its ELF was built from0d73b0fb, the same change beforemainwas merged in; the two changed files are identical). Both are ReleaseSafe aarch64 builds oftf-qwen35-model.Identity
Kernel contract
tf-vulkan-kernel-contracton the GPU, exit 0, with this change:No kernel is added or changed here; the run shows the gates still pass when every dispatch goes through the batched path. The output is in
kernel-contract-m1-13-inch.txt.Pre-registration
PREREG.mdwas written before any GPU run. The rule was: a median gain of at least 3% and every pair positive. The forecast was 15% to 35% on the 24-token prompt and 5% to 15% on the documents. All three results are above it: 68.2%, 48.8% and 30.5%. I had taken the profile's per-dispatch host time (about 90 us, 31,104 dispatches) as the cost. The real cost was larger, because the GPU also sat idle between dispatches while the host waited on each fence.Not covered
Checks
python3 tools/zig/lean_check.py: 465 problems before and after, none new.raw-files-sha256.txtlists the hash of each one.Coordination
Part of #1420.
atc check 1420 --jsonon 2026-11-03: open, complete, no other pull requests, adviceyours. My public session on #1420 is 191551954422646. This pull request is stacked on #1544, #1990 and the PRs between them;atc boardshows no duplicate attempt against the stack.