Skip to content

[MOD-14956] Add SQ8 quantization support for HNSW index - #1007

Open
dor-forer wants to merge 4 commits into
mainfrom
dor-forer-MOD-14956-hnsw-sq8
Open

[MOD-14956] Add SQ8 quantization support for HNSW index#1007
dor-forer wants to merge 4 commits into
mainfrom
dor-forer-MOD-14956-hnsw-sq8

Conversation

@dor-forer

@dor-forer dor-forer commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Describe the changes in the pull request

Cherry-pick of ARM-software/VectorSimilarity-for-Arm#4 (head 125ea15d), plus a follow-up review pass. Fourth PR in the SQ8 series, after #999, #1000 and #1002.

Adds 8-bit scalar quantization (SQ8) to the standalone HNSW index:

  • VecSimQuantType plus quantType / quantParams on HNSWParams. Both fields are appended at the end of the struct and VecSimQuant_NONE is 0, so existing zero-initialized and designated-initializer construction is unaffected.
  • HNSWFactory can build SQ8 indexes for FLOAT32 and FLOAT16 data types with the L2 and IP metrics, wiring QuantPreprocessor and DistanceCalculatorWithNorm, and accounts for SQ8 in EstimateInitialSize and EstimateElementSize.
  • For SQ8, quantParams points to a float[dim] mean vector; a null pointer selects quantization without mean normalization.
  • New test_hnsw_sq8 unit-test target and suite (44 tests: FP32/FP16 x L2/IP).

SQ8 support for the tiered HNSW index, serialization and benchmarks is deferred to later PRs in the series.

Commit 1: the cherry-pick

ARM's four commits squashed into one, with no functional change to their code. Two Redis-side adjustments:

Commit 2: review follow-up

No behavioural change intended. Interface, single-source-of-truth and idiom fixes:

  • quantParams is now const void *. Every use reads it and two already cast to const float *. Layout-identical, so not an ABI break, and callers passing non-const still compile. Better to fix before the field ships and freezes.
  • The VecSimQuant_SQ8 comment claimed "with mean normalization", but mean normalization is optional and selected by quantParams. Reworded.
  • GetSQ8StoredDataSize re-derived the stored blob size that QuantPreprocessor's constructors already computed. Two formulas for one layout drift silently, which is the bug class fixed in MOD-15303. The formula now lives once as sq8::storage_bytes_count<Metric, WithNorm>(dim), beside the storage_metadata_count it builds on, and both callers use it. This is why the diff touches types/sq8.h and spaces/computer/preprocessors.h, two files beyond ARM's original four: having one shared definition is the entire point of the fix.
  • Restored the return NULL closing the SQ8 branch. Unreachable today, since the type and metric checks leave only FP32/FP16 x L2/IP, but without it adding a type or metric silently falls through and builds an unquantized index.
  • assert(ret == 0) on addPreprocessor is now assert(ret != -1). The function returns -1 on failure, 0 when the container is full, and the next free index otherwise, so 0 is merely the only success value at the current container size of one. != -1 is the documented contract and the existing repo idiom.
  • Hoisted the tail both branches duplicated (container construction, addPreprocessor, assert, IndexComponents, return); only the preprocessor and calculator differ.
  • The mean vector is copied with one assign instead of a zero-filling constructor followed by memcpy, which wrote every element twice.
  • Obtaining the query alignment required calling GetDistFunc for a function that is never used, since spaces.h offers no alignment-only query and the asymmetric hint covers the storage operand. That call now lives in a small GetQueryAlignment<DataType> adapter returning the hint, so the call site neither discards a value nor keeps a third distance function in scope beside sym_func and asym_func that must never be called. query_alignment is const.
  • GetSQ8StoredDataSize is [[nodiscard]] constexpr and dim / with_norm are const.

Commit 3: reject quantized tiered indexes until MOD-14957

