Repository navigation
simd: TFLite/gemmlowp fixed-point requantization, bit-exact on every backend - #375
Conversation
Adds the integer core every TFLite int8 kernel ends in, which ndarray
lacked: SaturatingRoundingDoublingHighMul (i32, i16), RoundingDivideByPOT,
MultiplyByQuantizedMultiplier (double rounding = TFLM default build, plus
the single-rounding and int64 variants), QuantizeMultiplier, and the
per-channel Conv/FC epilogue requantize_i32_to_i8.
- src/simd_fixedpoint.rs: scalar reference, ports of TFLM fixedpoint.h
319-366, common.cc 30-110, quantization_util.cc 54-106.
- I32x16::{saturating_rounding_doubling_high_mul, rounding_divide_by_pot,
multiply_by_quantized_multiplier} on every backend: AVX-512
(_mm512_mul_epi32 even/odd, sllv/srav), AVX2 (_mm256_mul_epi32 per
half), NEON (vqrdmulhq_s32 + gemmlowp rounding shift), scalar, wasm and
nightly per lane. Per-lane multiplier and shift, so per-channel and
per-tensor share one path.
- tests/tflm_requant_golden.rs: 2392 cases generated by
tests/data/tflm_requant_oracle.cc compiled against the real TFLM
sources (AdaWorldAPI/tflite-micro 2d8adcc).
- simd-masking-parity group 0x12xx with an independent i128 oracle.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G22yT6htkcdyXsihxxXdrg
AVX1 has no 256-bit integer multiply; the three I32x16 methods go lane by lane through simd_fixedpoint there. avx-qemu parity arm: PASS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G22yT6htkcdyXsihxxXdrg
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars.
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Summary
Every TFLite int8 kernel (Conv, FullyConnected, Add, …) ends by requantizing its int32 accumulator through one fixed-point chain. ndarray did not have that chain, so no int8 TFLite kernel could be reproduced bit for bit. This PR adds it, ported from TFLite-micro (
AdaWorldAPI/tflite-micro@2d8adcc). It follows TFLM's default build (TFLITE_SINGLE_ROUNDINGundefined, i.e. double rounding, as in gemmlowp).Scalar reference (
src/simd_fixedpoint.rs, re-exported fromndarray::simd):srdhm_i32,srdhm_i16(SaturatingRoundingDoublingHighMul)fixedpoint.h:319-353rounding_divide_by_pot_i32fixedpoint.h:356-366multiply_by_quantized_multiplier_i32(double rounding, the default)common.cc:80-87multiply_by_quantized_multiplier_single_rounding_i32common.cc:33-46multiply_by_quantized_multiplier_i64(identical in both modes)common.cc:49-75/90-110quantize_multiplier,quantize_multiplier_single_rounding(std)quantization_util.cc:54-106requantize_i32_to_i8: the per-channel / per-tensor Conv/FC epilogue (scale, plusoutput_offset, then clamp)integer_ops/fully_connected.h:69-76Vector form:
I32x16::{saturating_rounding_doubling_high_mul, rounding_divide_by_pot, multiply_by_quantized_multiplier}. Multiplier, shift and exponent are per lane, so per-channel and per-tensor quantization take the same path. Backends are selected at compile time only:_mm512_mul_epi32on even and odd lanes, then(p + 2^30) >> 31(equal to C++'s truncation after the sign-dependent nudge, for both product signs);MIN·MINblended toMAX;sllv/sravfor the shiftsvqrdmulhq_s32, plus gemmlowp's rounding shift (vrshlq_s32with sign fix-up)Verification
Byte parity against the real TFLite C++.
tests/data/tflm_requant_oracle.cc, compiled against TFLM'sfixedpoint.h,common.ccandquantization_util.cc, generated 2392 cases intotests/data/tflm_requant_golden.txt.tests/tflm_requant_golden.rschecks the scalar chain,quantize_multiplierand the three vector methods against them. It passes on AVX-512 and AVX2 (config-v3).Parity group
0x12xxincrates/simd-masking-parity, with an independent i128 reference that does not call the scalar functions.MIN·MINmust be hit, and ties must occur with both signs.Disable runs, each failing:
MIN·MINsaturation[MIN, MIN] → MAX0x1211>>srdhm_i32rdpotsimd_fixedpointunit tests: 12 pass. One worker-written expectation was wrong: it expected shift −33 for1e-10, but TFLM itself flushes that value to(0, 0), and the golden data confirms it. The test now pins TFLM's value, along with the exact value for 3.7.Clippy
--all-targets -D warningsis clean. No-std--no-default-featureslib clippy is clean.--all-targetswithout std fails onbenches/higher-order.rs(3×never used); that fails identically onmaster, so it is not from this PR.Every build in this PR ran with
CARGO_PROFILE_DEV_DEBUG=0,CARGO_PROFILE_TEST_DEBUG=0andCARGO_INCREMENTAL=0.Not covered
srdhm_i16exists only as a scalar function; there is noI16x32method.int8_gemm_i32remains u8 × i8.Generated by Claude Code