Skip to content

feat(memory): block order belongs to the layout — BlockOrder, a visible prepack adapter, the normative doc (#973.1) - #1099

Merged
michalharakal merged 1 commit into
developfrom
feature/1094-block-order
Aug 24, 2026
Merged

michalharakal merged 1 commit into
developfrom
feature/1094-block-order

Conversation

@michalharakal

Copy link
Copy Markdown
Contributor

Closes #1094 · the foundational slice of #973 · Proposal §5.1

The contract, finally written in the type system

#973's census found seven mutually contradicting statements of one contract inside this repository — two of which had already shipped wrong numbers (#968, #971). The contract is simple to state and was nowhere expressed: a packed weight's blocks are in one of two orders, and which one is a property of the value.

order flat block index produced by read by
ROW_MAJOR (canonical) o * blocksPerRow + b GGUF, quantizers, TernaryCodec toFloatArray(), get(), the reference matmul
INPUT_BLOCK_MAJOR (kernel feed order) b * out + o prepack every packed matmul kernel

They coincide only when a row is a single block, which is exactly why mixing them produced plausible, finite, wrong numbers instead of a crash.

What landed

  • Layout.blockOrder, defaulting to ROW_MAJOR. The order is expressed in the strides — input-block-major is [1, out] over the block grid rather than [blocksPerRow, 1] — so narrow, transpose, get and toFloatArray keep working unchanged on a view in either order instead of each growing a branch.
  • LayoutClass.BLOCKED splits into BLOCKED_ROW_MAJOR / BLOCKED_INPUT_MAJOR, so a kernel declares the order it reads in its KernelKey and the dispatcher can relayout. This is precisely what [S1.7c] P3: DefaultCpuOpsJvm matmul arms → registered kernels; provider packs (reference always present, capabilities in the key) #1029 deferred, and it unblocks [973.2] Bridge the packed SPI kernels through the ordered key (unblocks #1029) #1095.
  • TensorView.prepack(order, scope, sink) — the conversion as a visible adapter: allocates in the caller's scope, emits AdapterInserted with its byte count, and returns this when nothing has to move. Named as the conversion it is, not as a transpose.
  • RelayoutedBlockDecoder keeps rule 4 true across a relayout. The M1 PackedBlockDecoder decodes from the TensorData it wraps rather than from the storage it is handed, so without this a prepacked view would silently decode the old bytes — found by a test, not by reading.
  • docs/design/memory/packed-weight-layout.md — the normative contract: the two orders, where the order lives, how to convert, five rules, and the semver policy that packedData byte semantics are public API (a change is minor/major, never a patch — that is how a byte-layout change shipped as a green-CI hotfix once already).
  • The stale kdocs the census names are corrected: Q5_0/Q5_1TensorData claimed input-block-major bytes and a shape-swap transpose, neither true since 0.40.1; two kernel SPI kdocs claimed byte-identity with a storage type that holds the other order.

Acceptance

BlockOrderTest uses a three-block-wide weight throughout, because at one block per row the two orders coincide and every such test passes vacuously. It pins:

  • a view built from file bytes is ROW_MAJOR, and says so in toString();
  • prepacking moves block (o, b) to flat index b * rows + o — asserted on the bytes;
  • the decoded matrix is unchanged element by element (the relayout is not allowed to change what the tensor means);
  • going there and back is the identity — the double-transpose hazard Packed-quant byte-order (block layout) is an unwritten, contradictory contract across the engine and downstream converters #973 lists, which nothing detected before, is now a test;
  • the order survives narrow / transpose / unsqueeze;
  • a same-order prepack copies nothing and reports nothing;
  • a dense view refuses to be prepacked.

Gate

scripts/pr-gate.sh — all legs passed. --golden passed on JVM and Kotlin/Native: no decoder or kernel moved a bit.

Scope

This is slice 1 of 5. The rest of #973 is tracked as #1095 (bridge the packed SPI kernels through the ordered key), #1096 (a weight-transposing matmul primitive so ops.transpose stops being a per-forward O(bytes) copy of a lie), #1097 (engine-owned prepacking + cross-repo contract fixtures) and #1098 (normalize weight orientation at the load boundary).

Keeps develop green by

Everything is additive except the LayoutClass enum split, which is @ExperimentalMemoryApi, introduced in M1 by this same work, and has six in-repo usages — all updated. No packed byte layout changes: the golden gate proves it.

🤖 Generated with Claude Code

…le prepack adapter, the normative doc

Closes #1094, the foundational slice of #973.

#973's census found seven mutually contradicting statements of one contract
inside this repository, two of which had already shipped wrong numbers
(#968, #971). The contract is: a packed weight's blocks are in one of two
orders, and which one is a property of the value.

- `BlockOrder { ROW_MAJOR, INPUT_BLOCK_MAJOR }` on `Layout`, defaulting to
  ROW_MAJOR — what a file holds. The order is expressed **in the strides**
  (input-block-major is `[1, out]` over the block grid, not `[blocksPerRow,
  1]`), so narrow, transpose, get and toFloatArray keep working unchanged on
  a view in either order rather than growing a branch each.
- `LayoutClass.BLOCKED` splits into `BLOCKED_ROW_MAJOR` and
  `BLOCKED_INPUT_MAJOR`, so a kernel *declares* the order it reads in its
  `KernelKey` and the dispatcher can relayout instead of the caller having
  to know. This is what #1029 was waiting for.
- `TensorView.prepack(order, scope, sink)`: the conversion as a visible
  adapter — allocates in the caller's scope, emits `AdapterInserted` with
  the byte count, returns `this` when nothing has to move. Named as the
  conversion it is, not as a transpose.
- `RelayoutedBlockDecoder` keeps rule 4 true across a relayout: the M1
  `PackedBlockDecoder` decodes from the `TensorData` it wraps rather than
  from the storage it is handed, so a prepacked view maps the block index
  back instead of silently decoding the old bytes.
- `docs/design/memory/packed-weight-layout.md`: the normative contract, with
  the five rules and the semver policy that `packedData` byte semantics are
  public API. The stale kdocs the census names are corrected to point at it
  — `Q5_0/Q5_1TensorData` claimed input-block-major bytes and a shape-swap
  transpose, neither of which has been true since 0.40.1, and two kernel SPI
  kdocs claimed byte-identity with a storage type that holds the other order.

`BlockOrderTest` uses a three-block-wide weight throughout, because the two
orders coincide at one block per row — the case that hides the bug. It pins
that prepacking moves the bytes to `b * rows + o`, that the decoded matrix is
unchanged, that going there and back is the identity (the double-transpose
hazard, which nothing detected before), that the order survives narrow /
transpose / unsqueeze, and that a same-order prepack copies nothing and
reports nothing.

Gate: scripts/pr-gate.sh — all legs passed; --golden passed on JVM and
linuxX64 (no decoder or kernel moved a bit).

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-1099 artifact to view the complete documentation locally.

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

@michalharakal
michalharakal merged commit b4e36ce into develop Aug 24, 2026
18 checks passed
@michalharakal
michalharakal deleted the feature/1094-block-order branch August 24, 2026 16:21
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.1] Block order is part of the layout, not a guess — BlockOrder, a visible prepack adapter, and the normative doc

1 participant