Skip to content

feat(mixed): pin MTL5 v5.11.0 and add accumulator='i32' - #95

Merged
Ravenwater merged 1 commit into
mainfrom
feat/mtl5-v5.11.0-integer-accumulator
Aug 28, 2026
Merged

feat(mixed): pin MTL5 v5.11.0 and add accumulator='i32'#95
Ravenwater merged 1 commit into
mainfrom
feat/mtl5-v5.11.0-integer-accumulator

Conversation

@Ravenwater

Copy link
Copy Markdown
Contributor

Closes #88 phase 2, and bumps the pin to v5.11.0 (version 5.10.35.11.0, since minor tracks MTL5). Clean from-scratch build: 1433 passed, 3 skipped (from 1411).

The narrow integer types phase 1 registered can now compute:

a = np.random.randint(0, 256, 4096, dtype=np.uint8)   # activations
w = np.random.randint(-128, 128, 4096, dtype=np.int8) # weights
mtl5.mixed.dot(a, w, accumulator="i32")

v5.11.0 routes dot<int32_t> over 8- and 16-bit operands onto the hardware widening multiply-accumulate — vpmaddwd / vpdpbusd on x86, SMLAL / SDOT on NEON — and int32 is what those instructions accumulate into.

Every signedness pairing, in either order

u8 × i8 is VNNI's native shape on x86 and what quantized inference is written in; ARM implements the symmetric pairings first. A dot product is symmetric, so MTL5 swaps the operands onto whatever the machine has.

My first pass registered same-type overloads only, which put the headline pairing out of reach — caught by probing the surface rather than by a compile error, since u8 × i8 simply didn't resolve. simd::reduce_dot_widen does reject (int8, uint8) one level down, but that restriction is the kernel's; re-exposing it would refuse a call the library can serve.

The overflow contract is in the API, not a footnote

Products are always exact. The sum wraps, and headroom goes as operand magnitude, not vector length — about 2^(31-2b) terms at b bits.

headroom at full range
i8 × i8 131071 terms
i16 × i16 2 — one product already uses 2³⁰ of the int32 range

Five orders of magnitude, and the reason quantized inference is 8-bit. Measured, not quoted: my first draft said "roughly 3 terms" for i16, taken from upstream prose; measuring gave 2. The wrap is two's complement, hence bit-identical across lane counts, backends and thread partitions.

Two deliberate guardrails

  • accumulator= is required for these dtypes, unlike every other. Omitting it means element precision — exactly the wrapping phase 1 refused to expose. Silently redefining None for three dtypes alone would be worse than asking.
  • result='element' is refused. Rounding an int32 sum back to 8 bits re-introduces the wrap the accumulator exists to avoid.

AccKind::I32 is guarded in dispatch_acc with if constexpr exactly as Quire is, so mtl::dot<int32_t> over a posit vector is never instantiated. dispatch_acc returns double, which is lossless here — int32 is exactly representable in a 53-bit significand. That looks like a narrowing bug and isn't, so it's commented.

mtl5.dtypes() is deliberately unchanged

I added the integer types to it and broke four existing tests. They were right: dtypes()' contract is the set convert() can target, and the suite parametrizes over it and converts into every entry.

convert() cannot target an integer — re-quantizing reals into 8 bits is a quantization scheme (scale, zero point, rounding mode), not a cast, and a naive version would silently clip everything outside [-128, 127]. That's precisely the class of footgun this session has been removing. accumulators('i8') answers for them instead, and dtypes()' docstring now explains the omission rather than leaving it to look like one.

norm/frobenius_norm also stay unregistered: two_norm takes sqrt of the accumulated sum, and the API has no way to say "accumulate in int32, deliver a real square root".

Why the pin is load-bearing

Not merely "where the feature is". batch<int32_t> does not exist before v5.11.0, so the kernels are absent — and detail/wrapping_arithmetic.hpp is new in it, without which the generic integer loops are UB on overflow rather than the documented modular wrap. For these operand widths overflow is the normal regime, not an edge case.

Note on what a wheel gets

A released wheel builds at the x86-64 baseline, so it gets int8's bandwidth win (one byte per element against float64's eight) but not the VNNI instruction. build_info()["build_isa"] reports which you have. Documented in the README section rather than left for a disappointing benchmark to reveal.

