Skip to content

feat(backend): bridge the packed SPI kernels through the ordered key (#973.2) - #1100

Merged
michalharakal merged 1 commit into
developfrom
feature/1095-packed-kernel-bridge
Aug 24, 2026
Merged

michalharakal merged 1 commit into
developfrom
feature/1095-packed-kernel-bridge

Conversation

@michalharakal

Copy link
Copy Markdown
Contributor

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 TensorView loaded 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 — a ViewKernel over any of the packed SPI kernels, keyed on BLOCKED_INPUT_MAJOR and 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 = true exists for one-shot calls where the copy beats decoding.

This is also why #1097 (engine-owned prepackForMatmul at 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:

  • the registry path is bit-identical to calling the SPI kernel by hand with block-major bytes;
  • the recorded digests' leading values are the existing scalar-matmul/* goldens — the bridge changes nothing about the numbers, which is the whole claim;
  • the relayout is visible as one prepack-input_block_major adapter;
  • a weight already in kernel order costs no adapter at all;
  • every weight is three blocks wide — at one block per row the two orders coincide and the test would pass vacuously.

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 in DefaultCpuOps/DefaultCpuOpsJvm is 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

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>
@michalharakal
michalharakal merged commit 2b39fd7 into develop Aug 24, 2026
14 checks passed
@michalharakal
michalharakal deleted the feature/1095-packed-kernel-bridge branch August 24, 2026 16:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[973.2] Bridge the packed SPI kernels through the ordered key (unblocks #1029)

1 participant