Adding quantType to HNSWParams makes it reachable on the tiered path, where nothing handles it. TieredHNSWFactory::NewIndex forwards primaryIndexParams into HNSWFactory::NewIndex, so the primary index quantizes its storage, while NewBFParams does not copy quantType and the frontend stays unquantized. Reachable from any direct C API caller with algo = VecSimAlgo_TIERED and quantType = VecSimQuant_SQ8:

  • FP32/FP16: the getStoredDataSize() assert at tiered_factory.cpp:54 aborts on a debug build; under NDEBUG the index is built with mismatched frontend and backend layouts.
  • FP64/BF16/INT8/UINT8: HNSWFactory::NewIndex returns NULL for these types under SQ8, and the result is reinterpret_cast and dereferenced with no null check, so the process segfaults.

The catch (...) in index_factory.cpp does not help, since neither an abort nor a null dereference is an exception. RediSearch cannot set quantType until MOD-14958, so there is no product exposure today; the guard exists so main does not carry the defect between cherry-picks. MOD-14957 should replace this check and its test rather than delete them.

Commit 4: cover SQ8 rejection of unsupported data types

HNSWSQ8ParamsTest.RejectsUnsupportedDataType asserts that FLOAT64, BFLOAT16, INT8 and UINT8 with VecSimQuant_SQ8 all return NULL from index creation. Verified red without the fix: with both the type fence and the fall-through return NULL removed, all four are silently built as unquantized indexes. Also documents at EstimateElementSize why the estimate deliberately does not repeat the check (see the Bugbot thread on this PR).

Verification

Run on the final tree, after both commits:

Check Result
./check-format.sh clean
g++ -Wall -Werror -fsyntax-only, with and without -DNDEBUG clean
make build DEBUG=1 clean, no warnings
test_hnsw_sq8 46/46 passed
make unit_test DEBUG=1 2653/2653 passed
make asan 2653/2653 passed, 0 sanitizer reports

Not run: FP_64=1 variants, since this change is FP32/FP16 only.

Reviewed and deliberately left alone

  • The query_alignment hint comes from the symmetric DataType dispatcher while the asymmetric kernel that consumes the query uses unaligned loads (_mm512_loadu_ps). This costs nothing: QuantPreprocessor::preprocessQuery always allocates a fresh blob via allocate_aligned, so the hint only selects that allocation's alignment. It matches the asymmetric-types contract in spaces.h.
  • The SQ8 branch of EstimateInitialSize uses <float> for the index class even on the FP16 path. Verified correct with a static_assert on sizeof for both the single and multi index classes.
  • mean_sum_squares accumulates in float. It is a constant additive term on the IP path only, identical for every candidate, so it cannot affect ranking, only the absolute reported distance. Left as is.
  • Three raw new (allocator) calls with no RAII between them leak if a later constructor throws. preprocessors_factory.h does the same, so this is repo-wide debt rather than something this PR introduced.

Which issues this PR fixes

  1. MOD-14956

Main objects this PR modified

  1. HNSWFactory index creation and memory estimation
  2. HNSWParams and the new VecSimQuantType public API
  3. sq8::storage_bytes_count, now the single definition of the SQ8 storage layout size
  4. SQ8 HNSW unit-test target and test suite

Mark if applicable

  • This PR introduces API changes
  • This PR introduces serialization changes

🤖 Generated with Claude Code

Cherry-picked from ARM-software#4 (head 125ea15),
squashing the fork's four commits into one.

Adds 8-bit scalar quantization (SQ8) to the standalone HNSW index:

* `VecSimQuantType` plus `quantType` / `quantParams` on `HNSWParams`. Both
  fields are appended at the end of the struct and `VecSimQuant_NONE` is 0, so
  existing zero-initialized and designated-initializer construction is
  unaffected.
* `HNSWFactory` can build SQ8 indexes for FLOAT32 and FLOAT16 data types with
  the L2 and IP metrics, wiring `QuantPreprocessor` and
  `DistanceCalculatorWithNorm`, and accounts for SQ8 in `EstimateInitialSize`
  and `EstimateElementSize`.
* For SQ8, `quantParams` points to a `float[dim]` mean vector; a null pointer
  selects quantization without mean normalization.
* New `test_hnsw_sq8` unit-test target and suite.

SQ8 support for the tiered HNSW index, serialization and benchmarks is deferred
to later PRs in the MOD-14956 series.

Redis-side adjustments made during the cherry-pick:

