feat(backend): bridge the packed SPI kernels through the ordered key (#973.2) - #1100
Merged
Merged
Conversation
Closes #1095 (#973.2). Unblocks what #1029 deferred. The packed matmul kernels read their weight input-block-major; a packed `TensorView` loaded from a file is canonical. Until #1094 nothing said which was which, so #1029 left them unbridged rather than risk the silent-wrong-numbers class of #968/#971. Now the order is in the key. - `PackedViewMatmulKernel`: a `ViewKernel` over any of the packed SPI kernels, keyed on `BLOCKED_INPUT_MAJOR` — the order it actually reads, checked at run time rather than assumed. Falls back to the reference for a strided activation or non-heap storage instead of mis-indexing. - `KernelPacks.installPacked(provider)` registers Q4_0, Q5_0, Q5_1, Q8_0, Q4_K, Q5_K and Q6_K when the provider offers them; a format it does not offer keeps being served by the decoding reference. - `KernelDispatch.matmul(..., prepackWeights = false)` can relayout a canonical weight to reach those kernels — **off by default, on purpose**. The relayout is O(bytes), so doing it inside a decode step would copy the whole weight per token, which is the per-forward copy #973 objects to, merely moved. Wiring it on by default broke M1-A3 (zero forward-scope allocations per step) in exactly that way, which is how the default got chosen. A weight is prepacked once at load; a canonical weight handed straight to the dispatcher still gets the correct, slower reference path. `PackedDispatchBridgeGoldenTest`: for all seven encodings, the registry path is **bit-identical** to calling the SPI kernel by hand with block-major bytes — and the recorded digests' leading values are the existing `scalar-matmul/*` goldens, which is the point: the bridge changes nothing about the numbers. Every weight is three blocks wide, the case where the two orders differ. A weight already in kernel order costs no adapter at all. Gate: scripts/pr-gate.sh — all legs passed; --golden passed (46 golden tests, the 39 existing digests unchanged). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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 #1095 · #973.2 · unblocks what #1029 deferred
What was in the way
The packed matmul kernels read their weight input-block-major; a packed
TensorViewloaded from a file is canonical. Until #1094 nothing in the key said which was which, so #1029 deliberately left them unbridged rather than risk the silent-wrong-numbers class of #968/#971. Now the order is part of the key, and a kernel can declare what it reads.PackedViewMatmulKernel— aViewKernelover any of the packed SPI kernels, keyed onBLOCKED_INPUT_MAJORand checking it at run time rather than assuming. A strided activation or non-heap storage falls back to the reference kernel instead of being mis-indexed.KernelPacks.installPacked(provider)registers Q4_0, Q5_0, Q5_1, Q8_0, Q4_K, Q5_K and Q6_K when the provider offers them; anything it does not offer keeps being served by the decoding reference.The default that a failing test chose
KernelDispatch.matmul(..., prepackWeights = false)can relayout a canonical weight to reach those kernels — off by default, on purpose.I first wired it on by default. That broke M1-A3 (zero forward-scope allocations per decode step): the dispatcher was relayouting the whole weight on every matmul of every token — precisely the per-forward O(bytes) copy #973 objects to in
ops.transpose, merely relocated. The regression is the argument: a weight is prepacked once, at load, and a canonical weight handed straight to the dispatcher still gets the correct, slower reference path.prepackWeights = trueexists for one-shot calls where the copy beats decoding.This is also why #1097 (engine-owned
prepackForMatmulat the load boundary) is the natural next slice: it is what makes the fast path the default without a per-call copy.Acceptance
PackedDispatchBridgeGoldenTest, for all seven encodings:scalar-matmul/*goldens — the bridge changes nothing about the numbers, which is the whole claim;prepack-input_block_majoradapter;Gate
scripts/pr-gate.sh— all legs passed.--golden— passed: 46 golden tests, the 39 pre-existing digests unchanged.Keeps develop green by
Additive registration plus one defaulted parameter whose default is today's behaviour. The
is-ladder inDefaultCpuOps/DefaultCpuOpsJvmis untouched — it can start shrinking once #1097 makes loaded weights arrive prepacked, and that is a separate PR with its own benchmark evidence.🤖 Generated with Claude Code