Skip to content

feat(ops)!: matmulWeightTransposed as the primitive; transpose refuses a packed weight (#973.3) - #1105

Merged
michalharakal merged 1 commit into
developfrom
feature/1096-matmul-weight-transposed
Aug 25, 2026
Merged

michalharakal merged 1 commit into
developfrom
feature/1096-matmul-weight-transposed

Conversation

@michalharakal

Copy link
Copy Markdown
Contributor

Closes #1096the last slice of #973

⚠️ Breaking. ops.transpose now throws for a heap block-quantized weight. Confirmed acceptable: downstream is pinned and will be updated.

The operation that never existed

ops.transpose on 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:

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 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.
  • TensorOps.relayoutPackedWeightForKernels(weight) — the conversion under its own name, for callers who want permuted bytes rather than a product. Defaults to refusing.
  • DefaultCpuOps.transpose throws for a heap packed weight, naming the primitive, the explicit relayout, and why the operation is not definable.
  • Linear.onForward asks 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 wants matmulWeightTransposed; a MemSeg or narrow-float site wants plain transpose and 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); 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.shall legs passed. --golden — passed: no packed byte layout moved.

#973 is now complete

#1094 block order in the type system + normative doc
#1095 packed SPI kernels bridged through the ordered key
#1097 engine-owned relayout + cross-repo fixtures
#1098 orientation at the load boundary
#1096 the primitive; packed transpose refuses (this PR)

🤖 Generated with Claude Code

…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>
@github-actions

Copy link
Copy Markdown

📖 Documentation Preview

The documentation has been built successfully for this PR.

Generated Files:

  • Operator documentation: docs/modules/operators/_generated_/
  • JSON schema output: operators.json

Artifacts:

  • Download the documentation-preview-1105 artifact to view the complete documentation locally.

This comment will be updated automatically when the PR is updated.

@michalharakal
michalharakal merged commit 421ec5e into develop Aug 25, 2026
23 checks passed
@michalharakal
michalharakal deleted the feature/1096-matmul-weight-transposed branch August 25, 2026 04:55
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.3] A weight-transposing matmul primitive; ops.transpose on packed data becomes a loud error

1 participant