Repository navigation
Decode threading: split the decode mat-vec across threads (plan D0–D1) - #4
Merged
Merged
Conversation
…[skip ci]
Plan of record: decode threading (one-row matvec through the parallel-for
hook), rev 1.2, step D0 (§3.2, §3.6, §3.7, §6). No behaviour change.
- src/forward/parallel_split.{h,cpp} (internal, no SUPERSLM_API):
SplitColumns (§3.2's T, rows_per_task and task_count), RunExactlyOnce
(the logits step's admission array, violation flag and acquire scan,
generic over a noexcept body), SaturationCounters (snapshot and restore
of the five per-sequence saturation counters), and
ParallelForHookValid with the implemented-bit mask, still empty.
- LogitsSiteParallel calls SplitColumns (align 64, no minimum work) and
RunExactlyOnce. Its partition, statuses and `run` calls are v1.9.0's,
the one-task call at V <= 64 included (M1).
- Both hook setters (CPU and the WIN32 GPU one) call ParallelForHookValid.
- Cells (tests/test_decode_threading.cpp, in superslm_tests and the forced
suites): 4.1 (the split's contract over align 32/64, 15 widths, three
minimum works and max_tasks 1-256), 7.1 (the split against v1.9.0's
formula for V 1-2,999 and 151,936 at max_tasks 2-256, and through
LogitsSiteParallel itself: task count, the block task 0 writes, the
one-task call and the row), and the snapshot round trip.
- The new source is added to CMake, build.bat and the tools/build_*.bat
recipes; gpu_layer_loop_guards.def is re-cited (+1 line, the include).
GCC 13: superslm_tests 121,658 checks, 0 failures (base 121,651).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfuJLAZ6EFSt7mB85oNm68
…he §8 red suite [skip ci] The red half of D1, authored before the implementation (plan rev 1.2, §6 D1, §8): - tools/gen_decode_threading_fixture.py: the §3.9 CI fixtures (F-DEF, F-QK and the no-clamp vitality variant), built through the reference pipeline; never committed. - The minimum-work and layer-site fault seams (src/forward/parallel_split.h) under SUPERSLM_ENABLE_MATVEC_TEST_SEAMS, on the test libraries only. - SSLM_PARALLEL_FOR_MATVEC, GemmThreading, the new RunLayerLoop and RunLayerLoopChunkBatched overloads, MatvecGroupParallel and GemmDispatch, wired through decode and one-token prefill, with the contract text. - Stubbed so the suite is red: the implemented-bit mask is still empty, MatvecGroupSplit always returns a serial split, and neither retry guard restores anything. - tests/test_decode_threading_d1.cpp (cells 1.1-1.3, 2.1-2.3, 3.1/3.2, 4.2-4.4, 5.1-5.5, 6.1-6.3, 7.2, 8.1, 8.2, 11.1), the scalar-tier standalone runner, the 7.4 allocation executable, and the WIN32 setter sibling block in cell_setters.cpp. - gpu_layer_loop_guards.def re-cited (+34); the parity check passes. Red run (GCC 13, fixtures generated): superslm_tests 123,438 checks, 478 failures, all in the D1 cells; superslm_decode_threading_alloc 2 failures. Without the fixture directory the D1 cells fail rather than skip (27 failures). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfuJLAZ6EFSt7mB85oNm68
…uards [skip ci] Plan rev 1.2 §3.1, §3.6 and §3.7: - SSLM_PARALLEL_FOR_MATVEC joins the implemented-bit mask; bit 0 is still rejected by both setters. - MatvecGroupSplit applies the §3.1 rule: a hook with max_tasks >= 2, alignment 64 and a minimum of MinRowBytesPerTask() weight bytes per task; below two tasks the group runs the serial calls of v1.9.0. - The decode guard restores the five saturation counters to the layer attempt's start on ParallelForIncomplete only. - The prefill guard restores them on any failed call, the exception arm included, iff context_length did not move. Green run (GCC 13): superslm_tests 123,464 checks, 0 failures (the D1 cells 2,044); superslm_decode_threading_alloc 84 checks, 0 failures. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfuJLAZ6EFSt7mB85oNm68
…ch [skip ci] - CI: every leg that runs a suite binary sets up Python, installs numpy, generates the §3.9 fixtures into dt-fixtures and points SUPERSLM_DECODE_THREADING_FIXTURE_DIR at them. That is fourteen legs: windows-x64, linux-x64, ASan, macOS, Debug, TSan, the three Linux forced tiers, the four Windows forced tiers and branch coverage. linux-x64 also runs the 7.4 allocation binary, and the SSE2-forced leg runs the scalar-tier decode-threading binary (4.5). - docs/api.md and CHANGELOG [Unreleased]: - the opt-in bit and its rule; - that setters refuse unimplemented bits; - the decode retry behaviour; - the prefill saturation-count change, which applies to every host. - The LogitsSiteParallel and ParallelForIncomplete comments name the shared split and the new source of the status. - tools/decode_threading_bench.cpp (§7 B0/B1), with the verify, time and onetoken modes, plus a groups mode that times one layer's five groups at Qwen2.5-0.5B shapes with no artifact. It is built as sslm_decode_threading_bench on the production library and as sslm_decode_threading_bench_seams for B1's minimum-work sweep. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfuJLAZ6EFSt7mB85oNm68
The tests/ci pytest against the base found three things the D0/D1 commits
missed:
- check_no_forward_leaf_calls.py: D0 added src/forward/parallel_split.{h,cpp};
the exact expected-file list now names them.
- geometry_site_census: the four D1 fault seams sat between a GS-12 marker
and its anchored call, regressing the site. They now sit above the marker,
and the decode q call is wrapped so the anchor line carries the output
width again. No behaviour change.
- test_geometry_site_census.py: the GS-12 reorder-plus-revert fixture
follows the decode q call's new text (ProjectAndFunnelRow(row_pf, ...)),
as the fixture's own assertion asks.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfuJLAZ6EFSt7mB85oNm68
…cts [skip ci] D0 added src/forward/parallel_split.cpp to SUPERSLM_CORE_SOURCES, so the superslm target is 18 translation units and 18 archive objects. The t2296 suite pinned 17 in the manifest cells (red here), the real-build object count, and the COFF and ELF archive member counts (Windows CI and an environment-provided archive). Each pin now reads 18, and the fp-at-k carrier sweep covers positions 1..19 of 22-member archives. Here, the suite is back to the base's result: the same 4 environmental failures, 154 passed; with a real ELF archive supplied, the same 5 as the base. The COFF cells run only on the Windows leg and are not executed here. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfuJLAZ6EFSt7mB85oNm68
…e evidence [skip ci] - Cell 2.1 also lands its four hostile cases on o. The coverage replica showed o's failure arm (its fail_layer) taken by no cell; §8 2.1 names every other threaded site. A mutant that routes o's failure around the decode guard dies on these cases and nothing else. - docs/decode-threading-d0d1-progress.md: the items, their states, the deviations and what the plan got wrong. - docs/decode-threading/: the red run, the suites on every tier and both compilers, the sanitizers, the digests, the mutants, the fp-free scan, the Python suites against the base, the fixture premise, 6.3 and 9.1 against v1.9.0 (with the probe's source), the 9.3 symbol check, the coverage replica and the indicative bench. Final tree: superslm_tests 123,766 checks, 0 failures (GCC 13 and Clang 18, every tier; the D1 cells 2,108 on each); digests equal to the base on all five tiers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfuJLAZ6EFSt7mB85oNm68
…1.10.0 release [skip ci] The D1 contract text was written against an [Unreleased] section that sat directly above 1.9.0. On the new main, 1.10.0 is released and its section sits between the two, so the three-way apply put the entry at the end of 1.10.0. It now opens [Unreleased], and its three references to the previous release name 1.10.0: its setter refuses a nonzero reserved field, its logits step alone reads the hook, and its prefill keeps the counts of a failed call, as 1.9.0's did. The 1.10.0 section is byte-identical to main's. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfuJLAZ6EFSt7mB85oNm68
… [skip ci] docs/decode-threading/rebase-1.10.0.txt records what applying the series onto main at fdeee2f changed (the CHANGELOG placement only), and the re-run against main on this host, GCC 13 and Clang 18: every suite at main's count plus 2,115 with 0 failures, the ten digest legs equal to main's GLOBAL hash, the fp-free scan, pytest tests/ci at main's 915 passed, and the named checkers. It also compares the coverage replica with the floors main re-pinned after c28f171. The progress file's base, resume steps and coverage note point at it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfuJLAZ6EFSt7mB85oNm68
…it.cpp [skip ci] tools/build_adapter_dump.bat named the new source without its %ENG% prefix, so cl looked for tools\src\forward\parallel_split.cpp and stopped with C1083. It now reads %ENG%\src\forward\parallel_split.cpp like every other source on that line. The same grep found a second, larger gap from D0: 26 other build lists (red-suite build and mutant scripts under tests/, the three t2922 PowerShell runners, and the two disposable t2132 diagnostics under tools/) compile src\forward\forward_sites.cpp without parallel_split.cpp at all. forward_sites.cpp calls SplitColumns, RunExactlyOnce, MatvecGroupParallel and the rest out of that file, so each of those links failed with unresolved externals. Each now names the file next to forward_sites.cpp, in the prefix and quoting style of its own line. A GCC replica of build_adapter_dump.bat's list links; the same list without parallel_split.cpp leaves 11 undefined references. No CI step or checker compiles these scripts. check_tools_have_build_recipe.py and check_tests_have_build_recipe.py only check that a recipe names each tool or suite, and CI runs neither. check_build_bat_exit_paths.py checks build.bat's exit paths only. The MSVC run of the scripts is still owed on Windows. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfuJLAZ6EFSt7mB85oNm68
…r, as the code does them [skip ci] docs/api.md said each decode layer's q, k and v form one group and that a group splits into tasks of at least 256 KiB each. The code sends q alone (ProjectAndFunnelRow) and groups only k + v, and SplitColumns uses 256 KiB to cap the task count (floor(N*K / 256 KiB)) before rounding rows per task up to 64, so the last task can be smaller. At N = K = 896 and max_tasks 4 the split is 320/320/256 rows and the last task streams 229,376 bytes. The text now names the five decode groups (q; k + v; o; gate + up; down), says a one-token prefill splits each of its seven projections alone, gives the split rule, and states that 256 KiB is a cap on the task count and not a per-task floor, with the 896 example. It also says why the 97 run calls hold at 0.5B: k + v (256 rows, 229,376 bytes) stays serial. parallel_for.h's "tasks of at least a fixed minimum each" and the CHANGELOG's "256 KiB per task" carried the same per-task claim and are corrected the same way. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfuJLAZ6EFSt7mB85oNm68
… prove [skip ci] The promise that every guard cell first proves the checked counters rose in the failed layer cannot hold at q and k + v: those sites fail before layer 1 can move any counter (progress file, deviation 3). The owner's seat ruled the review's recommendation (PD-1; review record: Wizard Claude/Poirot/decode-threading-review-2026-09-30.md, section 3). A site that fails before any counter can move proves retry identity and no drift. Only the later sites (o, gate + up, down) prove the counters rose and were restored. Cell 2.1 now encodes the ruling. The test hook reads the sequence's live saturation counters at the moment it fails the call (the forward pass is blocked in run then). Each site is tagged with what it proves: - q, k + v (no drift): the counters at the failure equal layer 0's, so the site really is early; the sequence rests at layer 1 with nothing of layer 1 left; the retry equals the never-failed run. No restore is claimed. - o, gate + up, down (restore): F1 holds on the counters read at the failure (every checked counter rose in layer 1, rope_q != rope_k), then the rest-at-layer-1 restore check and the retry, unchanged. - finish: every layer keeps its counts, unchanged. Every case also asserts the hostile call ran. The file header, the F1 comment and CheckRestsAtLayer1's message no longer say "restored" where nothing moved. D1 checks go from 2,108 to 2,152 (24 + 20). The progress file's deviation 3 is rewritten, deviation 18 records the ruling, and "What the plan got wrong" gains item 8. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfuJLAZ6EFSt7mB85oNm68
…xp; record and re-run [skip ci] The review regenerated the fixtures on Windows (Zen 2, no AVX-512): F-DEF and fnoclamp matched the record and F-QK did not (31dd0c5b... against the recorded 27e82bcb...). Root cause: numpy's float64 np.exp is dispatched on the CPU. On an AVX-512 host numpy runs its own AVX-512F exp; elsewhere it calls the C library's (glibc, or the MSVC CRT on Windows). The results differ in the last bits. The float calibration calls np.exp in the softmax and SiLU, so the per-channel post-RoPE K peaks differ in their low bits. _qk_channel_table_from_peaks converts each binary64 peak exactly into a 31-bit mantissa, its reciprocal and a Q31 ratio. So only F-QK's QKC1 section moves (181 of 16,656 bytes; every other section byte-equal). F-DEF and fnoclamp strip QK-norm and carry no QKC1 table. Measured on this host: - This head reproduces the record (27e82bcb...), and so do the OPENBLAS_CORETYPE variants. - With numpy's AVX-512 dispatch disabled, F-QK becomes 623d3efe... and F-DEF and fnoclamp do not change. - With np.exp swapped for math.exp, both dispatch settings give 623d3efe.... - tools/ is unchanged by 1.10.0 apart from coverage floors and a release-gate test. This is not an engine divergence: no engine code runs in the generator. The D1 cells pass on 623d3efe... here, as they did on 31dd0c5b... in the review. Re-run against v1.9.0 (v190_blob_probe, the d870d27 library against this head's seam library) on freshly generated F-DEF and both F-QK variants: 6.3 and 9.1 byte-equal on all three. The fixture record now says the generator is deterministic per host, with the evidence. The generator's docstring and the progress file (deviation 19, the 6.3/9.1 row, Resume here) say so too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfuJLAZ6EFSt7mB85oNm68
…nt re-run [skip ci] docs/decode-threading/review-fixes.txt records the four fixes and the re-run at this head, on fixtures generated fresh here. Linux, GCC 13 and Clang 18: - superslm_tests 123,814, SSE2-forced 123,756, AVX2- and AVX-512-forced 123,772 (executed on AVX-512), scalar-tier decode threading 2,159, allocation 84: 0 failures, the same on both compilers. - All ten digest legs give GLOBAL f740f833...4ae5acc. - The fp-free scan passes: 18 objects, 0 rejected. - pytest tests/ci: 915 passed, 1 skipped. The named checkers are OK. mutants.txt gains the PD-1 re-run: the 11 mutants whose red set includes 2.1, all killed. By site, M7 is red at o, gate+up and down only, as PD-1 says it must be. The progress file gains a review section, and deviation 14 now names the build-list gap P1 closed. The Windows runs are listed as still owed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfuJLAZ6EFSt7mB85oNm68
… and D2 B0 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XfuJLAZ6EFSt7mB85oNm68
dansupergameprogrammer
marked this pull request as ready for review
September 30, 2026 18:56
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Steps D0 and D1 of the decode-threading plan (rev 1.2, approved). This is meant to ship as 1.11.0. The version number is not bumped here, and nothing is released.
Before / After
Before: decode mat-vecs run on one thread. A host's
sslm_parallel_forhook is used only by the final logits step. Every per-layer projection in decode, and in a prefill that admits one token, runs on the calling thread.After: a host can opt in by setting bit 1 of
sslm_parallel_for.reserved, the newSSLM_PARALLEL_FOR_MATVEC. With the bit set, each one-row (M = 1) projection splits its output rows across the hook'srun. That covers each decode layer's five groups (q alone, k + v together, o alone, gate + up together, down alone), and each of the seven projections of a prefill that admits exactly one token. Every row is still one exact integer sum computed on one thread, so the split is exact.Default: off. Without the bit nothing changes, and a 1.10.0 host behaves exactly as before. The plugin's own default is not set here; the owner sets it after the box measurements (see Still owed).
How
src/forward/parallel_split.{h,cpp}, holds the column split, the run-exactly-once runner and a snapshot of the per-sequence saturation counters. The logits step now uses the same split. Its partition is unchanged.max_tasks, ceil(rows / 64) and floor(weight bytes / 256 KiB). Rows per task are then rounded up to 64, so 256 KiB caps the task count; it is not a floor on each task, and the last task can be smaller (896 × 896 at 4 tasks: 320/320/256 rows, the last 229,376 bytes). A group that gets fewer than two tasks stays serial. Qwen2.5-0.5B with 4 tasks makes 97runcalls per decode token (k + v stays serial).runbreaks its exactly-once contract inside a decode layer, the call fails withSSLM_INVALID_ARGUMENT. The sequence rests at the start of that layer with that layer's saturation counts put back, and the next call resumes there.RunLayerLoopandRunLayerLoopChunkBatched. The existing symbols stay. No struct, status, C function or save format changes.tools/gen_decode_threading_fixture.py; they are never committed. Every CI leg that runs a suite binary gains a step that generates them. A missing fixture fails the tests rather than skipping them.tools/decode_threading_bench.cppis the tool for the box measurements.docs/decode-threading-d0d1-progress.md.Code review (HOLD at 5043ce4) and fixes
The review passed the core threading on Windows MSVC: output identity, retry state, allocation, ABI and seams. On Windows, 133,887 checks with 0 failures, forced SSE2 and AVX2 green, and the WIN32 setter cell passed. It raised four findings, each fixed in its own commit:
9677431.tools/build_adapter_dump.batnamedparallel_split.cppwithout%ENG%. Grepping for the same defect found 26 more MSVC build lists (red-suite scripts undertests/, the t2922 PowerShell runners, the two t2132 diagnostics) that omitted the file entirely and so failed to link. All are fixed. No CI step or checker compiles these scripts.6671e43. Cell 2.1 encodes the owner's ruling. The hook reads the live counters at the failure. At q and k + v nothing has moved yet, so those cases prove no drift and retry identity and claim no restore. At o, gate + up and down, the checked counters are proved to have risen before the failure, then restored. The 11 mutants that touch 2.1 were re-run: 11 killed.5025769.docs/api.md,parallel_for.hand the CHANGELOG now say what the split does (above).c5efc72. F-QK is host-dependent. numpy dispatches float64np.expon the CPU (its own AVX-512 exp here, the C library's elsewhere), and F-QK's QKC1 table is exact in the float calibration peaks. Only that section moves; F-DEF and fnoclamp have no QKC1 table and match everywhere. Nothing in 1.10.0 changed the generator, and no engine code runs in it. This host at this head still writes27e82bcb…. 6.3 and 9.1 against v1.9.0 were re-run on the fresh fixtures and on both local F-QK variants (27e82bcb…,623d3efe…): byte-equal.Bit-identity evidence
Checked on Linux with GCC 13 and Clang 18, Release, on an AVX-512 host, at the review-fix head. Full record:
docs/decode-threading/review-fixes.txt; the move onto 1.10.0 is indocs/decode-threading/rebase-1.10.0.txt.superslm_testshas 123,814 checks (main 121,655), with 2,152 D1 checks after the PD-1 change. The scalar-tier decode-threading binary (2,159 checks) and the per-token allocation binary (84 checks) also pass with 0 failures.f740f833…4ae5acc, which is main's hash. The whole output matches main's byte for byte on every leg.max_tasks1, 2, 4 and 8. The tests also check each row against the scalar reference.pytest tests/cigives 915 passed, 1 skipped, the same as main. The CI-claims, present-tense, GPU-guard parity, linkage, isolation, forward-leaf and site-census checks all pass.Indicative cloud speed
Decode tokens/s with the bit on are 1.27–1.57x the rate with it off. This was measured on a shared 4-vCPU cloud Xeon with a synthetic 2-layer artifact at 0.5B width. Indicative only. Not measured on the owner's desktop, and not a plan reading.
Still owed
tools\build_adapter_dump.batand at least one of the other patched MSVC lists; the WIN32 setter gate and the Windows suites at the new head; optionally the v1.9 probe on the Windows-generated F-QK (31dd0c5b…).🤖 Generated with Claude Code
https://claude.ai/code/session_01XfuJLAZ6EFSt7mB85oNm68
Generated by Claude Code