Skip to content

simd: TFLite/gemmlowp fixed-point requantization, bit-exact on every backend - #375

Merged
AdaWorldAPI merged 2 commits into
masterfrom
claude/requant-fixedpoint
Oct 24, 2026
Merged

AdaWorldAPI merged 2 commits into
masterfrom
claude/requant-fixedpoint

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

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_ROUNDING undefined, i.e. double rounding, as in gemmlowp).

Scalar reference (src/simd_fixedpoint.rs, re-exported from ndarray::simd):

function TFLM source
srdhm_i32, srdhm_i16 (SaturatingRoundingDoublingHighMul) fixedpoint.h:319-353
rounding_divide_by_pot_i32 fixedpoint.h:356-366
multiply_by_quantized_multiplier_i32 (double rounding, the default) common.cc:80-87
multiply_by_quantized_multiplier_single_rounding_i32 common.cc:33-46
multiply_by_quantized_multiplier_i64 (identical in both modes) common.cc:49-75 / 90-110
quantize_multiplier, quantize_multiplier_single_rounding (std) quantization_util.cc:54-106
requantize_i32_to_i8: the per-channel / per-tensor Conv/FC epilogue (scale, plus output_offset, then clamp) integer_ops/fully_connected.h:69-76

Vector 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:

backend implementation
AVX-512 _mm512_mul_epi32 on even and odd lanes, then (p + 2^30) >> 31 (equal to C++'s truncation after the sign-dependent nudge, for both product signs); MIN·MIN blended to MAX; sllv/srav for the shifts
AVX2 the same, per 256-bit half
AVX without AVX2 per lane through the scalar reference (no 256-bit integer multiply)
NEON vqrdmulhq_s32, plus gemmlowp's rounding shift (vrshlq_s32 with sign fix-up)
scalar, wasm, nightly per lane through the scalar reference

Verification

  • Byte parity against the real TFLite C++. tests/data/tflm_requant_oracle.cc, compiled against TFLM's fixedpoint.h, common.cc and quantization_util.cc, generated 2392 cases into tests/data/tflm_requant_golden.txt. tests/tflm_requant_golden.rs checks the scalar chain, quantize_multiplier and the three vector methods against them. It passes on AVX-512 and AVX2 (config-v3).

  • Parity group 0x12xx in crates/simd-masking-parity, with an independent i128 reference that does not call the scalar functions.

    • Anti-vacuity: rounding must differ from truncation, MIN·MIN must be hit, and ties must occur with both signs.
    • All 8 arms report 19 groups bit-identical: native v4, native v3, avx-qemu, v2-qemu, neon-qemu, wasm, wasm-scalar, nightly. Each arm's printed CPU flags were read.
  • Disable runs, each failing:

    removed fails at
    AVX-512 MIN·MIN saturation golden [MIN, MIN] → MAX
    AVX-512 sign term in the rounding threshold golden rounding row
    AVX2 sign term in the rounding threshold golden, v3 build
    NEON sign fix-up parity 0x1211
    scalar truncating division → >> golden srdhm_i32
    scalar sign term in the rounding threshold golden rdpot
  • simd_fixedpoint unit tests: 12 pass. One worker-written expectation was wrong: it expected shift −33 for 1e-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 warnings is clean. No-std --no-default-features lib clippy is clean. --all-targets without std fails on benches/higher-order.rs (3× never used); that fails identically on master, so it is not from this PR.

  • Every build in this PR ran with CARGO_PROFILE_DEV_DEBUG=0, CARGO_PROFILE_TEST_DEBUG=0 and CARGO_INCREMENTAL=0.

Not covered

  • No timing was measured.
  • srdhm_i16 exists only as a scalar function; there is no I16x32 method.
  • No i8 × i8 GEMM with zero points, and no LUT activations. Those are the next steps of the TFLite polyfill. The existing int8_gemm_i32 remains u8 × i8.
  • With a positive shift, the left shift wraps where TFLM asserts. That case is outside TFLM's documented contract, and the doc comments say so.

Generated by Claude Code

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
@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 600b4e44-21ee-4ad7-8f3e-1f5fa3c82a31
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Billing warning: we have not been able to collect payment for this subscription for more than 72 hours. Please update the payment method or pay any pending invoices in Billing to avoid service interruption.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@AdaWorldAPI
AdaWorldAPI marked this pull request as ready for review October 24, 2026 21:56
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T21:58:07.880525Z 22c54b5 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@AdaWorldAPI
AdaWorldAPI merged commit 84addbd into master Oct 24, 2026
29 checks passed
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.

2 participants