* Dropped the added `SPDX-FileCopyrightText` Arm line from the two modified
  files, matching how #999, #1000 and #1002 landed. It is kept on the new
  `tests/unit/test_hnsw_sq8.cpp`, where the `BSD-3-Clause` identifier was
  replaced by this repo's Redis tri-license header.
* Wrapped that header so `make check-format` passes at the 100-column limit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dor-forer
dor-forer force-pushed the dor-forer-MOD-14956-hnsw-sq8 branch from 5e84a59 to 2d29bd9 Compare August 5, 2026 09:09
Follow-up review pass over the cherry-picked MOD-14956 change. No behavioural
change is intended: all of these are interface, single-source-of-truth and
idiom fixes.

Public API (`vec_sim_common.h`):

* `quantParams` is now `const void *`. Every use in the tree reads it, and two
  already cast it to `const float *`. The layout is unchanged, so this is not an
  ABI break, and callers passing a non-const pointer still compile. Worth doing
  now, before the field ships and freezes.
* The `VecSimQuant_SQ8` comment claimed "with mean normalization". Mean
  normalization is optional and selected by `quantParams`, exactly as the field's
  own comment says. Reworded.

Storage layout (`types/sq8.h`, `spaces/computer/preprocessors.h`,
`index_factories/hnsw_factory.cpp`):

* `GetSQ8StoredDataSize` re-derived the stored blob size that
  `QuantPreprocessor`'s constructors already computed. Two independent formulas
  for one layout drift silently, which is the bug class fixed in MOD-15303. The
  formula now lives once, as `sq8::storage_bytes_count<Metric, WithNorm>(dim)`,
  next to the `storage_metadata_count` it builds on, and both the preprocessor
  and the factory call it.

Factory (`index_factories/hnsw_factory.cpp`):

* Restored the `return NULL` that closes the SQ8 branch. It is unreachable today,
  since the type and metric checks leave only FP32/FP16 x L2/IP, but without it
  adding a type or metric silently falls through and builds an unquantized index.
* `assert(ret == 0)` on `addPreprocessor` is now `assert(ret != -1)`. The function
  returns -1 on failure, 0 when the container is full, and the next free index
  otherwise, so 0 is merely the only success value at the current container size
  of one. `!= -1` is the documented contract and the existing repo idiom.
* Hoisted the tail the two branches duplicated (container construction,
  `addPreprocessor`, assert, `IndexComponents`, return). Only the preprocessor and
  the distance calculator actually differ.
* The mean vector is copied with a single `assign` instead of a zero-filling
  constructor followed by `memcpy`, which wrote every element twice.
* Obtaining the query alignment required calling `GetDistFunc` for a function that
  is never used, since spaces.h offers no alignment-only query and the asymmetric
  hint covers the storage operand. That call now lives in a small
  `GetQueryAlignment<DataType>` adapter that returns the hint, so the call site
  neither discards a value nor keeps a third distance function in scope next to
  `sym_func` and `asym_func` that must never be called. `query_alignment` is const.
* `GetSQ8StoredDataSize` is `[[nodiscard]] constexpr` and `dim` / `with_norm` are
  const.

Verified:
- ./check-format.sh
- g++ -std=gnu++20 -Wall -Werror -fsyntax-only, with and without -DNDEBUG
- make build DEBUG=1 (no warnings)
- test_hnsw_sq8: 44/44 passed
- make unit_test DEBUG=1: 2651/2651 passed
- make asan: 2651/2651 passed, 0 sanitizer reports