🤖 Generated with Claude Code

Closes #88 phase 2, and bumps the pin to v5.11.0 (version 5.10.3 ->
5.11.0, since minor tracks MTL5).

The narrow integer element types phase 1 registered can now compute.
v5.11.0 routes dot<int32_t> over 8- and 16-bit operands onto the hardware
widening multiply-accumulate -- vpmaddwd / vpdpbusd on x86, SMLAL / SDOT
on NEON -- and int32 is what those instructions accumulate into.

    mtl5.mixed.dot(activations_u8, weights_i8, accumulator="i32")

Every signedness pairing is accepted, in either order. u8 x i8 is VNNI's
native shape on x86 and what quantized inference is written in; ARM does
the symmetric ones first. A dot is symmetric, so MTL5 swaps operands onto
whatever the machine has. simd::reduce_dot_widen rejects (int8, uint8)
one level down, but that restriction is the kernel's -- re-exposing it
would refuse a call the library can serve. A same-type-only surface would
have put the whole point of the 8-bit path out of reach.

The overflow contract is in the API rather than a footnote. Products are
exact; the sum wraps, and headroom goes as operand MAGNITUDE, not vector
length -- about 2^(31-2b) terms at b bits. Measured at full range rather
than quoted: one i16 x i16 product uses 2^30 of the int32 range so TWO
overflow it, while i8 x i8 holds 131071 terms. (My first draft said
"roughly 3 terms" for i16, taken from upstream prose; measuring gave 2.)
The wrap is two's complement, hence bit-identical across lane counts,
backends and thread partitions.

Two guardrails. accumulator= is REQUIRED for these dtypes, unlike every
other: omitting it means element precision, which is exactly the wrapping
phase 1 refused to expose, and redefining None for three dtypes alone
would be worse than asking. result='element' is REFUSED, since rounding
an int32 sum back to 8 bits re-introduces the wrap.

AccKind::I32 is guarded in dispatch_acc with if-constexpr exactly as
Quire is, so mtl::dot<int32_t> over a posit vector is never instantiated.
dispatch_acc returns double, which is lossless here: int32 is exactly
representable in a 53-bit significand. That looks like a narrowing bug
and is not, so it is commented.

mtl5.dtypes() is deliberately unchanged. Adding the integer types broke
four existing tests, and they were right: its contract is the set
convert() can TARGET, and the suite parametrizes over it and converts
into every entry. convert() cannot target an integer -- re-quantizing
reals into 8 bits is a quantization scheme, not a cast, and a naive
version would silently clip outside [-128, 127]. accumulators('i8')
answers for them instead, and dtypes()' docstring now says why.

norm and frobenius_norm stay unregistered for these types: two_norm takes
sqrt of the accumulated sum, and the API cannot say "accumulate in int32,
deliver a real square root".

The pin is what makes this sound rather than merely available.
batch<int32_t> does not exist before v5.11.0, and
detail/wrapping_arithmetic.hpp is new in it -- without which the generic
integer loops are UB on overflow rather than the documented modular wrap.
For these operand widths overflow is the normal regime.

Clean from-scratch build against v5.11.0: 1433 passed, 3 skipped
(from 1411).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 28, 2026

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

On-demand reviews are free for the next 23 days. After that, they cost $0.25 per reviewed file.

Or wait 16 minutes for your next included review.

View limit details

Limit details: You’ve used the included review currently available. Your 72 included PR review attempts over the past 7 days set your current allowance at 1 review per hour.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6716c7c5-b2a3-46c8-9aca-3075c4d0885b

📥 Commits

Reviewing files that changed from the base of the PR and between 78d739a and f82ba8a.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • CMakeLists.txt
  • README.md
  • pyproject.toml
  • python/src/mtl5_mixed_precision.cpp
  • python/src/mtl5_types.hpp
  • tests/test_mixed_precision.py

Comment @coderabbitai help to get the list of available commands.

@Ravenwater
Ravenwater merged commit da713af into main Aug 28, 2026
14 checks passed
@Ravenwater
Ravenwater deleted the feat/mtl5-v5.11.0-integer-accumulator branch August 28, 2026 16:22
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.

Expose MTL5 v5.11.0's integer/quantized kernels (int8/int16 dot, integer GEMM, VNNI)

1 participant