feat(#1195): spin-then-park thread pool for the packed matmul kernels — 153 → 61 ms/step - #1196
Merged
michalharakal merged 2 commits intoAug 27, 2026
Conversation
…n the Pixel 8a Row-partition threading for both orders of both kernels (feed-order and the #1189 row-major variants), through a shared runner (skainet_row_threads). The design is the product of a measured elimination on device, each variant a cooled Qwen2.5-1.5B Q4_K_M decode step: single-threaded (#1190 baseline) 153 ms pthread_create/join per call 994 ms (cpuidle wakeup × ~600/step) parked pool, static quarter chunks 115 ms (big.LITTLE straggle) parked pool, 64-row stealing 227 ms (short scattered windows) parked pool, guided grains 306 ms (still scheduler-starved) spin-then-park pool + guided grains 61 ms ← shipped The decisive piece is the spin: sub-millisecond parallel bursts separated by sleeps never build per-thread utilization, so EAS/schedutil parks workers on little cores at low clocks — the pool was slower than one pegged big core. Workers spin (`yield`) on the job epoch for ~1 ms before parking on the condvar: utilization stays pegged during decode, the scheduler answers with big cores and full clocks, and everyone parks when work stops (no idle burn). Same reason llama.cpp's pool spins. Structure: kernels refactored around shared per-block terms + row-range workers; entries quantize the activation to Q8 once (read-only across threads) and hand the rows to skainet_run_rows. Guided grains off an atomic cursor — long contiguous streams first, small tail last — degrade to an even split on symmetric cores; nothing is tuned to one SoC. Threshold 512 keeps GQA k/v projections (256 rows) single-threaded. MSVC (no pthreads) and any pthread_create failure degrade to the caller's thread. Bit-identity: workers own disjoint out[] ranges and per-row accumulation order never changes. RowMajorMatmulParityTest grows threaded cases with two oracles: threaded-vs-feed-order on permuted bytes, and threaded-vs-1536 independent single-row calls (pins the partition arithmetic itself). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…t fallback trap, measured `-e residency heap` restores heap staging (default mapped) so staging strategies can be A/B'd on models that fit the cap. First use found a trap instead of an answer: SmolLM2-135M's hidden size (576) is not a 256-multiple, so llama.cpp quantized most of its matrices as Q8_0 (k-quant fallback) — and heap Q8_0 in canonical order without prepack has no BLOCKED_ROW_MAJOR kernel, so dispatch silently served the decoding reference: heap + prepack=true 66 ms/step mapped + prepack=false 48,771 ms/step (Q8_0 -> reference, ~800x) mapped + prepack=true 65 ms/step (mapped rm + prepacked Q8_0) Exactly #1193's "silent fallback" case and #1192's Q8_0 priority, now with numbers. The Qwen runs never hit it (all dims 256-multiples, pure Q4_K/Q6_K). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
michalharakal
merged commit Aug 27, 2026
5d1395f
into
feature/1189-mapped-packed-staging
4 checks passed
Open
5 tasks
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.
Closes #1195. Stacked on #1190 (threads the
_rmkernels that live there) — merge #1190 first, then retarget/merge this onto develop.Threads the Q4_K/Q6_K matmuls (both feed-order and row-major variants) over output rows via a shared runner. Verified with the M2-A5 harness on the Pixel 8a, Qwen2.5-1.5B Q4_K_M, each variant a cooled run:
pthread_create/join per call2.5× end-to-end, and the elimination explains Android inference threading in one table:
pthread_once.yield) ~1 ms for the next job before parking. Utilization stays pegged during decode → big cores, full clocks; parked (no battery burn) once decoding stops.Bit-identity is preserved (disjoint row ranges; per-row accumulation order unchanged) and pinned by two new oracle families in
RowMajorMatmulParityTest: threaded-vs-feed-order on permuted bytes, and threaded full-matrix vs independent single-row calls. 13/13 green on Apple-M (a second, symmetric-ish chip), full native-cpu jvmTest 139/139, MSVC path degrades to single-thread (no pthreads).Combined with #1190: the same 1.0 GB model that OOM'd two days ago now decodes at 61 ms/step ≈ ~14 tok/s equivalent under the 256 MB cap, with zero steady-state page faults — the "threaded kernels" rung of the #1195 ceilings ladder, reached.
🤖 Generated with Claude Code