Skip to content

feat(memory): int8 activation requant adapter and the reference bitnet_gemv (SKEEP-003 P6, S2.8) - #1091

Merged
michalharakal merged 1 commit into
developfrom
feature/1040-requant-adapter-bitnet-ref
Aug 24, 2026
Merged

michalharakal merged 1 commit into
developfrom
feature/1040-requant-adapter-bitnet-ref

Conversation

@michalharakal

Copy link
Copy Markdown
Contributor

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 activation hint; 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 * cols codes, then rows FP32 scales. Deliberately not parameterized by the row length: 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, where it belongs. BlockSpec.INT8_ACTIVATION now names this rather than the Dense(1) placeholder #1033 left.

I8Absmax.requantize(view, scope, sink) — the adapter. Per-row absmax, scale = absmax / 127, and a row of zeros keeps scale 0 instead of dividing by it. It allocates in the caller's scope and emits AdapterInserted, 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 through get() like every other packed format (rule 4).

BitNetGemvKernel — the reference bitnet_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 between O(rows·n·k) and O(n·k) decodes. Registered for TQ1_0, TQ2_0 and BitNet b1.58.

Dispatch reads the weight encoding's activation hint: when a kernel exists for the requantized pair, KernelDispatch inserts 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

  • Parity against the definition for all three encodings: the kernel's output matches Σ decoded_activation × decoded_weight within 1e-3 relative, using the fixtures [S2.7] P6: ternary encodings TQ1_0, TQ2_0, BITNET_B1_58 with block spec + activation hint; reference decoder and parity fixtures generated from the descriptor #1033's codec generates.
  • Zero weights contribute nothing — an all-zero ternary weight yields exactly 0f, which is the property the skip-the-zeros loop must preserve.
  • Requantization is faithful: every element stays within one scale step, and a zero row survives.
  • Dispatch: a ternary weight selects bitnet_gemv/reference and emits exactly one requantize-i8-absmax adapter; a dense weight emits none.
  • The adapter's price is asserted, not estimated: rows * cols + rows * 4 bytes — 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 Float arithmetic in double precision, so amax / 127 can 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_ACTIVATION constant changed from a placeholder to the real format; it is @ExperimentalMemoryApi and its only consumer is this slice.

🤖 Generated with Claude Code

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

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

@michalharakal
michalharakal merged commit 48e6474 into develop Aug 24, 2026
20 checks passed
@michalharakal
michalharakal deleted the feature/1040-requant-adapter-bitnet-ref branch August 24, 2026 13:31
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.

[S2.8] P6: F32→I8 absmax requant adapter (Forward scope, traced) + reference bitnet_gemv

1 participant