feat(io): weight orientation at the load boundary, and a guard that refuses a transposed label (#973.5) - #1103
Merged
Conversation
…efuses a transposed label Closes #1098 (#973 census contradiction #6). GGUF writes dimensions in `ne` order, so a weight the engine calls `[out, in]` arrives from the streaming loader labelled `[in, out]` — while its bytes are already `[out, in]` row-major. Only the label is wrong, and the label is what the block relayout reads: driven by `[in, out]` it permutes the wrong grid, or refuses because `out` is not a multiple of the block size. Both failures are in the census. - `WeightOrientation { AS_STORED, OUT_IN }` on `StreamingGgufParametersLoader`. `OUT_IN` reverses 2-D weights at the boundary and touches nothing else — not the bytes, not 1-D tensors, which have no orientation to get wrong. Defaults to `AS_STORED`, today's behaviour, because reversing shapes changes what every consumer sees. - `PackedWeights.requireOutIn(rows, inputDim, encoding)`, run by `prepackForMatmul`: refuses a weight that looks transposed instead of computing a wrong permutation from it, and names the fix. It is a heuristic and says so — it fires when the *first* dimension is block-aligned and the second is not, which is exactly what `ne` order produces, and stays quiet when both are aligned and it cannot tell. A false refusal would be worse than none. - The normative doc gains the orientation section, including the fact that this repository's two GGUF readers disagree about it today: the legacy `GGUFReader` reverses dimensions, the streaming one does not. `WeightOrientation` is how a caller states which it wants. `SyntheticGguf` can now write multi-dimensional tensors, so the test writes a real `ne = [64, 4]` weight and asserts that `AS_STORED` reports `[64, 4]`, `OUT_IN` reports `[4, 64]`, and the decoded values are identical either way. Gate: scripts/pr-gate.sh — all legs 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 #1098 · #973 census contradiction #6
The mismatch
GGUF writes dimensions in
neorder — fastest-varying first — so a weight the engine calls[out, in]arrives from the streaming loader labelled[in, out], while its bytes are already[out, in]row-major. Only the label is wrong.The label is what the block relayout reads. Driven by
[in, out]it permutes the wrong grid, orrequire-fails becauseoutis not a multiple of the block size. Both failures are named in the census; neither was detectable from the value.Two changes, both opt-in
WeightOrientation { AS_STORED, OUT_IN }onStreamingGgufParametersLoader.OUT_INreverses 2-D weights at the boundary and touches nothing else — not the bytes, and not 1-D tensors, which have no orientation to get wrong. It defaults toAS_STORED(today's behaviour) because reversing shapes changes what every consumer sees; new code should ask forOUT_IN, and making it the default is a follow-up once downstream has moved.PackedWeights.requireOutIn(rows, inputDim, encoding), run byprepackForMatmul: refuses a weight that looks transposed rather than computing a wrong permutation from it, with a message that names the fix.It is a heuristic and says so in its own kdoc. It fires when the first dimension is block-aligned and the second is not — exactly what
neorder produces for a real weight — and stays quiet when both are aligned and it genuinely cannot tell. A false refusal on an ambiguous shape would be worse than no check, so the guard is deliberately one-sided.Something the census implies and the doc now records
The two GGUF readers in this repository disagree about this today: the legacy
GGUFReaderreverses dimensions (npDims = dims.reversed()), the streaming reader does not. That is a live inconsistency in one module, andWeightOrientationis how a caller states which behaviour it wants instead of discovering it.Acceptance
SyntheticGgufcan now write multi-dimensional tensors, so the test writes a realne = [64, 4]weight:AS_STOREDreports[64, 4],OUT_INreports[4, 64], and the decoded values are identical either way — the claim that only the label changes.[128, 3]Q8_0 with a message containing both "looks like [in, out]" and theWeightOrientation.OUT_INfix, accepts[3, 128], stays quiet on[64, 128]and[128, 128], refuses an input dimension that is unaligned whichever way round it is, and ignores dense encodings entirely.Gate
scripts/pr-gate.sh— all legs passed.Keeps develop green by
A new enum with a default equal to today's behaviour, and a guard that only fires on the shape that was already broken. No existing call site changes.
#973 after this
matmulWTprimitive; packedops.transposebecomes a loud error#1096 is the only structural change left — and the only one that needs downstream coordination, since it deprecates a public op and removes the per-forward O(bytes) copy
Linear.onForwardcurrently pays.🤖 Generated with Claude Code