feat(memory): int8 activation requant adapter and the reference bitnet_gemv (SKEEP-003 P6, S2.8) - #1091
Merged
Conversation
…t_gemv Closes #1040 (SKEEP-003 P6, S2.8, proposal §5.3; M2-F3). A ternary weight is only cheap if the other operand is cheap too. #1033 gave the ternary encodings an `activation` hint; nothing produced that activation and nothing consumed it. - `TensorEncoding.DENSE_I8_ABSMAX`: int8 codes with a per-token absmax scale — `rows * cols` codes then `rows` FP32 scales. Deliberately not parameterized by the row length, because kernel selection keys on the Format and a per-shape encoding would make every hidden size its own key; the row count comes from the view's shape. `BlockSpec.INT8_ACTIVATION` now names this instead of the `Dense(1)` placeholder. - `I8Absmax.requantize(view, scope, sink)`: the adapter. Per-row absmax, scale = absmax/127, a zero row keeps scale 0 instead of dividing by it. It allocates in the caller's scope and emits `AdapterInserted` — the cost is real every step, so it is visible rather than hidden in a kernel. The view decodes through `get()` like every other packed format (rule 4). - `BitNetGemvKernel`: the reference `bitnet_gemv` — no multiplies in the inner loop, since a ternary weight is an add, a subtract, or nothing. Scales are factored out per block exactly as a NEON kernel (#1041) will factor them, and the weight's codes are decoded once per call rather than once per row. Registered for TQ1_0, TQ2_0 and BitNet b1.58. - `KernelDispatch` reads the weight encoding's `activation` hint: when a kernel exists for the requantized pair it inserts the adapter and selects it. Only ternary formats ask, so nothing else changes path. - `TernaryBlockDecoder(encoding, elementCount)` completes #1033's per-tensor case: BitNet b1.58's single scale covers the whole tensor, so the tensor is one block. Acceptance: the kernel matches the decoded definition for all three encodings within 1e-3 relative; an all-zero weight yields exactly zero; requantization stays within one scale step per element; the dispatcher selects `bitnet_gemv/reference` for a ternary weight and emits exactly one visible `requantize-i8-absmax` adapter, while a dense weight gets none. The adapter's price is asserted: `rows * cols + rows * 4` bytes — 4100 for one token of a 4096-wide model, the ≈ 4 KB/step §5.3 predicts. One test assertion is relative rather than exact because Kotlin/JS computes Float arithmetic in double precision, so `amax / 127` can differ in the last bit between the test and the implementation. Gate: scripts/pr-gate.sh — all legs passed (JVM, JS, Wasm, linuxX64). 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 #1040 · Phase P6 · Milestone M2 · PRD M2-F3 · Proposal §5.3
The other half of a ternary weight
A 1.58-bit weight is only cheap if the operand it meets is cheap too. #1033 gave the ternary encodings an
activationhint; nothing produced that activation and nothing consumed it. This closes both ends.TensorEncoding.DENSE_I8_ABSMAX— int8 codes with a per-token absmax scale:rows * colscodes, thenrowsFP32 scales. Deliberately not parameterized by the row length: kernel selection keys on theFormat, and a per-shape encoding would make every hidden size its own key. The row count comes from the view's shape, where it belongs.BlockSpec.INT8_ACTIVATIONnow names this rather than theDense(1)placeholder #1033 left.I8Absmax.requantize(view, scope, sink)— the adapter. Per-row absmax,scale = absmax / 127, and a row of zeros keeps scale0instead of dividing by it. It allocates in the caller's scope and emitsAdapterInserted, because this cost is paid every step and the whole point of §5.1 is that such costs are visible rather than buried in a kernel. The resulting view decodes throughget()like every other packed format (rule 4).BitNetGemvKernel— the referencebitnet_gemv, with no multiplies in the inner loop: a ternary weight is an add, a subtract, or nothing at all. Scales are factored out per block exactly as a NEON kernel (#1041) will factor them, and the weight's codes are decoded once per call rather than once per row — for a decode step that is the difference betweenO(rows·n·k)andO(n·k)decodes. Registered forTQ1_0,TQ2_0and BitNet b1.58.Dispatch reads the weight encoding's
activationhint: when a kernel exists for the requantized pair,KernelDispatchinserts the adapter and selects it. Only ternary formats ask for it, so nothing else changes path — there is a test asserting a dense weight gets no requantization.Also:
TernaryBlockDecoder(encoding, elementCount)finishes #1033's per-tensor case — BitNet b1.58's single scale covers the whole tensor, so the tensor is one block. Without it a BitNet weight could be encoded but not viewed.Acceptance
Σ decoded_activation × decoded_weightwithin 1e-3 relative, using the fixtures [S2.7] P6: ternary encodingsTQ1_0,TQ2_0,BITNET_B1_58with block spec +activationhint; reference decoder and parity fixtures generated from the descriptor #1033's codec generates.0f, which is the property the skip-the-zeros loop must preserve.bitnet_gemv/referenceand emits exactly onerequantize-i8-absmaxadapter; a dense weight emits none.rows * cols + rows * 4bytes — 4100 bytes for one token of a 4096-wide model, the ≈ 4 KB/step §5.3 predicts.One assertion is relative rather than exact: Kotlin/JS computes
Floatarithmetic in double precision, soamax / 127can differ in the last bit between the test and the implementation. Everything else is exact.Gate
scripts/pr-gate.sh— all legs passed. The kernel and adapter tests run on JVM, JS (browser), Wasm and Kotlin/Native.Keeps develop green by
Everything is additive: a new encoding, a new adapter, a new kernel, and one new dispatch branch that only fires when the weight's encoding asks for a different activation format — which today only the ternary encodings do. The
BlockSpec.INT8_ACTIVATIONconstant changed from a placeholder to the real format; it is@ExperimentalMemoryApiand its only consumer is this slice.🤖 Generated with Claude Code