feat(mixed): pin MTL5 v5.11.0 and add accumulator='i32' - #95
Merged
Conversation
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>
|
Warning Review limit reached
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 detailsLimit 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. Review configuration: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
Comment |
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 #88 phase 2, and bumps the pin to v5.11.0 (version
5.10.3→5.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:
v5.11.0 routes
dot<int32_t>over 8- and 16-bit operands onto the hardware widening multiply-accumulate —vpmaddwd/vpdpbusdon x86,SMLAL/SDOTon NEON — and int32 is what those instructions accumulate into.Every signedness pairing, in either order
u8 × i8is 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 × i8simply didn't resolve.simd::reduce_dot_widendoes 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 atbbits.i8 × i8i16 × i16Five 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 redefiningNonefor 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::I32is guarded indispatch_accwithif constexprexactly asQuireis, somtl::dot<int32_t>over a posit vector is never instantiated.dispatch_accreturnsdouble, 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 unchangedI added the integer types to it and broke four existing tests. They were right:
dtypes()' contract is the setconvert()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, anddtypes()' docstring now explains the omission rather than leaving it to look like one.norm/frobenius_normalso stay unregistered:two_normtakessqrtof 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 — anddetail/wrapping_arithmetic.hppis 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