feat(ops)!: matmulWeightTransposed as the primitive; transpose refuses a packed weight (#973.3) - #1105
Merged
Conversation
…s a packed weight Closes #1096, the last slice of #973. `ops.transpose` on block-quantized data was never a transpose. Blocks quantize runs along the input dimension, so a real one needs requantization; what happened instead was a layout conversion wearing transpose's name, with a shape label that lied about the bytes. It was not an involution — `transpose(transpose(W))` is a different matrix for a non-square block grid — and since #969 it copied the whole weight on *every* call, which for `Linear.onForward` meant every forward pass. - `TensorOps.matmulWeightTransposed(x, weight)`: `x · Wᵀ` with the weight as `[out, in]` — the primitive ggml (`mul_mat`) and BLAS (`op(B)`) actually have. Defaulted to `matmul(x, transpose(w))`, so no implementation has to change; `DefaultCpuOps` overrides it to relayout a packed weight **once per weight** and reuse it, which is the point. - `TensorOps.relayoutPackedWeightForKernels(weight)`: the conversion under its own name, for callers who want the permuted bytes rather than a product. Defaulted to refusing. - `DefaultCpuOps.transpose` **throws** for a heap packed weight, naming the primitive, the explicit relayout, and why the operation is not definable. Scoped to the heap tier: the MemorySegment kernels read canonical bytes, so a shape swap is genuinely correct there — census contradiction #3, now stated in code rather than implied by a marker interface. Narrow floats keep their view rewrap (#888). - `Linear.onForward` asks for the product instead of transposing. Breaking, deliberately: 27 in-repo call sites moved to one of the two new entry points. The user has confirmed the downstream break is acceptable and that versions are pinned. The relayout cache is bounded (64 weights) and keyed by the identity of the packed bytes: a model's weights are stable, so the first forward converts and every later one reuses; beyond the bound the oldest is dropped and reconverted, which is exactly the old per-call behaviour. Tests: the primitive computes exactly what relayout-then-matmul did; repeated calls keep agreeing with the explicit relayout; transpose refuses with a message naming the primitive, the relayout and the reason; a dense weight transposes as it always did. Gate: scripts/pr-gate.sh — all legs passed; --golden passed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
📖 Documentation Preview The documentation has been built successfully for this PR. Generated Files:
Artifacts:
This comment will be updated automatically when the PR is updated. |
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 #1096 — the last slice of #973
The operation that never existed
ops.transposeon block-quantized data was never a transpose. Blocks quantize runs along the input dimension, so a real transpose needs requantization. What happened instead was a layout conversion wearing transpose's name, with a shape label that lied about the bytes — and:transpose(transpose(W))is a different matrix for a non-square block grid, and nothing detected it;Linear.onForwardmeant every forward pass;[in, out]shape had no self-consistent packed interpretation, sotoFloatArray()on it was garbage.What replaces it
TensorOps.matmulWeightTransposed(x, weight)—x · Wᵀwith the weight as[out, in]: the primitive ggml (mul_mat) and BLAS (op(B)) actually have. Defaulted tomatmul(x, transpose(w)), so no implementation has to change;DefaultCpuOpsoverrides it to relayout a packed weight once per weight and reuse it.TensorOps.relayoutPackedWeightForKernels(weight)— the conversion under its own name, for callers who want permuted bytes rather than a product. Defaults to refusing.DefaultCpuOps.transposethrows for a heap packed weight, naming the primitive, the explicit relayout, and why the operation is not definable.Linear.onForwardasks for the product instead of transposing.A distinction this made explicit
The refusal is scoped to the heap tier. The MemorySegment kernels read canonical bytes, so for those a shape swap is genuinely correct — that is census contradiction #3, and it is now stated in code instead of being implied by which marker interface a class happens to have. Narrow floats keep their view rewrap (#888). Both of those came out of test failures while doing this, not from reading.
The cache, and its bound
The relayout is memoized per weight, keyed by the identity of the packed bytes, bounded at 64. A model's weights are stable, so the first forward converts and every later one reuses. Past the bound the oldest is dropped and reconverted — which is exactly the old per-call behaviour, so the failure mode of the cache is "as slow as before", never "wrong".
Migration
27 in-repo call sites moved to one of the two entry points, and the choice per site was not mechanical: a test asserting "transpose keeps packed bytes and swaps the shape" wants
relayoutPackedWeightForKernels; one computing a product wantsmatmulWeightTransposed; a MemSeg or narrow-float site wants plaintransposeand was reverted after I over-migrated it.Acceptance
MatmulWeightTransposedTest: the primitive computes exactly what relayout-then-matmul did; repeated calls keep agreeing with the explicit relayout (the cache is not stale);transposerefuses with a message naming the primitive, the relayout and the reason; a dense weight transposes as it always did.Gate
scripts/pr-gate.sh— all legs passed.--golden— passed: no packed byte layout moved.#973 is now complete
transposerefuses (this PR)🤖 Generated with Claude Code