Not run:
- FP_64=1 variants (this change is FP32/FP16 only)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dor-forer
dor-forer force-pushed the dor-forer-MOD-14956-hnsw-sq8 branch from 2d29bd9 to 4d09236 Compare August 5, 2026 10:31
@dor-forer
dor-forer marked this pull request as ready for review August 5, 2026 10:40
stored_data_size = GetSQ8StoredDataSize<VecSimMetric_L2>(params->dim, with_norm);
} else {
stored_data_size = GetSQ8StoredDataSize<VecSimMetric_IP>(params->dim, with_norm);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SQ8 element estimate skips validation

Low Severity

The new SQ8 branch in EstimateElementSize always applies GetSQ8StoredDataSize whenever quantType is VecSimQuant_SQ8, without checking that type is FLOAT32 or FLOAT16 or that the metric is supported. In the same file, NewIndex returns NULL for unsupported types and standalone Cosine, and EstimateInitialSize throws on invalid SQ8 types. Callers that size capacity from VecSimIndex_EstimateElementSize alone can get per-element byte counts for parameter sets that cannot produce an index.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4d09236. Configure here.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accurate, but I do not think it should change here, and the reasoning is worth recording.

The asymmetry is pre-existing behaviour of EstimateElementSize rather than something SQ8 introduced. Its unquantized path calls VecSimParams_GetStoredDataSize (vec_utils.cpp:296-302), which is VecSimType_sizeof(type) * dim plus a Cosine/int8 adjustment and validates nothing, for any algorithm. So the function has always returned a per-element size for parameters that cannot produce an index; the SQ8 branch matches that existing contract.

Making it strict needs an error channel it does not have. The return type is size_t, so the options are a sentinel or a throw, and EstimateElementSize currently contains no throw at all: the only ones in the file are in EstimateInitialSize and the file-loading paths. Adding one would newly carry a C++ exception across the extern "C" boundary via VecSimIndex_EstimateElementSize, which this library specifically avoids, since an exception reaching the C host aborts the host process.

The genuinely inconsistent one is arguably EstimateInitialSize being strict enough to throw, not EstimateElementSize being lax. Deciding the error model for both belongs with MOD-14958, which is what first makes quantType reachable from RediSearch; there is no product exposure before then.

What I did add is coverage of the boundary that does enforce the supported set: HNSWSQ8ParamsTest.RejectsUnsupportedDataType (7719c58) asserts that FLOAT64, BFLOAT16, INT8 and UINT8 with VecSimQuant_SQ8 all return NULL from index creation, plus a comment at the estimate explaining why it deliberately does not repeat the check. Verified red without the fix: with both the type fence and the fall-through return NULL removed, all four types are silently built as unquantized indexes.

Comment thread src/VecSim/index_factories/hnsw_factory.cpp
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.40230% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.13%. Comparing base (efd63da) to head (7719c58).

Files with missing lines Patch % Lines
src/VecSim/index_factories/hnsw_factory.cpp 95.06% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1007      +/-   ##
==========================================
- Coverage   97.15%   97.13%   -0.03%     
==========================================
  Files         141      141              
  Lines        8328     8409      +81     
==========================================
+ Hits         8091     8168      +77     
- Misses        237      241       +4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Adding `quantType` to `HNSWParams` makes it reachable on the tiered path, where
nothing handles it. `TieredHNSWFactory::NewIndex` forwards `primaryIndexParams`
straight into `HNSWFactory::NewIndex`, so the primary index quantizes its
storage, while `NewBFParams` does not copy `quantType` and the brute-force
frontend stays unquantized. The two then disagree on the stored blob layout.

Reachable from any direct C API caller with `algo = VecSimAlgo_TIERED` and
`quantType = VecSimQuant_SQ8`, in two ways:

* FP32 / FP16: `assert(hnsw_index->getStoredDataSize() == storedDataSize)` at
  tiered_factory.cpp:54 aborts on a debug build. Under NDEBUG the assert is gone
  and the index is built with mismatched frontend and backend layouts.
* FP64 / BF16 / INT8 / UINT8: `HNSWFactory::NewIndex` returns NULL for these
  types under SQ8, and the result is reinterpret_cast and dereferenced without a
  null check, so the process segfaults.

The `catch (...)` in `index_factory.cpp` does not help: neither an abort nor a
null dereference is an exception.

RediSearch cannot set `quantType` until MOD-14958, so there is no product
exposure today. This guard exists so main does not carry the defect between
cherry-picks in this series. MOD-14957, which wires quantization through the
tiered index properly, should replace the check and the test that covers it
rather than delete them.

The test builds `TieredIndexParams` with only `primaryIndexParams` set: no job
queue or thread pool is needed, since the factory rejects the params before
reaching anything that would use them. Deliberately not using `tieredIndexMock`
here, because its destructor dereferences `ctx->index_strong_ref`
unconditionally and so requires an index to have been created successfully.

Verified:
- Test is red without the guard and green with it: exit 134 (SIGABRT on the
  tiered_factory.cpp:54 assert) versus exit 0.
- ./check-format.sh
- make build DEBUG=1 (no warnings)
- test_hnsw_sq8: 45/45 passed
- make unit_test DEBUG=1: 2652/2652 passed
- make asan: 2652/2652 passed, 0 sanitizer reports

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

There are 4 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 814eab5. Configure here.

}

VecSimIndex *NewIndex(const TieredIndexParams *params) {
// Quantization is not wired into the tiered index yet (MOD-14957). Reject it here rather than

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tiered estimates ignore SQ8 rejection

Medium Severity

Tiered index NewIndex functions reject invalid configurations, such as non-NONE quantization, but the associated EstimateInitialSize and EstimateElementSize functions don't perform these same validation checks. This can lead to positive memory estimates for configurations that cannot actually be instantiated.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 814eab5. Configure here.

// type or metric cannot silently fall through and build an unquantized index instead.
return NULL;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unknown quantType builds unquantized index

Medium Severity

HNSWFactory::NewIndex only handles quantType == VecSimQuant_SQ8 explicitly. Any other non-VecSimQuant_NONE value falls through to the ordinary unquantized CreateIndexComponents path and builds a full-precision HNSW index without error. Callers that set an unsupported or forward-looking quantization code get silent misconfiguration instead of NULL, unlike the tiered factory which rejects any non-NONE quantType.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 814eab5. Configure here.

est += allocations_overhead + sizeof(MultiPreprocessorsContainer<float, 1>);
est += allocations_overhead + sizeof(QuantPreprocessor<float, VecSimMetric_L2>);
}
est += EstimateInitialSize_ChooseMultiOrSingle<float>(params->multi);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SQ8 initial size skips metric

Low Severity

The VecSimQuant_SQ8 branch in EstimateInitialSize validates params->type but not params->metric. Configurations such as SQ8 with VecSimMetric_Cosine receive a full SQ8 initial-size estimate even though NewIndex returns NULL for the same params (cosine is rejected unless remapped via is_normalized, which the public C API does not use).

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 814eab5. Configure here.

SQ8 quantizes to uint8 with FP32 metadata and only has kernels for FP32 and FP16
sources, so every other data type must be rejected at index creation. Nothing
covered that, which Cursor Bugbot noticed from the other direction on #1007: it
flagged that `EstimateElementSize` will happily size a configuration that
`NewIndex` refuses to build.

That asymmetry is intentional and pre-existing rather than something SQ8
introduced. `EstimateElementSize`'s unquantized path calls
`VecSimParams_GetStoredDataSize` (vec_utils.cpp:296), which is
`VecSimType_sizeof(type) * dim` plus a Cosine adjustment and validates nothing
for any algorithm, so the function has always answered for parameters that
cannot produce an index. Making it strict would mean either inventing a sentinel
for a `size_t` return or throwing, and `EstimateElementSize` currently contains
no `throw` at all, so that would newly carry a C++ exception across the
`extern "C"` boundary through `VecSimIndex_EstimateElementSize`. Settling the
error model for these two functions belongs with MOD-14958, which is what first
makes `quantType` reachable from RediSearch.

So this pins the boundary that actually enforces the supported set, and records
in a comment why the estimate deliberately does not repeat it.

Verified:
- Test is red without the fix: removing both the type fence and the fall-through
  `return NULL` makes it fail for all four types (FLOAT64, BFLOAT16, INT8,
  UINT8), which are otherwise silently built as unquantized indexes.
- ./check-format.sh
- make build DEBUG=1 (no warnings)
- test_hnsw_sq8: 46/46 passed
- make unit_test DEBUG=1: 2653/2653 passed
- make asan: 2653/2653 passed, 0 sanitizer reports (the new test exercises the
  early-return path, so this also covers leaking the allocator set up before it)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dor-forer
dor-forer requested a review from lerman25 August 5, 2026 11:56

@lerman25 lerman25 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I lack context for this,
Left some comments, some are AI that seem reasonable
Also there are other AI comments if you can address them


VecSimIndex *NewIndex(const VecSimParams *params, bool is_normalized) {
const HNSWParams *hnswParams = &params->algoParams.hnswParams;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

Nit

Comment on lines +153 to +156

// Unreachable today: the checks above leave only FP32/FP16 x L2/IP. Kept so that adding a
// type or metric cannot silently fall through and build an unquantized index instead.
return NULL;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe assert false here ?

ASSERT_EQ(GenerateAndAddVector(0, 0.25f, 0.25f), 1);
data_t query[4];
GenerateVector(query, 0.5f, 0.25f);
auto processed_query = CastToHNSW()->preprocessQuery(query);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test bypasses the public VecSimIndex_GetDistanceFrom_Unsafe contract by manually constructing an internal processed query. The API documents blob as a raw type×dimension vector, and C callers have no preprocessing API. For FP32 dim=4 L2, a valid raw query is 16 bytes, but the SQ8 kernel reads the appended y_sum and y_sum_squares at bytes 16–23, causing out-of-bounds reads. Please test this API with query directly and either preprocess internally, expose a public reusable prepared-query context, or reject direct-distance lookups for SQ8.

mean_sum_squares += v * v;
}

pp = new (allocator) QuantPreprocessor<DataType, Metric, true>(allocator, dim, mean_vec);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: FP16 + mean + L2 loses correctness through this instantiation. QuantPreprocessor<float16, L2, true>::preprocessQuery computes input[i] - mean[i] in FP32, then narrows it back into the FP16 query body, while storage quantization keeps its centered min/delta in FP32. Identical vector/query pairs can therefore diverge: for x = y = [1,1,1,1] and mean [10000,...], storage represents -9999 but the query rounds to -10000, yielding self-distance 4. A valid FP16 query -40000 with mean 40000 also overflows after centering. Please keep mean-centered FP16 L2 queries in FP32 with a matching asymmetric kernel, or reject/validate this combination, and add a regression.

abstractInitParams.storedDataSize = GetSQ8StoredDataSize<Metric>(dim, with_norm);

// Symmetric: both stored vectors are SQ8 blobs.
auto sym_func = spaces::GetDistFunc<sq8, float>(Metric, dim, &storage_alignment);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: the newly selected symmetric SQ8 kernel can overflow for valid large dimensions. On AVX512 VNNI, SQ8_SQ8_InnerProductImp receives an int from UINT8_InnerProductImp, whose horizontal reduction is signed 32-bit. A dimension-33027 vector that quantizes one component to 0 and 33026 components to 255 has self-dot 65025 * 33026 = 2147515650, exceeding INT_MAX. The wrapped value feeds both IP and L2 graph construction/pruning, so HNSW can be built with incorrect distances. Please use a wide/chunked accumulation or select a safe fallback above the overflow boundary, and add a boundary regression.

unsigned char storage_alignment = 0, asym_storage_alignment = 0;

// Override blob size for the SQ8 storage layout.
abstractInitParams.storedDataSize = GetSQ8StoredDataSize<Metric>(dim, with_norm);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing test serializer now silently accepts a layout its loader cannot decode. V4 records type, dim, and metric, but not quantType or the mean, and loading always constructs unquantized components. For example, FP32/L2 at dim 128 writes a 144-byte SQ8 blob per vector, while the loader expects 512 bytes of FP32 data and consumes following graph bytes as vector data. If SQ8 serialization is intentionally deferred, please make saveIndex() reject SQ8 and test that failure so it cannot emit a corrupt/unloadable file.

static constexpr bool with_quant_params = WithQuantParams;
};

using HNSWSQ8DataTypeSet =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type set varies source type and mean presence, but not metric or multi. As a result, the graph/search/range/batch/override tests all exercise the default L2 single-index path; IP only appears in the one-vector direct-distance test. That leaves the new symmetric IP kernel used during HNSW graph construction and the multi-label path untested. Please parameterize the functional suite over metric and multi, and use non-constant vectors so those paths are meaningfully exercised.

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