Skip to content

feat(core): portable seeded rotations and a frozen fast-mode scale table - #199

Merged
gouyt13 merged 8 commits into
VectorDB-NTU:mainfrom
TamimEhsan:core-quant-cpp-preq
Nov 1, 2026
Merged

gouyt13 merged 8 commits into
VectorDB-NTU:mainfrom
TamimEhsan:core-quant-cpp-preq

Conversation

@TamimEhsan

@TamimEhsan TamimEhsan commented Oct 31, 2026 •

Copy link
Copy Markdown
Member

Summary

These are the C++ prerequisites for the provisional Python quantization module (rabitqlib.quant). The goal is that the same arguments give the same rotation and the same fast-mode scale on every platform, so that codes and saved bytes produced by the module can be reproduced. Both changes are additive. C++ signatures, defaults and saved-index formats are unchanged.

Promoting combined_code and code_factors to a public API will ship with the Python module or in a later PR.

Seeded rotations (fix(rotator))

  • FHT/Kac signs are now portable. Each sign byte is the top byte of an std::mt19937 output (gen() >> 24). This replaces std::uniform_int_distribution<int>(0, 255), whose output the C++ standard does not specify. The new mapping gives the same bytes as libstdc++'s distribution (checked on 100,000 draws, 0 mismatches), so existing Linux rotations are unchanged. Every standard library now agrees.
  • MatrixRotator uses the seed. Before, it called random_gaussian_matrix without a seed, which fell back to std::random_device, so choose_rotator's seed argument was ignored for matrix rotations. MatrixRotator now takes a seed (defaulting to std::random_device, as FhtKacRotator does), and choose_rotator passes it through. A seeded matrix is reproducible only with the same standard library and build, because std::normal_distribution and QR rounding differ, so the docs still say to persist matrix rotators with save().

Frozen fast-mode scale table (feat(quant))

faster_config computed its scale (t_const) with std::normal_distribution samples on every call. That value differs between standard libraries by several percent, and between FMA and non-FMA builds in the last bits. It also cost time at every HNSW construction and load (13 ms at 768 dimensions, 50 ms at 3072; Xeon Gold 6418H, GCC 14.3, -O3).

  • src/core/fast_scale_table.cpp holds the scale for every padded dimension that is a multiple of 32 up to 65536 and every ex_bits from 1 to 8 (total bits 2–9). Each value is stored as an exact hexadecimal literal. The table is 2048 × 8 doubles (128 KiB).
  • The values are the ones that portable x86-64 Linux builds already compute: libstdc++, baseline SSE2, no FMA. Results on those builds are therefore unchanged.
  • faster_config reads the table through frozen_const_scaling_factor(padded_dim, ex_bits), which returns std::optional<double>. Dimensions outside the table fall back to the existing computation.
  • scripts/generate_fast_scale_table.cpp records how the table was generated. Its guards refuse to build with libraries other than libstdc++, or with AVX or FMA. The table is frozen data: never regenerate or edit it. A different table would be a new, separately named table.

Behaviour changes

  • MSVC and libc++: a seeded FHT/Kac rotator gives a different rotation than before (now the same one as Linux). Saved indexes store their sign bytes, so loading them is unaffected.
  • MSVC, libc++ and native FMA builds: faster_config now returns the table value, which changes fast-mode codes and HNSW query quantization slightly, toward the Linux results.
  • SymQG with MatrixRotator: the index now passes its seed (default 42), so the rotation is deterministic where it used to be random. IVF passes no seed and stays random.
  • HNSW construction and load: faster_config no longer computes the scale at padded dimensions in the table.

Docs

  • docs/docs/rabitq/rotator.md describes the portable FHT/Kac seeding and the seeded matrix rotator.
  • docs/docs/rabitq/quantizer.md describes the frozen table and how much the fallback can differ between platforms.

Testing

  • FhtRotatorTest.SeededSignFlipsAreFrozen: golden sign bytes (FNV-1a hash and an 8-byte prefix) for four (dim, padded_dim, seed) cases, captured from the libstdc++ path this replaces.
  • MatrixRotatorFactoryTest.SeedReproducesTheMatrix: the same seed gives the same matrix and a different seed a different one, through both choose_rotator and the constructor.
  • FastScaleTableTest.FasterConfigReadsTheFrozenTable: pinned table entries; one bit keeps the default configuration.
  • FastScaleTableTest.FrozenValuesMatchTheComputation: the table equals the computation exactly in the generating configuration (libstdc++, x86-64, no AVX or FMA). Elsewhere it checks the value is plausible within 10%. I measured a 1.8–4.1% spread across different sample streams, which is what MSVC and libc++ produce.
  • FastScaleTableTest.DimensionsOutsideTheTableFallBackToTheComputation: table bounds and the fallback.

Results:

  • Release build with RABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF: ctest --no-tests=error passes 439 of 439 tests. One test is skipped, as on main.
  • ./scripts/check-format.sh and git diff --check: clean.
  • python -m mkdocs build --strict --config-file docs/mkdocs.yml: clean.
  • The generator compiles at baseline x86-64 and refuses -mavx.

@github-actions github-actions Bot added the release:added Release category managed from the PR title label Oct 31, 2026
@TamimEhsan
TamimEhsan marked this pull request as ready for review October 31, 2026 15:17
@TamimEhsan
TamimEhsan requested a review from gouyt13 as a code owner October 31, 2026 15:17
@gouyt13 gouyt13 self-assigned this Oct 31, 2026
@gouyt13

gouyt13 commented Oct 31, 2026

Copy link
Copy Markdown
Member

Thanks for the PR! The table matches the computed values on our side, and the seeded FHT rotations look stable across standard-library implementations. I'm merging a docs update first, so this may need a rebase afterwards. I'll follow up with a full review after that.

FrozenValuesMatchTheComputation compares only 32 of the 16384 entries,
so an edit elsewhere in the table would pass on every platform. Pin all
entries with an FNV-1a hash of their IEEE-754 bits, read through
frozen_const_scaling_factor so the index mapping is covered too.

Also update the QG seed docs: the graph seed now seeds MatrixRotator.
The libstdc++ uniform_int_distribution bytes match the new FHT/Kac
mapping only from GCC 11, which adopted Lemire's algorithm; older
libstdc++ occasionally drew different bytes.

The frozen scale table forbids changing its values, not editing the
lookup code beside them. List the table in the development code map
and the quantization audit list.
The rotator and scale-table goldens hashed from 1469598103934665603,
which drops the last digit of the FNV-1a 64-bit offset basis
14695981039346656037, so standard FNV-1a tools could not reproduce them.
Recompute the goldens; the hashed bytes are unchanged.
Say that faster_config computes the factor for dimensions outside the
table, and that math libraries also change its last bits.
@gouyt13
gouyt13 merged commit cc2e4f2 into VectorDB-NTU:main Nov 1, 2026
48 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:added Release category managed from the PR title

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants