From c1d37079d4cfe4debd876f27ec7f26dd4923dd11 Mon Sep 17 00:00:00 2001 From: gouyt13clear Date: Fri, 6 Nov 2026 19:54:50 +0800 Subject: [PATCH 01/11] docs(formats): draft index file format 2 Add a draft specification of the shared index container planned for 0.9 (#212): header, section table, feature flags, record layouts, companion files, reading order and compatibility rules. Nothing in it is implemented yet. --- docs/formats.md | 232 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) diff --git a/docs/formats.md b/docs/formats.md index 7aa60efb..86bf2e4a 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -92,3 +92,235 @@ The body is what `Rotator::save()` writes, and `body_bytes` must equal it | Matrix | The rotation matrix as row-major float32 | `4 * dim * padded_dim` | The body defines the rotation; `seed` only records where it came from. A seeded FHT/Kac rotation is identical on every platform, while a matrix drawn from a seed is reproducible only with the same standard library and build, which is why the matrix itself is stored. + +## Index files, format 2 (draft) + +> **Status: draft for 0.9 (#211, #212).** Nothing below is implemented. Field numbers, section kinds and the open questions at the end may still change; until the format ships, no file in this layout exists. + +Format 2 is one container for IVF-RaBitQ, HNSW-RaBitQ and SymphonyQG, and later for the SSD-resident IVF index from RaBitQ-SSD. It replaces the per-index layouts: IVF-RaBitQ `RABQIDX1` versions 1 and 2, the unversioned HNSW-RaBitQ layout, SymphonyQG's quantized version 1 and its raw layout, and the unversioned hnswlib sidecar `.hnsw` of IVF-RaBitQ. Every released layout stays readable (see [Earlier layouts](#earlier-layouts)); `save` always writes format 2. + +The number continues the name used in #212. An index file is told apart from earlier layouts by its magic, not by the version, so the unrelated IVF-RaBitQ `RABQIDX1` version 2 does not clash. + +Unlike the provisional `rabitqlib.quant` formats above, index files carry the library's full compatibility promise: every format-2 file written by a release stays readable by every later release. + +### Conventions + +- Multi-byte fields are little-endian; types are fixed-width (`u32`, `u64`, `i32`, `f32`, `f64`). No field is `size_t`. +- Offsets are absolute byte offsets from the start of the file. +- The section table starts at a multiple of 8, and every section starts at a multiple of 64, so a reader can map the file and read sections in place. Writers fill gaps with zero bytes, and readers do not inspect them. +- Reserved fields and undefined flag bits are written as zero. A reader rejects a nonzero value unless a rule below says otherwise. +- Enumerations start at 1, so zero always means unset; their values are never reused or renumbered. +- Checksums are CRC-32 as `zlib.crc32` computes it, the same as in [blobs](#blobs). + +### Layout + +| Part | Position | +| --- | --- | +| Header | Offset 0, `header_bytes` long | +| Section table | `section_table_offset`, `section_count` entries of 32 bytes | +| Sections | Anywhere after the header, each at a multiple of 64 | + +Sections do not overlap the header, the table or each other, and the file ends exactly at the end of the last section. The table is sorted by offset. + +### Header + +`header_bytes` is 128 in this version. It is a multiple of 8 and at least 128; a later version may append fields, which a reader that knows only these 128 bytes skips (see [Compatibility rules](#compatibility-rules)). + +| Offset | Size | Type | Field | Rule | +| --- | --- | --- | --- | --- | +| 0 | 8 | ASCII | `magic` = `"RABQINDX"` | Anything else is not a format-2 file; the reader tries [earlier layouts](#earlier-layouts) | +| 8 | 4 | u32 | `version` = 2 | Any other value is rejected | +| 12 | 4 | u32 | `header_bytes` | See above | +| 16 | 8 | u64 | `incompat_features` | See [Feature flags](#feature-flags) | +| 24 | 8 | u64 | `ro_compat_features` | | +| 32 | 8 | u64 | `compat_features` | | +| 40 | 4 | u32 | `index_kind` | 1 = IVF-RaBitQ, 2 = HNSW-RaBitQ, 3 = SymphonyQG; 4 = IVF-SSD (reserved). Unknown values are rejected | +| 44 | 4 | u32 | `metric` | 1 = L2, 2 = inner product, 3 = cosine. See [Metrics](#metrics) | +| 48 | 8 | u64 | `dim` | Original dimension, at least 1 | +| 56 | 8 | u64 | `padded_dim` | Working dimension; a multiple of 32, at least `dim`. Readers use the stored value and never recompute it | +| 64 | 4 | u32 | `code_bits` | Bits per coordinate of the stored quantized code, 1–9; 0 when the index stores no quantized code of its points | +| 68 | 4 | u32 | `raw_type` | Element type of stored raw vectors: 0 = none, 1 = float32, 2 = uint8 | +| 72 | 4 | u32 | `id_bits` | Width of the point IDs stored in the file: 32 or 64 | +| 76 | 4 | u32 | `flags` | Bit 0, `seed_known`: `seed` is valid | +| 80 | 8 | u64 | `seed` | The index's construction seed (graph levels, graph construction, training sample). Zero when `seed_known` is clear. The rotator records its own seed | +| 88 | 8 | u64 | `num_points` | Stored points, including removed ones | +| 96 | 8 | u64 | `section_table_offset` | A multiple of 8, at least `header_bytes` | +| 104 | 4 | u32 | `section_count` | At least 1 | +| 108 | 4 | u32 | `header_crc32` | Over the header with this field taken as zero, followed by the section table | +| 112 | 16 | ASCII | `writer_version` | Library version that wrote the file, NUL-padded. For error messages only; readers never branch on it | + +The Python `nbits` maps to these fields as follows. IVF-RaBitQ `nbits` 1–9 is `code_bits`. IVF-RaBitQ `nbits = 32` is `code_bits = 1` with `raw_type = float32`, because it keeps 1-bit FastScan codes and reranks with raw vectors. SymphonyQG `nbits = 32` is `code_bits = 0` with `raw_type = float32`. + +### Section table + +| Offset | Size | Type | Field | Rule | +| --- | --- | --- | --- | --- | +| 0 | 4 | u32 | `kind` | See [Section kinds](#section-kinds) | +| 4 | 4 | u32 | `flags` | Bit 0, `required`; bit 1, `checksummed` | +| 8 | 8 | u64 | `offset` | A multiple of 64 | +| 16 | 8 | u64 | `size` | Bytes; may be 0 | +| 24 | 4 | u32 | `crc32` | Over the section's bytes when `checksummed` is set; zero otherwise | +| 28 | 4 | — | reserved | | + +A kind appears at most once. Writers checksum the header-like sections (parameters, cluster sizes, companions), whose values size later allocations, and leave bulk data unchecksummed, so that loading a large index does not read it twice. + +A section the reader does not know is handled by its `required` bit: a required one rejects the file, and an optional one is skipped. A writer marks a section optional only if dropping it leaves a correct index, for example a cache that can be recomputed. `save` drops optional sections it does not know. + +### Feature flags + +The three masks follow the ext4 superblock: they say what an older reader may do with a file that uses something newer than itself. + +| Mask | A reader that does not know a set bit | +| --- | --- | +| `incompat_features` | Rejects the file; reading it would give wrong results | +| `ro_compat_features` | May load and search; `add`, `remove` and `save` raise, because they would leave the unknown data stale | +| `compat_features` | Ignores the bit | + +The error names the unknown bits and `writer_version`, such as "index uses features 0x4 unknown to rabitqlib 0.9.1; written by 0.10.0". + +No bit is defined in version 2. Bits are assigned when a feature needs one. Examples: + +| Possible feature | Mask | Why | +| --- | --- | --- | +| Caller-provided IDs (`add(x, ids=...)`), an external-ID section | `incompat` | An older reader would return row numbers instead of the caller's IDs | +| A per-cluster statistics section that `add` updates | `ro_compat` | An older reader can search, but its `add` would not update the section | +| A new code layout of an existing section | `incompat`, or a new section kind | The bytes mean something else | + +### Metrics + +L2 and inner product are as in [Distance conventions](development.md#distance-conventions). Cosine stores the codes and factors of inner product over L2-normalized vectors. The index normalizes data on `add` and queries on `search`, and reports `1 - cos` as the distance. It is a stored metric, not a flag of the caller, so a reloaded index keeps normalizing. + +### Point IDs + +Search results expose IDs as `int64`, and a missing result is `-1` (#211). `id_bits` records what the file stores. The in-memory indexes keep 32-bit row numbers internally and write `id_bits = 32`; their upper limit stays below 2^31 points (the search-buffer marker bit). IVF-SSD writes `id_bits = 64`. A reader rejects an `id_bits` its index kind does not support. + +### Section kinds + +Kinds 1–255 are shared, 256–511 are IVF-RaBitQ, 512–767 HNSW-RaBitQ, 768–1023 SymphonyQG and 1024–1279 IVF-SSD. Every section listed here is `required`. + +| Kind | Name | Used by | Content | +| --- | --- | --- | --- | +| 1 | `rotator` | All | A [rotator blob](#rotator-kind-1-version-1), byte for byte what `Rotator.to_bytes()` writes. Its `dim` and `padded_dim` must equal the header's | +| 2 | `companions` | IVF-SSD | The [companion table](#companion-files) | +| 256 | `ivf_params` | IVF | `num_clusters` u64; `initializer` u32 (1 = flat, 2 = FlatRaBitQ, 3 = HNSW); reserved u32 | +| 257 | `ivf_cluster_sizes` | IVF | `num_clusters` × u64; the sum equals `num_points` | +| 258 | `ivf_centroids` | IVF, flat and FlatRaBitQ | `num_clusters × padded_dim` f32, rotated | +| 259 | `ivf_flat_rabitq` | IVF, FlatRaBitQ | The global centroid (`padded_dim` f32), then the centroids' [FastScan blocks](#fastscan-blocks-version-1) | +| 260 | `ivf_hnsw_routing` | IVF, HNSW | The centroid graph, byte for byte the current `.hnsw` sidecar (hnswlib layout) | +| 261 | `ivf_codes` | IVF | Per cluster in cluster order, `ceil(size / 32)` [FastScan blocks](#fastscan-blocks-version-1). Removed points have `add = +inf` | +| 262 | `ivf_rerank` | IVF, `code_bits > 1` or raw | Per point in cluster order: a [single-code record](#single-code-record) with `b = code_bits - 1`, or `dim` raw values of `raw_type` | +| 263 | `ivf_ids` | IVF | Per point in cluster order, `id_bits`-wide IDs | +| 512 | `hnsw_params` | HNSW | `max_elements`, `num_clusters`, `M`, `upper_max_degree`, `base_max_degree`, `ef_construction` as u64; `level_multiplier` f64; `max_level` i32; reserved u32; `entry_point` u64. Record sizes are derived, not stored (see [HNSW-RaBitQ records](#hnsw-rabitq-records)) | +| 513 | `hnsw_centroids` | HNSW | `num_clusters × padded_dim` f32 | +| 514 | `hnsw_records` | HNSW | `num_points` [base-layer records](#hnsw-rabitq-records) | +| 515 | `hnsw_levels` | HNSW | `num_points` × u32 levels | +| 516 | `hnsw_upper_links` | HNSW | The upper-layer link lists of points with level > 0 in point order; each is `level × upper_links_bytes` | +| 768 | `qg_params` | SymphonyQG | `max_degree` u64; `entry_point` u64; `ef_construction` u64 (0 = unknown); `init` u32 (0 = unknown); reserved u32 | +| 769 | `qg_centroid` | SymphonyQG, `code_bits > 0` | `padded_dim` f32 | +| 770 | `qg_records` | SymphonyQG | `num_points` [records](#symphonyqg-records) | + +The HNSW per-point length prefix of the current layout becomes `hnsw_levels`, so a reader knows every size before reading the link lists. + +### Record layouts + +These are the in-memory layouts the indexes use today, written as is, so that `load` copies sections without converting them. Record sizes follow from the header and parameter sections; readers derive them and compare them with the section sizes. Floats inside records are not aligned. Reserved bytes inside bulk data are written as zero and ignored by readers, which do not scan bulk data for them. + +The tables use `id_bits = 32`, so an ID takes 4 bytes; this is the only value the in-memory indexes write. + +#### Single-code record + +One vector's `b`-bit code with its two factors. IVF-RaBitQ and HNSW-RaBitQ use it for the extra bits (`b = code_bits - 1`), SymphonyQG for its whole code (`b = code_bits`). + +| Offset | Size | Content | +| --- | --- | --- | +| 0 | `padded_dim * b / 8` | The codes as [packed codes](#packed-codes-version-1) of width `b` | +| `padded_dim * b / 8` | 4 | `add`, f32 | +| `padded_dim * b / 8 + 4` | 4 | `rescale`, f32 | + +The record has no `error` factor. It should also become a raw-array format of `rabitqlib.quant`, so that both share one definition. + +#### HNSW sign code + +HNSW-RaBitQ stores the 1-bit code in a different bit order from the `rabitqlib.quant` bitset. Split the `padded_dim` coordinates into groups of 64, where the last group has 32 coordinates when `padded_dim` is not a multiple of 64. A group of `G` bytes, read as a little-endian integer, stores coordinate `t` of the group in bit `8G - 1 - t`. This is what `pack_binary_to_bytes` writes (checked against it for `padded_dim` 32, 64, 96, 160 and 1024); the implementation adds a golden test that freezes it. + +#### HNSW-RaBitQ records + +Let `L = 4 * (base_max_degree + 1)` and `U = 4 * (upper_max_degree + 1)`. + +| Offset | Size | Content | +| --- | --- | --- | +| 0 | 2 | Base-layer neighbor count, u16 | +| 2 | 2 | Reserved | +| 4 | `4 * base_max_degree` | Base-layer neighbor IDs; entries past the count are unspecified | +| `L` | 4 | Cluster ID, u32; bit 31 is the removal mark (see [HNSW removal](development.md#hnsw-removal)) | +| `L + 4` | 4 | Row ID of the point, u32 | +| `L + 8` | `padded_dim / 8` | [Sign code](#hnsw-sign-code) | +| `L + 8 + padded_dim / 8` | 12 | `add`, `rescale` and `error`, f32 | +| `L + 20 + padded_dim / 8` | | When `code_bits > 1`, a [single-code record](#single-code-record) with `b = code_bits - 1` | + +A point at level `l > 0` has `l` upper-layer link lists in `hnsw_upper_links`, for levels 1 to `l`, each `U` bytes: a u16 count, 2 reserved bytes, then `upper_max_degree` neighbor IDs. + +#### SymphonyQG records + +Every vertex has `max_degree` neighbor slots, a multiple of 32. Let `V` be the size of the vector part: `4 * dim` for raw vectors, or the [single-code record](#single-code-record) size with `b = code_bits` otherwise. + +| Offset | Size | Content | +| --- | --- | --- | +| 0 | `V` | The vector: `dim` float32 values (`raw_type = float32`, `code_bits = 0`), or a single-code record | +| `V` | `(max_degree / 32) * (padded_dim * 4 + 256)` | The neighbors' 1-bit codes, quantized against this vertex, as [FastScan blocks](#fastscan-blocks-version-1) without the `error` factor: sign codes, then 32 `add` and 32 `rescale` values | +| `V + (max_degree / 32) * (padded_dim * 4 + 256)` | `4 * max_degree` | Neighbor IDs | + +Unused lanes of a partial neighbor block have zero factors, and their IDs are unspecified. + +### Companion files + +Only IVF-SSD uses them. `.ssd` holds the page-packed inverted lists, read with `O_DIRECT` and too large to keep in the main file. The `companions` section lists each one: + +| Offset | Size | Type | Field | +| --- | --- | --- | --- | +| 0 | 4 | u32 | `role`: 1 = SSD inverted lists | +| 4 | 4 | u32 | reserved | +| 8 | 32 | ASCII | `suffix` appended to the main file's path, such as `".ssd"`, NUL-padded; no path separators | +| 40 | 8 | u64 | `size` in bytes | +| 48 | 8 | u64 | `fingerprint` | + +`fingerprint` is the 64-bit FNV-1a hash that RaBitQ-SSD already uses (offset basis 1469598103934665603, prime 1099511628211) over the rotated centroids as f32 bytes. It is deterministic, so saving the same index twice gives identical files. + +A companion starts with a 4,096-byte header page: magic `"RABQSSD\0"`, version, `fingerprint`, `num_points` and `code_bits`, then zeros, with a CRC-32 over the page. Data pages follow at offset 4,096, keeping the 4,096-byte alignment that `O_DIRECT` needs. Loading rejects a missing companion, or one whose size, fingerprint or header does not match the main file. + +RaBitQ-SSD's swappable coarse quantizer is not a companion: the base does not pin one. A coarse-quantizer file is its own file and records the base's fingerprint, so the pairing is checked from the coarse side. Its layout is specified when IVF-SSD moves into the library. + +### Reading + +A reader checks, in order, and validates every size before allocating: + +1. The file is at least 128 bytes long, and the magic, `version` and `header_bytes` are valid. +2. The section table lies within the file, and `header_crc32` matches. +3. Feature flags, as in the table above. +4. The header fields are valid for `index_kind`. +5. Each section lies within the file, is aligned, and does not overlap another; each checksummed section matches its `crc32`. +6. Required sections are present and unknown required sections are absent. +7. Each section's size equals what the header and parameter sections imply, before its content is read. + +Index files remain trusted input (see [Index file compatibility](docs/index/usage.md#index-file-compatibility)). These checks report truncated, corrupt or mismatched files; they do not make crafted files safe. + +### Writing + +`save` writes to a temporary file and renames it into place, as today. With the routing graph inside the main file, IVF-RaBitQ no longer has a second rename that can leave a new sidecar beside an old index. Resaving a file loaded from an earlier layout writes format 2. IVF-SSD companion files are written before the main file, so a failed save never leaves a main file that points to a missing companion. + +### Earlier layouts + +Readers dispatch on the leading bytes: `"RABQINDX"`, then each index's legacy magic (IVF `RABQIDX1`, SymphonyQG quantized), then the unversioned fallbacks the readers accept today. Legacy files have no cosine metric, seed or 64-bit IDs; they load as `seed_known = false` with 32-bit IDs, as they do in 0.8. The frozen fixtures in `tests/fixtures/persistence/` keep covering every layout. + +### Compatibility rules + +- A new release reads every earlier released index file. +- An older release reads a newer format-2 file or rejects it with an error naming the problem; it never misreads it. Older releases reject `version` values other than 2, unknown `index_kind`, `metric` or `id_bits` values, unknown required sections and unknown `incompat` bits. +- An additive change appends header fields (raising `header_bytes`), adds sections, or adds feature bits, chosen by the table in [Feature flags](#feature-flags). A change that an older reader cannot detect from these is not allowed; it needs `version = 3`. +- Every released change adds a frozen fixture. Synthetic "future" fixtures check the rules above: an unknown optional section loads; an unknown required section, an `incompat` bit or a larger `version` is rejected with its message; a `ro_compat` bit loads read-only; a longer header loads. + +### Open questions + +- Python error type for a rejected file: index loads raise `RuntimeError` today, `rabitqlib.quant` raises `FormatError` (a `ValueError`). Proposed: one top-level `rabitqlib.FormatError` that subclasses both `ValueError` and `RuntimeError`, raised by `load` and by `rabitqlib.quant`, so existing `except RuntimeError` handlers keep working. +- Whether the header should be a [blob](#blobs) kind instead of its own magic. Proposed: its own magic, as above. A blob checksums its whole body, which is too costly for large index files, so index files get their own container and nest blobs in sections. +- Whether `seed` should keep the full u64 that HNSW-RaBitQ accepts, while the rotator blob records a u32 seed. Proposed: u64 in the header; the rotator blob's seed stays informational and has `seed_known` clear when the seed does not fit in u32. From 04e81920752bf88c21399d919c0f881258462eaf Mon Sep 17 00:00:00 2001 From: gouyt13clear Date: Fri, 6 Nov 2026 20:42:24 +0800 Subject: [PATCH 02/11] feat(index): add the format-2 index container Add the container layer of index file format 2 (#212): the header and section table encoding, a zlib-compatible CRC-32, a writer that emits the file front to back without seeking, and a reader that validates the header, feature flags, expected index kind and section table in the order the specification gives. No index uses it yet. Update the draft specification with the rules the implementation settled: empty sections, the check order, padded_dim at most 65,536, reserved section kind 0, and what a resave drops. --- CMakeLists.txt | 1 + docs/formats.md | 35 +- src/index/format2.cpp | 592 +++++++++++++++++++ src/index/format2.hpp | 157 +++++ tests/unit/rabitqlib/index/format2_test.cpp | 616 ++++++++++++++++++++ 5 files changed, 1386 insertions(+), 15 deletions(-) create mode 100644 src/index/format2.cpp create mode 100644 src/index/format2.hpp create mode 100644 tests/unit/rabitqlib/index/format2_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2649d66d..2dcc28ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -247,6 +247,7 @@ if(RABITQ_BUILD_FASTSCAN) endif() if(RABITQ_BUILD_INDEX) add_library(rabitq_index STATIC + src/index/format2.cpp src/index/hnsw.cpp src/index/ivf.cpp src/index/ivf_initializer.cpp diff --git a/docs/formats.md b/docs/formats.md index 86bf2e4a..c4e3f55b 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -95,7 +95,7 @@ The body defines the rotation; `seed` only records where it came from. A seeded ## Index files, format 2 (draft) -> **Status: draft for 0.9 (#211, #212).** Nothing below is implemented. Field numbers, section kinds and the open questions at the end may still change; until the format ships, no file in this layout exists. +> **Status: draft for 0.9 (#211, #212).** The container layer (header, section table, checksums) is implemented in `src/index/format2.*`, but no index reads or writes format 2 yet. Field numbers, section kinds and the open questions at the end may still change; until the format ships, no file in this layout exists. Format 2 is one container for IVF-RaBitQ, HNSW-RaBitQ and SymphonyQG, and later for the SSD-resident IVF index from RaBitQ-SSD. It replaces the per-index layouts: IVF-RaBitQ `RABQIDX1` versions 1 and 2, the unversioned HNSW-RaBitQ layout, SymphonyQG's quantized version 1 and its raw layout, and the unversioned hnswlib sidecar `.hnsw` of IVF-RaBitQ. Every released layout stays readable (see [Earlier layouts](#earlier-layouts)); `save` always writes format 2. @@ -120,7 +120,7 @@ Unlike the provisional `rabitqlib.quant` formats above, index files carry the li | Section table | `section_table_offset`, `section_count` entries of 32 bytes | | Sections | Anywhere after the header, each at a multiple of 64 | -Sections do not overlap the header, the table or each other, and the file ends exactly at the end of the last section. The table is sorted by offset. +Every section starts at or after `header_bytes`. Non-empty sections do not overlap the table or each other; empty sections take no bytes and may share an offset. Table entries are in non-decreasing offset order. The file ends exactly where the table or the last section ends, whichever is later; an empty section still has an aligned offset, so writers pad up to it. ### Header @@ -137,7 +137,7 @@ Sections do not overlap the header, the table or each other, and the file ends e | 40 | 4 | u32 | `index_kind` | 1 = IVF-RaBitQ, 2 = HNSW-RaBitQ, 3 = SymphonyQG; 4 = IVF-SSD (reserved). Unknown values are rejected | | 44 | 4 | u32 | `metric` | 1 = L2, 2 = inner product, 3 = cosine. See [Metrics](#metrics) | | 48 | 8 | u64 | `dim` | Original dimension, at least 1 | -| 56 | 8 | u64 | `padded_dim` | Working dimension; a multiple of 32, at least `dim`. Readers use the stored value and never recompute it | +| 56 | 8 | u64 | `padded_dim` | Working dimension; a multiple of 32, at least `dim` and at most 65,536 (the rotator's limit). Readers use the stored value and never recompute it | | 64 | 4 | u32 | `code_bits` | Bits per coordinate of the stored quantized code, 1–9; 0 when the index stores no quantized code of its points | | 68 | 4 | u32 | `raw_type` | Element type of stored raw vectors: 0 = none, 1 = float32, 2 = uint8 | | 72 | 4 | u32 | `id_bits` | Width of the point IDs stored in the file: 32 or 64 | @@ -147,7 +147,7 @@ Sections do not overlap the header, the table or each other, and the file ends e | 96 | 8 | u64 | `section_table_offset` | A multiple of 8, at least `header_bytes` | | 104 | 4 | u32 | `section_count` | At least 1 | | 108 | 4 | u32 | `header_crc32` | Over the header with this field taken as zero, followed by the section table | -| 112 | 16 | ASCII | `writer_version` | Library version that wrote the file, NUL-padded. For error messages only; readers never branch on it | +| 112 | 16 | ASCII | `writer_version` | Library version that wrote the file, printable ASCII, NUL-padded. For error messages only; readers never branch on it | The Python `nbits` maps to these fields as follows. IVF-RaBitQ `nbits` 1–9 is `code_bits`. IVF-RaBitQ `nbits = 32` is `code_bits = 1` with `raw_type = float32`, because it keeps 1-bit FastScan codes and reranks with raw vectors. SymphonyQG `nbits = 32` is `code_bits = 0` with `raw_type = float32`. @@ -162,9 +162,9 @@ The Python `nbits` maps to these fields as follows. IVF-RaBitQ `nbits` 1–9 is | 24 | 4 | u32 | `crc32` | Over the section's bytes when `checksummed` is set; zero otherwise | | 28 | 4 | — | reserved | | -A kind appears at most once. Writers checksum the header-like sections (parameters, cluster sizes, companions), whose values size later allocations, and leave bulk data unchecksummed, so that loading a large index does not read it twice. +Kind 0 is reserved, and a kind appears at most once. Section flags other than the two above are rejected; a later version cannot add a section flag without a new `version`. Writers checksum the header-like sections (parameters, cluster sizes, companions), whose values size later allocations, and leave bulk data unchecksummed, so that loading a large index does not read it twice. -A section the reader does not know is handled by its `required` bit: a required one rejects the file, and an optional one is skipped. A writer marks a section optional only if dropping it leaves a correct index, for example a cache that can be recomputed. `save` drops optional sections it does not know. +A section the reader does not know is handled by its `required` bit: a required one rejects the file, and an optional one is skipped without reading it, so its checksum is not verified. A writer marks a section optional only if dropping it leaves a correct index, for example a cache that can be recomputed. `save` drops optional sections it does not know. ### Feature flags @@ -174,9 +174,11 @@ The three masks follow the ext4 superblock: they say what an older reader may do | --- | --- | | `incompat_features` | Rejects the file; reading it would give wrong results | | `ro_compat_features` | May load and search; `add`, `remove` and `save` raise, because they would leave the unknown data stale | -| `compat_features` | Ignores the bit | +| `compat_features` | Ignores the bit; `save` clears it | -The error names the unknown bits and `writer_version`, such as "index uses features 0x4 unknown to rabitqlib 0.9.1; written by 0.10.0". +The error names the unknown bits and `writer_version`, such as "index file uses incompatible features 0x4 that this release does not know; written by rabitqlib 0.10.0". + +A writer sets only the bits it knows. Resaving a file therefore drops the unknown `compat` bits, together with the unknown optional sections and header fields past the writer's `header_bytes`. A `compat` bit may describe only data whose loss on such a resave is harmless. No bit is defined in version 2. Bits are assigned when a feature needs one. Examples: @@ -292,21 +294,24 @@ RaBitQ-SSD's swappable coarse quantizer is not a companion: the base does not pi ### Reading -A reader checks, in order, and validates every size before allocating: +A reader checks, in order, and validates every size before allocating. The loader of each index knows the index kind it expects, so a file of another kind is reported as such before any section is checked: 1. The file is at least 128 bytes long, and the magic, `version` and `header_bytes` are valid. -2. The section table lies within the file, and `header_crc32` matches. +2. The section table is non-empty and lies within the file, and `header_crc32` matches. 3. Feature flags, as in the table above. -4. The header fields are valid for `index_kind`. -5. Each section lies within the file, is aligned, and does not overlap another; each checksummed section matches its `crc32`. -6. Required sections are present and unknown required sections are absent. -7. Each section's size equals what the header and parameter sections imply, before its content is read. +4. `index_kind` is the expected one; the fields that every kind shares are valid; then the expecting index's own header rules hold. +5. Each table entry, in order: its flags and reserved field, alignment, bounds, order, overlap and repetition, then its kind. An unknown required kind rejects the file here. +6. The file ends where the table or the last section ends. +7. Each known checksummed section matches its `crc32`. +8. The index's required sections are present, and each section's size equals what the header and parameter sections imply, before its content is read. + +Steps 1–7 belong to the container; step 8 and the index's header rules in step 4 belong to each index. Readers read through the stream buffer, so the stream's exception mask does not matter, and need seekable input. Index files remain trusted input (see [Index file compatibility](docs/index/usage.md#index-file-compatibility)). These checks report truncated, corrupt or mismatched files; they do not make crafted files safe. ### Writing -`save` writes to a temporary file and renames it into place, as today. With the routing graph inside the main file, IVF-RaBitQ no longer has a second rename that can leave a new sidecar beside an old index. Resaving a file loaded from an earlier layout writes format 2. IVF-SSD companion files are written before the main file, so a failed save never leaves a main file that points to a missing companion. +A writer writes the file once, front to back, so it does not need seekable output (`save` writes devices and FIFOs in place): it buffers the checksummed sections, which are small, to know their checksums before the table. `save` writes to a temporary file and renames it into place, as today. With the routing graph inside the main file, IVF-RaBitQ no longer has a second rename that can leave a new sidecar beside an old index. Resaving a file loaded from an earlier layout writes format 2. IVF-SSD companion files are written before the main file, so a failed save never leaves a main file that points to a missing companion. ### Earlier layouts diff --git a/src/index/format2.cpp b/src/index/format2.cpp new file mode 100644 index 00000000..c5b1ff98 --- /dev/null +++ b/src/index/format2.cpp @@ -0,0 +1,592 @@ +#include "format2.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace rabitqlib::index_detail::format2 { +namespace { + +constexpr uint32_t kHeaderFlagSeedKnown = 1U << 0; +constexpr uint32_t kKnownSectionFlags = kSectionRequired | kSectionChecksummed; +constexpr size_t kChecksumOffset = 108; +constexpr size_t kChunkBytes = size_t{1} << 16; + +constexpr std::array make_crc_table() { + std::array table{}; + for (uint32_t i = 0; i < 256; ++i) { + uint32_t value = i; + for (int bit = 0; bit < 8; ++bit) { + value = (value & 1U) != 0 ? 0xEDB88320U ^ (value >> 1) : value >> 1; + } + table[i] = value; + } + return table; +} + +constexpr std::array kCrcTable = make_crc_table(); + +void put_u32(uint8_t* out, uint32_t value) { + for (size_t i = 0; i < 4; ++i) { + out[i] = static_cast(value >> (8 * i)); + } +} + +void put_u64(uint8_t* out, uint64_t value) { + for (size_t i = 0; i < 8; ++i) { + out[i] = static_cast(value >> (8 * i)); + } +} + +uint32_t get_u32(const uint8_t* in) { + uint32_t value = 0; + for (size_t i = 0; i < 4; ++i) { + value |= static_cast(in[i]) << (8 * i); + } + return value; +} + +uint64_t get_u64(const uint8_t* in) { + uint64_t value = 0; + for (size_t i = 0; i < 8; ++i) { + value |= static_cast(in[i]) << (8 * i); + } + return value; +} + +// Round up to a multiple of kSectionAlignment; the caller has bounded `value`. +uint64_t align_section(uint64_t value) { + return (value + kSectionAlignment - 1) / kSectionAlignment * kSectionAlignment; +} + +std::string hex(uint64_t value) { + std::ostringstream out; + out << "0x" << std::hex << value; + return out.str(); +} + +// The error-message component of each index, as in docs/development.md. +const char* component(IndexKind kind) { + switch (kind) { + case IndexKind::Ivf: + return "IVF"; + case IndexKind::Hnsw: + return "HNSW"; + case IndexKind::SymphonyQg: + return "QuantizedGraph"; + } + return "Index"; +} + +const char* kind_name(uint32_t kind) { + switch (kind) { + case 1: + return "an IVF-RaBitQ"; + case 2: + return "an HNSW-RaBitQ"; + case 3: + return "a SymphonyQG"; + case 4: + return "an IVF-SSD"; + default: + return nullptr; + } +} + +// The header rules that hold for every index kind, as the text after "index file", +// or an empty string. The index kind itself is checked by the caller. +std::string header_error(const Header& header) { + const auto metric = static_cast(header.metric); + if (metric < 1 || metric > 3) { + return "has unknown metric " + std::to_string(metric); + } + if (header.dim == 0) { + return "has dimension 0"; + } + if (header.padded_dim % 32 != 0 || header.padded_dim < header.dim || + header.padded_dim > kMaxPaddedDim) { + return "has padded dimension " + std::to_string(header.padded_dim) + + ", which must be a multiple of 32 from the dimension " + + std::to_string(header.dim) + " to 65536"; + } + if (header.code_bits > 9) { + return "has " + std::to_string(header.code_bits) + + " code bits; at most 9 are supported"; + } + const auto raw_type = static_cast(header.raw_type); + if (raw_type > 2) { + return "has unknown raw element type " + std::to_string(raw_type); + } + if (header.id_bits != 32 && header.id_bits != 64) { + return "has " + std::to_string(header.id_bits) + + "-bit IDs; only 32 and 64 are valid"; + } + return {}; +} + +void encode_header( + const Header& header, + uint64_t section_count, + std::string_view writer_version, + uint8_t* out +) { + std::memset(out, 0, kHeaderBytes); + std::memcpy(out, kMagic.data(), kMagic.size()); + put_u32(out + 8, kVersion); + put_u32(out + 12, kHeaderBytes); + put_u64(out + 16, header.incompat_features); + put_u64(out + 24, header.ro_compat_features); + put_u64(out + 32, header.compat_features); + put_u32(out + 40, static_cast(header.index_kind)); + put_u32(out + 44, static_cast(header.metric)); + put_u64(out + 48, header.dim); + put_u64(out + 56, header.padded_dim); + put_u32(out + 64, header.code_bits); + put_u32(out + 68, static_cast(header.raw_type)); + put_u32(out + 72, header.id_bits); + put_u32(out + 76, header.seed.has_value() ? kHeaderFlagSeedKnown : 0); + put_u64(out + 80, header.seed.value_or(0)); + put_u64(out + 88, header.num_points); + put_u64(out + 96, kHeaderBytes); + put_u32(out + 104, static_cast(section_count)); + std::memcpy(out + 112, writer_version.data(), writer_version.size()); +} + +void encode_section(const Section& section, uint8_t* out) { + std::memset(out, 0, kSectionEntryBytes); + put_u32(out, section.kind); + put_u32(out + 4, section.flags); + put_u64(out + 8, section.offset); + put_u64(out + 16, section.size); + put_u32(out + 24, section.crc32); +} + +// The header checksum: the header with its checksum field taken as zero, then the table. +uint32_t header_checksum( + const std::vector& header_bytes, const std::vector& table_bytes +) { + constexpr std::array kZero{}; + uint32_t crc = crc32(header_bytes.data(), kChecksumOffset); + crc = crc32(kZero.data(), kZero.size(), crc); + crc = crc32( + header_bytes.data() + kChecksumOffset + 4, + header_bytes.size() - kChecksumOffset - 4, + crc + ); + return crc32(table_bytes.data(), table_bytes.size(), crc); +} + +void write_zeros(std::ostream& output, uint64_t count) { + static const std::array kZeros{}; + while (count > 0) { + const auto chunk = static_cast(std::min(count, kZeros.size())); + output.write(kZeros.data(), static_cast(chunk)); + count -= chunk; + } +} + +} // namespace + +uint32_t crc32(const void* data, size_t size, uint32_t crc) { + const auto* bytes = static_cast(data); + crc = ~crc; + for (size_t i = 0; i < size; ++i) { + crc = kCrcTable[(crc ^ bytes[i]) & 0xFFU] ^ (crc >> 8); + } + return ~crc; +} + +void SectionSink::write(const void* data, size_t size) { + if (size > remaining_) { + throw std::invalid_argument("index section writer wrote more bytes than declared"); + } + output_.write(static_cast(data), static_cast(size)); + remaining_ -= size; +} + +void write_file( + std::ostream& output, + const Header& header, + const std::vector& sections, + std::string_view writer_version +) { + const std::string name = component(header.index_kind); + const auto kind = static_cast(header.index_kind); + if (kind < 1 || kind > 3) { + throw std::invalid_argument( + name + " index file cannot have index kind " + std::to_string(kind) + ); + } + if (const std::string error = header_error(header); !error.empty()) { + throw std::invalid_argument(name + " index file " + error); + } + if ((header.incompat_features & ~kKnownIncompatFeatures) != 0 || + (header.ro_compat_features & ~kKnownRoCompatFeatures) != 0 || + (header.compat_features & ~kKnownCompatFeatures) != 0) { + throw std::invalid_argument(name + " index file cannot use unknown feature bits"); + } + if (sections.empty()) { + throw std::invalid_argument(name + " index file needs at least one section"); + } + const bool printable = + std::all_of(writer_version.begin(), writer_version.end(), [](char c) { + return c >= 0x20 && c <= 0x7E; + }); + if (writer_version.size() > kWriterVersionBytes || !printable) { + throw std::invalid_argument( + name + + " index file writer version must be at most 16 printable ASCII characters" + ); + } + + std::vector
entries(sections.size()); + std::unordered_set kinds; + const uint64_t table_end = kHeaderBytes + (kSectionEntryBytes * sections.size()); + uint64_t position = table_end; + for (size_t i = 0; i < sections.size(); ++i) { + const SectionWrite& section = sections[i]; + if (section.kind == 0) { + throw std::invalid_argument(name + " index file section kind 0 is reserved"); + } + if ((section.flags & ~kKnownSectionFlags) != 0) { + throw std::invalid_argument(name + " index file section has unknown flags"); + } + if (!kinds.insert(section.kind).second) { + throw std::invalid_argument( + name + " index file section kind " + std::to_string(section.kind) + + " is repeated" + ); + } + position = align_section(position); + if (section.size > + std::numeric_limits::max() - kSectionAlignment - position) { + throw std::invalid_argument( + name + " index file sections exceed the offset range" + ); + } + entries[i].kind = section.kind; + entries[i].flags = section.flags; + entries[i].offset = position; + entries[i].size = section.size; + position += section.size; + } + + // Run a section's writer and check that it wrote exactly the declared size. + const auto write_section = [&name](const SectionWrite& section, SectionSink& sink) { + if (section.write) { + section.write(sink); + } + if (sink.remaining() != 0) { + throw std::invalid_argument( + name + " index file section kind " + std::to_string(section.kind) + + " writer wrote fewer bytes than declared" + ); + } + }; + + // Checksummed sections are small: buffer them, so that the header and the table, + // which hold their checksums, can be written first. + std::vector buffered(sections.size()); + for (size_t i = 0; i < sections.size(); ++i) { + if ((entries[i].flags & kSectionChecksummed) == 0) { + continue; + } + std::ostringstream buffer; + SectionSink sink(buffer, entries[i].size); + write_section(sections[i], sink); + buffered[i] = buffer.str(); + entries[i].crc32 = crc32(buffered[i].data(), buffered[i].size()); + } + + std::vector header_bytes(kHeaderBytes); + encode_header(header, sections.size(), writer_version, header_bytes.data()); + std::vector table_bytes(kSectionEntryBytes * sections.size()); + for (size_t i = 0; i < entries.size(); ++i) { + encode_section(entries[i], table_bytes.data() + (i * kSectionEntryBytes)); + } + put_u32( + header_bytes.data() + kChecksumOffset, header_checksum(header_bytes, table_bytes) + ); + + output.write( + reinterpret_cast(header_bytes.data()), + static_cast(header_bytes.size()) + ); + output.write( + reinterpret_cast(table_bytes.data()), + static_cast(table_bytes.size()) + ); + uint64_t written = table_end; + for (size_t i = 0; i < sections.size(); ++i) { + write_zeros(output, entries[i].offset - written); + if ((entries[i].flags & kSectionChecksummed) != 0) { + output.write( + buffered[i].data(), static_cast(buffered[i].size()) + ); + } else { + SectionSink sink(output, entries[i].size); + write_section(sections[i], sink); + } + written = entries[i].offset + entries[i].size; + } + output.flush(); + if (!output) { + throw std::runtime_error(name + " index file cannot be written"); + } +} + +bool has_magic(std::istream& input) { + std::streambuf& buffer = *input.rdbuf(); + const auto start = buffer.pubseekoff(0, std::ios::cur, std::ios::in); + if (start == std::streampos(std::streamoff(-1))) { + throw FormatError("index file input must be seekable"); + } + std::array magic{}; + const std::streamsize count = buffer.sgetn(magic.data(), magic.size()); + buffer.pubseekpos(start, std::ios::in); + return count == static_cast(magic.size()) && magic == kMagic; +} + +Reader::Reader( + std::istream& input, + IndexKind expected_kind, + const std::vector& known_kinds, + const std::function& check_header +) + : input_(*input.rdbuf()) + , component_(std::string(component(expected_kind)) + " index file") { + const auto end = input_.pubseekoff(0, std::ios::end, std::ios::in); + if (end == std::streampos(std::streamoff(-1))) { + throw FormatError(component_ + " input must be seekable"); + } + const auto file_size = static_cast(static_cast(end)); + + // 1. Magic, version and header size. + if (file_size < kHeaderBytes) { + throw FormatError(component_ + " is too short for a format-2 header"); + } + std::vector header_bytes(kHeaderBytes); + read_at(0, header_bytes.data(), kHeaderBytes, "header"); + if (std::memcmp(header_bytes.data(), kMagic.data(), kMagic.size()) != 0) { + throw FormatError(component_ + " is not in format 2 (bad magic)"); + } + const uint32_t version = get_u32(header_bytes.data() + 8); + if (version != kVersion) { + throw FormatError( + component_ + " has format version " + std::to_string(version) + + "; this release reads version 2" + ); + } + const uint32_t header_size = get_u32(header_bytes.data() + 12); + if (header_size < kHeaderBytes || header_size % 8 != 0 || header_size > file_size) { + throw FormatError( + component_ + " has an invalid header size " + std::to_string(header_size) + ); + } + header_bytes.resize(header_size); + read_at( + kHeaderBytes, + header_bytes.data() + kHeaderBytes, + header_size - kHeaderBytes, + "header" + ); + + // 2. The section table lies within the file, and the header checksum matches. + const uint8_t* fields = header_bytes.data(); + const uint64_t table_offset = get_u64(fields + 96); + const uint32_t section_count = get_u32(fields + 104); + if (section_count == 0) { + throw FormatError(component_ + " has no sections"); + } + if (table_offset % 8 != 0 || table_offset < header_size || table_offset > file_size || + section_count > (file_size - table_offset) / kSectionEntryBytes) { + throw FormatError(component_ + " section table lies outside the file"); + } + const uint64_t table_end = table_offset + (kSectionEntryBytes * section_count); + std::vector table_bytes(kSectionEntryBytes * section_count); + read_at(table_offset, table_bytes.data(), table_bytes.size(), "section table"); + if (get_u32(fields + kChecksumOffset) != header_checksum(header_bytes, table_bytes)) { + throw FormatError(component_ + " header checksum mismatch (corrupt file)"); + } + + // 3. Feature flags. + const char* const writer = reinterpret_cast(fields + 112); + writer_version_.assign(writer, std::find(writer, writer + kWriterVersionBytes, '\0')); + header_.incompat_features = get_u64(fields + 16); + header_.ro_compat_features = get_u64(fields + 24); + header_.compat_features = get_u64(fields + 32); + const uint64_t unknown_incompat = header_.incompat_features & ~kKnownIncompatFeatures; + if (unknown_incompat != 0) { + throw FormatError( + component_ + " uses incompatible features " + hex(unknown_incompat) + + " that this release does not know; written by rabitqlib " + writer_version_ + ); + } + read_only_ = (header_.ro_compat_features & ~kKnownRoCompatFeatures) != 0; + + // 4. The index kind, the header rules of every kind, then the index's own. + const uint32_t kind = get_u32(fields + 40); + if (kind != static_cast(expected_kind)) { + const char* name = kind_name(kind); + if (name == nullptr) { + throw FormatError( + component_ + " has unknown index kind " + std::to_string(kind) + ); + } + throw FormatError(component_ + " holds " + name + " index"); + } + header_.index_kind = expected_kind; + header_.metric = static_cast(get_u32(fields + 44)); + header_.dim = get_u64(fields + 48); + header_.padded_dim = get_u64(fields + 56); + header_.code_bits = get_u32(fields + 64); + header_.raw_type = static_cast(get_u32(fields + 68)); + header_.id_bits = get_u32(fields + 72); + const uint32_t header_flags = get_u32(fields + 76); + const uint64_t seed = get_u64(fields + 80); + header_.num_points = get_u64(fields + 88); + if ((header_flags & ~kHeaderFlagSeedKnown) != 0) { + throw FormatError(component_ + " has unknown header flags " + hex(header_flags)); + } + if ((header_flags & kHeaderFlagSeedKnown) != 0) { + header_.seed = seed; + } else if (seed != 0) { + throw FormatError(component_ + " has a seed without the seed_known flag"); + } + if (const std::string error = header_error(header_); !error.empty()) { + throw FormatError(component_ + " " + error); + } + if (check_header) { + check_header(header_); + } + + // 5. Each entry in table order: its fields, alignment, bounds, order, overlap and + // repetition, then whether its kind is known. + std::unordered_set kinds; + uint64_t previous_end = 0; + uint64_t content_end = table_end; + for (uint32_t i = 0; i < section_count; ++i) { + const uint8_t* entry = table_bytes.data() + (i * kSectionEntryBytes); + Section section; + section.kind = get_u32(entry); + section.flags = get_u32(entry + 4); + section.offset = get_u64(entry + 8); + section.size = get_u64(entry + 16); + section.crc32 = get_u32(entry + 24); + const std::string name = + component_ + " section kind " + std::to_string(section.kind); + if (section.kind == 0) { + throw FormatError(name + " is reserved"); + } + if ((section.flags & ~kKnownSectionFlags) != 0) { + throw FormatError(name + " has unknown flags " + hex(section.flags)); + } + if (get_u32(entry + 28) != 0) { + throw FormatError(name + " has a non-zero reserved field"); + } + if ((section.flags & kSectionChecksummed) == 0 && section.crc32 != 0) { + throw FormatError(name + " has a checksum but is not checksummed"); + } + if (section.offset % kSectionAlignment != 0) { + throw FormatError(name + " is not aligned to 64 bytes"); + } + if (section.offset > file_size || section.size > file_size - section.offset) { + throw FormatError(name + " lies outside the file"); + } + const uint64_t end_offset = section.offset + section.size; + if (section.offset < previous_end) { + throw FormatError(name + " overlaps the previous section or is out of order"); + } + const bool overlaps_table = + section.size != 0 && section.offset < table_end && table_offset < end_offset; + if (section.offset < header_size || overlaps_table) { + throw FormatError(name + " overlaps the header or the section table"); + } + if (!kinds.insert(section.kind).second) { + throw FormatError(name + " is repeated"); + } + previous_end = end_offset; + content_end = std::max(content_end, end_offset); + + if (std::find(known_kinds.begin(), known_kinds.end(), section.kind) == + known_kinds.end()) { + if ((section.flags & kSectionRequired) != 0) { + throw FormatError( + name + + " is required but unknown to this release; written by rabitqlib " + + writer_version_ + ); + } + continue; + } + sections_.push_back(section); + } + + // 6. The file ends where the table or the last section ends. + if (content_end != file_size) { + throw FormatError( + component_ + " is " + std::to_string(file_size) + + " bytes, but its sections end at " + std::to_string(content_end) + ); + } + + // 7. Known checksummed sections, streamed without holding them in memory. + std::vector chunk; + for (const Section& section : sections_) { + if ((section.flags & kSectionChecksummed) == 0) { + continue; + } + chunk.resize(static_cast(std::min(section.size, kChunkBytes))); + uint32_t crc = 0; + for (uint64_t done = 0; done < section.size;) { + const auto count = + static_cast(std::min(section.size - done, kChunkBytes)); + read(section, done, chunk.data(), count); + crc = crc32(chunk.data(), count, crc); + done += count; + } + if (crc != section.crc32) { + throw FormatError( + component_ + " section kind " + std::to_string(section.kind) + + " checksum mismatch (corrupt file)" + ); + } + } +} + +const Section* Reader::find(uint32_t kind) const { + for (const Section& section : sections_) { + if (section.kind == kind) { + return §ion; + } + } + return nullptr; +} + +void Reader::read(const Section& section, uint64_t offset, void* out, size_t size) { + if (offset > section.size || size > section.size - offset) { + throw std::out_of_range(component_ + " read past the end of a section"); + } + read_at( + section.offset + offset, out, size, "section kind " + std::to_string(section.kind) + ); +} + +void Reader::read_at(uint64_t offset, void* out, size_t size, const std::string& what) { + if (size == 0) { + return; + } + const auto position = + input_.pubseekpos(static_cast(offset), std::ios::in); + if (position != std::streampos(static_cast(offset)) || + input_.sgetn(static_cast(out), static_cast(size)) != + static_cast(size)) { + throw FormatError(component_ + " " + what + " cannot be read"); + } +} + +} // namespace rabitqlib::index_detail::format2 diff --git a/src/index/format2.hpp b/src/index/format2.hpp new file mode 100644 index 00000000..62cb8911 --- /dev/null +++ b/src/index/format2.hpp @@ -0,0 +1,157 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// The index container of format 2, specified in docs/formats.md ("Index files, +// format 2"). This layer reads and writes the header, the section table and section +// bytes; each index defines its own sections and validates their contents. +namespace rabitqlib::index_detail::format2 { + +// A file that is not a valid format-2 container, or uses something this release +// does not support. +class FormatError : public std::runtime_error { + public: + using std::runtime_error::runtime_error; +}; + +inline constexpr std::array kMagic = {'R', 'A', 'B', 'Q', 'I', 'N', 'D', 'X'}; +inline constexpr uint32_t kVersion = 2; +inline constexpr uint32_t kHeaderBytes = 128; +inline constexpr uint64_t kSectionEntryBytes = 32; +inline constexpr uint64_t kSectionAlignment = 64; +inline constexpr size_t kWriterVersionBytes = 16; +// The rotator blob's limit, which bounds every index's working dimension. +inline constexpr uint64_t kMaxPaddedDim = 65536; + +// Feature bits this release knows. Version 2 defines none. +inline constexpr uint64_t kKnownIncompatFeatures = 0; +inline constexpr uint64_t kKnownRoCompatFeatures = 0; +inline constexpr uint64_t kKnownCompatFeatures = 0; + +enum class IndexKind : uint32_t { Ivf = 1, Hnsw = 2, SymphonyQg = 3 }; +enum class Metric : uint32_t { L2 = 1, InnerProduct = 2, Cosine = 3 }; +enum class RawType : uint32_t { None = 0, Float32 = 1, Uint8 = 2 }; + +inline constexpr uint32_t kSectionRequired = 1U << 0; +inline constexpr uint32_t kSectionChecksummed = 1U << 1; + +struct Header { + uint64_t incompat_features = 0; + uint64_t ro_compat_features = 0; + uint64_t compat_features = 0; + IndexKind index_kind = IndexKind::Ivf; + Metric metric = Metric::L2; + uint64_t dim = 0; + uint64_t padded_dim = 0; + uint32_t code_bits = 0; + RawType raw_type = RawType::None; + uint32_t id_bits = 32; + std::optional seed; + uint64_t num_points = 0; +}; + +// A section table entry. +struct Section { + uint32_t kind = 0; + uint32_t flags = 0; + uint64_t offset = 0; + uint64_t size = 0; + uint32_t crc32 = 0; +}; + +// CRC-32 as zlib.crc32 computes it; pass an earlier result as `crc` to continue it. +uint32_t crc32(const void* data, size_t size, uint32_t crc = 0); + +class SectionSink; + +// One section to write: `write` must pass exactly `size` bytes to the sink. Writers +// mark only small sections checksummed: those are buffered in memory before writing. +struct SectionWrite { + uint32_t kind = 0; + uint32_t flags = 0; + uint64_t size = 0; + std::function write; +}; + +// Write a complete file from the current position of `output`, which must be the +// start of the file: the header, the section table right after it, then the sections +// in the given order, each at a multiple of 64. The output is written once, front to +// back, so it need not be seekable. Invalid arguments raise std::invalid_argument and +// write nothing; a section writer that passes the wrong number of bytes raises +// std::invalid_argument after part of the file is written. +void write_file( + std::ostream& output, + const Header& header, + const std::vector& sections, + std::string_view writer_version +); + +// Receives one section's bytes during write_file; it rejects more bytes than declared. +class SectionSink { + public: + void write(const void* data, size_t size); + [[nodiscard]] uint64_t remaining() const { return remaining_; } + + private: + friend void write_file( + std::ostream&, const Header&, const std::vector&, std::string_view + ); + SectionSink(std::ostream& output, uint64_t size) : output_(output), remaining_(size) {} + + std::ostream& output_; + uint64_t remaining_; +}; + +// Whether the input starts with the format-2 magic. It reads through the stream +// buffer, so the stream's state and exception mask do not matter, and restores the +// position. Input that cannot seek raises FormatError. +bool has_magic(std::istream& input); + +class Reader { + public: + // Read and validate the header and the section table, in the order of "Reading" + // in the specification: the generic header rules, then `expected_kind`, then + // `check_header` (the index's own header rules, which throw FormatError), then + // the sections. Sections whose kind is not in `known_kinds` are rejected when + // required and skipped otherwise. The reader reads through the stream buffer of + // `input`, which must outlive it, and ignores the stream's state. + Reader( + std::istream& input, + IndexKind expected_kind, + const std::vector& known_kinds, + const std::function& check_header = {} + ); + + [[nodiscard]] const Header& header() const { return header_; } + // True when the file uses read-only-compatible features this release does not know: + // it may be searched, but add, remove and save must refuse it. + [[nodiscard]] bool read_only() const { return read_only_; } + [[nodiscard]] const std::string& writer_version() const { return writer_version_; } + // The entry of a known section kind, or nullptr when the file has none. + [[nodiscard]] const Section* find(uint32_t kind) const; + // Read `size` bytes starting `offset` bytes into `section`. + void read(const Section& section, uint64_t offset, void* out, size_t size); + + private: + void read_at(uint64_t offset, void* out, size_t size, const std::string& what); + + std::streambuf& input_; + std::string component_; + Header header_; + bool read_only_ = false; + std::string writer_version_; + std::vector
sections_; +}; + +} // namespace rabitqlib::index_detail::format2 diff --git a/tests/unit/rabitqlib/index/format2_test.cpp b/tests/unit/rabitqlib/index/format2_test.cpp new file mode 100644 index 00000000..3308c85d --- /dev/null +++ b/tests/unit/rabitqlib/index/format2_test.cpp @@ -0,0 +1,616 @@ +#include "format2.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +namespace f2 = rabitqlib::index_detail::format2; + +constexpr uint32_t kParams = 256; +constexpr uint32_t kBulk = 261; +constexpr uint32_t kEmpty = 263; +const std::vector kKnown = {kParams, kBulk, kEmpty}; + +uint32_t get_u32(const std::string& bytes, size_t offset) { + uint32_t value = 0; + for (size_t i = 0; i < 4; ++i) { + value |= static_cast(static_cast(bytes[offset + i])) << (8 * i); + } + return value; +} + +uint64_t get_u64(const std::string& bytes, size_t offset) { + uint64_t value = 0; + for (size_t i = 0; i < 8; ++i) { + value |= static_cast(static_cast(bytes[offset + i])) << (8 * i); + } + return value; +} + +void put_u32(std::string& bytes, size_t offset, uint32_t value) { + for (size_t i = 0; i < 4; ++i) { + bytes[offset + i] = static_cast(value >> (8 * i)); + } +} + +void put_u64(std::string& bytes, size_t offset, uint64_t value) { + for (size_t i = 0; i < 8; ++i) { + bytes[offset + i] = static_cast(value >> (8 * i)); + } +} + +// Recompute the header checksum after a test edits the header or the table, so that +// the check under test fires rather than the checksum. +void reseal(std::string& bytes) { + const uint32_t header_bytes = get_u32(bytes, 12); + const uint64_t table_offset = get_u64(bytes, 96); + const uint32_t count = get_u32(bytes, 104); + std::string header = bytes.substr(0, header_bytes); + put_u32(header, 108, 0); + uint32_t crc = f2::crc32(header.data(), header.size()); + crc = f2::crc32(bytes.data() + table_offset, size_t{32} * count, crc); + put_u32(bytes, 108, crc); +} + +f2::Header test_header() { + f2::Header header; + header.index_kind = f2::IndexKind::Ivf; + header.metric = f2::Metric::InnerProduct; + header.dim = 100; + header.padded_dim = 128; + header.code_bits = 7; + header.raw_type = f2::RawType::None; + header.id_bits = 32; + header.seed = 42; + header.num_points = 3; + return header; +} + +const std::string kParamBytes = "params!!"; + +std::string bulk_bytes() { + std::string bulk(1000, '\0'); + for (size_t i = 0; i < bulk.size(); ++i) { + bulk[i] = static_cast(i * 7); + } + return bulk; +} + +// A checksummed parameter section, an unchecksummed bulk section written in two +// pieces, and an empty section. Layout: header 0-127, table 128-223, parameters at +// 256, bulk at 320 (1000 bytes), the empty section at 1344, which ends the file. +std::vector test_sections() { + const std::string bulk = bulk_bytes(); + return { + {kParams, + f2::kSectionRequired | f2::kSectionChecksummed, + kParamBytes.size(), + [](f2::SectionSink& sink) { sink.write(kParamBytes.data(), kParamBytes.size()); }}, + {kBulk, + f2::kSectionRequired, + bulk.size(), + [bulk](f2::SectionSink& sink) { + sink.write(bulk.data(), 400); + sink.write(bulk.data() + 400, bulk.size() - 400); + }}, + {kEmpty, f2::kSectionRequired, 0, nullptr}}; +} + +std::string write(const f2::Header& header, const std::vector& sections) { + std::ostringstream out; + f2::write_file(out, header, sections, "0.9.0"); + return out.str(); +} + +std::string valid_file() { return write(test_header(), test_sections()); } + +// Byte offset of section table entry `i` when the table follows a 128-byte header. +size_t entry(size_t i) { return 128 + (32 * i); } + +f2::Reader open_reader(std::istream& stream, f2::IndexKind kind = f2::IndexKind::Ivf) { + return f2::Reader(stream, kind, kKnown); +} + +void expect_format_error( + const std::string& bytes, + const std::string& message, + f2::IndexKind kind = f2::IndexKind::Ivf +) { + std::istringstream stream(bytes); + try { + f2::Reader reader(stream, kind, kKnown); + ADD_FAILURE() << "expected FormatError containing: " << message; + } catch (const f2::FormatError& error) { + EXPECT_NE(std::string(error.what()).find(message), std::string::npos) + << "message: " << error.what(); + } +} + +// Input and output buffers that cannot seek, like pipes. +class SequentialInput : public std::streambuf { + public: + explicit SequentialInput(std::string bytes) : bytes_(std::move(bytes)) { + setg(bytes_.data(), bytes_.data(), bytes_.data() + bytes_.size()); + } + + private: + std::string bytes_; +}; + +class SequentialOutput : public std::streambuf { + public: + std::string bytes; + + private: + int_type overflow(int_type c) override { + if (!traits_type::eq_int_type(c, traits_type::eof())) { + bytes.push_back(traits_type::to_char_type(c)); + } + return traits_type::not_eof(c); + } + std::streamsize xsputn(const char* data, std::streamsize count) override { + bytes.append(data, static_cast(count)); + return count; + } +}; + +TEST(Format2Crc32Test, MatchesZlib) { + EXPECT_EQ(f2::crc32("", 0), 0U); + EXPECT_EQ(f2::crc32("123456789", 9), 0xCBF43926U); + std::vector data(1024); + for (size_t i = 0; i < data.size(); ++i) { + data[i] = static_cast(i); + } + EXPECT_EQ(f2::crc32(data.data(), data.size()), 0xB70B4C26U); // zlib.crc32 + const uint32_t first = f2::crc32(data.data(), 100); + EXPECT_EQ(f2::crc32(data.data() + 100, data.size() - 100, first), 0xB70B4C26U); +} + +TEST(Format2ContainerTest, RoundTripsHeaderAndSections) { + const std::string bytes = valid_file(); + std::istringstream stream(bytes); + f2::Reader reader = open_reader(stream); + + const f2::Header& header = reader.header(); + EXPECT_EQ(header.index_kind, f2::IndexKind::Ivf); + EXPECT_EQ(header.metric, f2::Metric::InnerProduct); + EXPECT_EQ(header.dim, 100U); + EXPECT_EQ(header.padded_dim, 128U); + EXPECT_EQ(header.code_bits, 7U); + EXPECT_EQ(header.raw_type, f2::RawType::None); + EXPECT_EQ(header.id_bits, 32U); + ASSERT_TRUE(header.seed.has_value()); + EXPECT_EQ(*header.seed, 42U); + EXPECT_EQ(header.num_points, 3U); + EXPECT_FALSE(reader.read_only()); + EXPECT_EQ(reader.writer_version(), "0.9.0"); + + const f2::Section* params = reader.find(kParams); + const f2::Section* bulk = reader.find(kBulk); + const f2::Section* empty = reader.find(kEmpty); + ASSERT_NE(params, nullptr); + ASSERT_NE(bulk, nullptr); + ASSERT_NE(empty, nullptr); + EXPECT_EQ(reader.find(999), nullptr); + EXPECT_EQ(params->offset, 256U); + EXPECT_EQ(bulk->offset, 320U); + EXPECT_EQ(bulk->crc32, 0U); + EXPECT_EQ(empty->offset, 1344U); // 320 + 1000, padded to 64 + EXPECT_EQ(bytes.size(), 1344U); // the empty section ends the file + + std::string read_params(params->size, '\0'); + reader.read(*params, 0, read_params.data(), read_params.size()); + EXPECT_EQ(read_params, kParamBytes); + std::string read_bulk(bulk->size - 10, '\0'); + reader.read(*bulk, 10, read_bulk.data(), read_bulk.size()); + EXPECT_EQ(read_bulk, bulk_bytes().substr(10)); + EXPECT_THROW( + reader.read(*bulk, 10, read_bulk.data(), read_bulk.size() + 1), std::out_of_range + ); +} + +TEST(Format2ContainerTest, WritesIdenticalBytesTwice) { + EXPECT_EQ(valid_file(), valid_file()); +} + +TEST(Format2ContainerTest, WritesAnUnknownSeed) { + f2::Header header = test_header(); + header.seed.reset(); + const std::string bytes = write(header, {{kEmpty, f2::kSectionRequired, 0, nullptr}}); + EXPECT_EQ(get_u32(bytes, 76), 0U); + EXPECT_EQ(get_u64(bytes, 80), 0U); + std::istringstream stream(bytes); + EXPECT_FALSE(open_reader(stream).header().seed.has_value()); +} + +TEST(Format2ContainerTest, WritesAndReadsWithoutSeeking) { + SequentialOutput output_buffer; + std::ostream output(&output_buffer); + f2::write_file(output, test_header(), test_sections(), "0.9.0"); + EXPECT_EQ(output_buffer.bytes, valid_file()); + + SequentialInput input_buffer(valid_file()); + std::istream input(&input_buffer); + EXPECT_THROW(f2::has_magic(input), f2::FormatError); + try { + f2::Reader reader(input, f2::IndexKind::Ivf, kKnown); + ADD_FAILURE() << "expected FormatError"; + } catch (const f2::FormatError& error) { + EXPECT_STREQ(error.what(), "IVF index file input must be seekable"); + } +} + +TEST(Format2ContainerTest, DetectsTheMagicAndRestoresThePosition) { + std::istringstream good(valid_file()); + EXPECT_TRUE(f2::has_magic(good)); + EXPECT_EQ(good.tellg(), std::streampos(0)); + std::istringstream legacy(std::string("RABQIDX1") + std::string(200, '\0')); + EXPECT_FALSE(f2::has_magic(legacy)); + + std::istringstream short_file("RABQ"); + short_file.exceptions(std::ios::failbit | std::ios::badbit); + EXPECT_FALSE(f2::has_magic(short_file)); + EXPECT_EQ(short_file.tellg(), std::streampos(0)); +} + +TEST(Format2ContainerTest, IgnoresTheStreamExceptionMask) { + // Truncated in the padding before the last section: the header and the table are + // read, then a section check fails. + std::istringstream stream(valid_file().substr(0, 1336)); + stream.exceptions(std::ios::failbit | std::ios::badbit | std::ios::eofbit); + EXPECT_THROW(open_reader(stream), f2::FormatError); + std::istringstream valid(valid_file()); + valid.exceptions(std::ios::failbit | std::ios::badbit | std::ios::eofbit); + EXPECT_NO_THROW(open_reader(valid)); +} + +TEST(Format2ContainerTest, WriterRejectsInvalidArgumentsBeforeWriting) { + const f2::SectionWrite empty{kEmpty, f2::kSectionRequired, 0, nullptr}; + const auto expect_rejected = [](const f2::Header& header, + const std::vector& sections, + const char* version = "0.9.0") { + std::ostringstream out; + EXPECT_THROW(f2::write_file(out, header, sections, version), std::invalid_argument); + EXPECT_TRUE(out.str().empty()); + }; + + f2::Header header = test_header(); + header.padded_dim = 100; + expect_rejected(header, {empty}); + header = test_header(); + header.index_kind = static_cast(4); + expect_rejected(header, {empty}); + for (const int mask : {0, 1, 2}) { + header = test_header(); + (mask == 0 ? header.incompat_features + : mask == 1 ? header.ro_compat_features + : header.compat_features) = 1; + expect_rejected(header, {empty}); + } + expect_rejected(test_header(), {}); + expect_rejected(test_header(), {empty, empty}); + expect_rejected(test_header(), {{0, f2::kSectionRequired, 0, nullptr}}); + expect_rejected(test_header(), {{kEmpty, 4, 0, nullptr}}); + expect_rejected( + test_header(), + {{kBulk, 0, 1, nullptr}, + {kEmpty, 0, std::numeric_limits::max() - 10, nullptr}} + ); + expect_rejected(test_header(), {empty}, "a version longer than 16"); + expect_rejected(test_header(), {empty}, "0.9\n"); +} + +TEST(Format2ContainerTest, WriterChecksSectionSizesAndPropagatesFailures) { + const auto two_bytes = [](f2::SectionSink& sink) { sink.write("ab", 2); }; + for (const uint32_t flags : {0U, f2::kSectionChecksummed}) { + EXPECT_THROW( + write(test_header(), {{kBulk, flags, 1, two_bytes}}), std::invalid_argument + ); + EXPECT_THROW( + write(test_header(), {{kBulk, flags, 3, two_bytes}}), std::invalid_argument + ); + } + const auto fails = [](f2::SectionSink&) { throw std::runtime_error("section failed"); }; + EXPECT_THROW(write(test_header(), {{kBulk, 0, 1, fails}}), std::runtime_error); + + std::ostringstream broken; + broken.setstate(std::ios::badbit); + try { + f2::write_file(broken, test_header(), test_sections(), "0.9.0"); + ADD_FAILURE() << "expected runtime_error"; + } catch (const std::runtime_error& error) { + EXPECT_STREQ(error.what(), "IVF index file cannot be written"); + } +} + +TEST(Format2ContainerTest, RejectsBrokenHeaders) { + const std::string good = valid_file(); + expect_format_error(good.substr(0, 127), "IVF index file is too short"); + + std::string bytes = good; + bytes[0] = 'X'; + expect_format_error(bytes, "IVF index file is not in format 2 (bad magic)"); + + bytes = good; + put_u32(bytes, 8, 3); + reseal(bytes); + expect_format_error(bytes, "format version 3; this release reads version 2"); + + for (const uint32_t size : {120U, 132U, 4096U}) { + bytes = good; + put_u32(bytes, 12, size); + expect_format_error(bytes, "invalid header size " + std::to_string(size)); + } + + bytes = good; + bytes[50] ^= 1; // dim, without resealing + expect_format_error(bytes, "header checksum mismatch"); + + bytes = good; + put_u32(bytes, 104, 0); + reseal(bytes); + expect_format_error(bytes, "has no sections"); + + for (const uint64_t table_offset : {uint64_t{132}, uint64_t{120}, uint64_t{1344}}) { + bytes = good; + put_u64(bytes, 96, table_offset); + expect_format_error(bytes, "section table lies outside the file"); + } + bytes = good; + put_u32(bytes, 104, 1000); + expect_format_error(bytes, "section table lies outside the file"); +} + +TEST(Format2ContainerTest, ChecksTheIndexKindBeforeTheSections) { + std::string bytes = valid_file(); + put_u32(bytes, entry(1), 777); // an unknown required section + reseal(bytes); + expect_format_error( + bytes, "HNSW index file holds an IVF-RaBitQ index", f2::IndexKind::Hnsw + ); + expect_format_error( + bytes, + "QuantizedGraph index file holds an IVF-RaBitQ index", + f2::IndexKind::SymphonyQg + ); + + const struct { + uint32_t kind; + const char* message; + } cases[] = { + {4, "IVF index file holds an IVF-SSD index"}, + {9, "IVF index file has unknown index kind 9"}, + {0, "IVF index file has unknown index kind 0"}, + }; + for (const auto& c : cases) { + bytes = valid_file(); + put_u32(bytes, 40, c.kind); + reseal(bytes); + expect_format_error(bytes, c.message); + } + + // The index's own header rules also run before the sections. + bytes = valid_file(); + put_u32(bytes, entry(1), 777); + reseal(bytes); + std::istringstream stream(bytes); + try { + f2::Reader reader(stream, f2::IndexKind::Ivf, kKnown, [](const f2::Header& header) { + if (header.id_bits != 64) { + throw f2::FormatError("IVF index file needs 64-bit IDs"); + } + }); + ADD_FAILURE() << "expected FormatError"; + } catch (const f2::FormatError& error) { + EXPECT_STREQ(error.what(), "IVF index file needs 64-bit IDs"); + } +} + +TEST(Format2ContainerTest, RejectsInvalidHeaderFields) { + const std::string good = valid_file(); + const struct { + size_t offset; + uint64_t value; + const char* message; + } cases[] = { + {44, 7, "IVF index file has unknown metric 7"}, + {44, 0, "IVF index file has unknown metric 0"}, + {48, 0, "IVF index file has dimension 0"}, + {56, 100, "has padded dimension 100, which must be a multiple of 32"}, + {56, + 96, + "has padded dimension 96, which must be a multiple of 32 from the dimension 100"}, + {56, 65568, "has padded dimension 65568, which must be a multiple of 32"}, + {64, 10, "has 10 code bits; at most 9 are supported"}, + {68, 5, "has unknown raw element type 5"}, + {72, 16, "has 16-bit IDs; only 32 and 64 are valid"}, + {76, 3, "has unknown header flags 0x3"}, + }; + for (const auto& c : cases) { + std::string bytes = good; + if (c.offset == 48 || c.offset == 56) { + put_u64(bytes, c.offset, c.value); + } else { + put_u32(bytes, c.offset, static_cast(c.value)); + } + reseal(bytes); + expect_format_error(bytes, c.message); + } + + std::string bytes = good; + put_u32(bytes, 76, 0); // seed 42 without seed_known + reseal(bytes); + expect_format_error(bytes, "has a seed without the seed_known flag"); +} + +TEST(Format2ContainerTest, AppliesFeatureFlags) { + const std::string good = valid_file(); + + std::string bytes = good; + put_u64(bytes, 16, 4); + reseal(bytes); + expect_format_error( + bytes, + "IVF index file uses incompatible features 0x4 that this release does not know; " + "written by rabitqlib 0.9.0" + ); + + bytes = good; + put_u64(bytes, 24, 1); + reseal(bytes); + std::istringstream read_only(bytes); + EXPECT_TRUE(open_reader(read_only).read_only()); + + bytes = good; + put_u64(bytes, 32, 0x80); + reseal(bytes); + std::istringstream compat(bytes); + const f2::Reader reader = open_reader(compat); + EXPECT_FALSE(reader.read_only()); + EXPECT_EQ(reader.header().compat_features, 0x80U); +} + +TEST(Format2ContainerTest, SkipsUnknownOptionalSectionsAndRejectsUnknownRequiredOnes) { + std::string bytes = valid_file(); + put_u32(bytes, entry(1), 777); + put_u32(bytes, entry(1) + 4, 0); // optional + reseal(bytes); + std::istringstream optional(bytes); + const f2::Reader reader = open_reader(optional); + EXPECT_EQ(reader.find(kBulk), nullptr); + EXPECT_EQ(reader.find(777), nullptr); + EXPECT_NE(reader.find(kParams), nullptr); + + put_u32(bytes, entry(1) + 4, f2::kSectionRequired); + reseal(bytes); + expect_format_error( + bytes, + "IVF index file section kind 777 is required but unknown to this release; written " + "by " + "rabitqlib 0.9.0" + ); + + // An unknown optional section is not read, so its checksum is not verified. + bytes = valid_file(); + put_u32(bytes, entry(0), 778); + put_u32(bytes, entry(0) + 4, f2::kSectionChecksummed); + reseal(bytes); + bytes[256] ^= 1; + std::istringstream unverified(bytes); + EXPECT_EQ(open_reader(unverified).find(kParams), nullptr); +} + +// The valid file with a 136-byte header whose extra bytes come from a later version. +std::string longer_header_file() { + const std::string good = valid_file(); + std::string bytes = good.substr(0, 128) + std::string(8, '\x5A'); + put_u32(bytes, 12, 136); + put_u64(bytes, 96, 136); + bytes += good.substr(128, 96); // the table + bytes.resize(256, '\0'); // sections start at 256 in both layouts + bytes += good.substr(256); + reseal(bytes); + return bytes; +} + +TEST(Format2ContainerTest, LoadsALongerHeaderAndChecksumsItsExtension) { + std::string bytes = longer_header_file(); + std::istringstream stream(bytes); + const f2::Reader reader = open_reader(stream); + EXPECT_EQ(reader.header().dim, 100U); + EXPECT_NE(reader.find(kBulk), nullptr); + + bytes[130] ^= 1; + expect_format_error(bytes, "header checksum mismatch"); +} + +TEST(Format2ContainerTest, LoadsATableAfterTheSections) { + const std::string good = valid_file(); + std::string bytes = good.substr(0, 128) + std::string(128, '\0') + good.substr(256); + put_u64(bytes, 96, bytes.size()); // 1344, a multiple of 8 + bytes += good.substr(128, 96); + reseal(bytes); + std::istringstream stream(bytes); + EXPECT_NE(open_reader(stream).find(kBulk), nullptr); + + // The empty last section now grows into the table. + put_u64(bytes, 1344 + 64 + 16, 50); + reseal(bytes); + expect_format_error(bytes, "section kind 263 overlaps the header or the section table"); +} + +TEST(Format2ContainerTest, RejectsBrokenSectionEntries) { + const std::string good = valid_file(); + const struct { + size_t offset; + uint64_t value; + bool wide; + const char* message; + } cases[] = { + {entry(0), 0, false, "IVF index file section kind 0 is reserved"}, + {entry(0) + 4, 0x7, false, "section kind 256 has unknown flags 0x7"}, + {entry(0) + 28, 1, false, "section kind 256 has a non-zero reserved field"}, + {entry(1) + 24, 5, false, "section kind 261 has a checksum but is not checksummed"}, + {entry(1) + 8, 328, true, "section kind 261 is not aligned to 64 bytes"}, + {entry(1) + 16, 1344, true, "section kind 261 lies outside the file"}, + {entry(1) + 16, + std::numeric_limits::max(), + true, + "261 lies outside the file"}, + {entry(1) + 8, + std::numeric_limits::max() - 63, + true, + "261 lies outside the file"}, + {entry(1) + 8, 256, true, "section kind 261 overlaps the previous section"}, + {entry(0) + 8, + 128, + true, + "section kind 256 overlaps the header or the section table"}, + {entry(0) + 8, + 0, + true, + "section kind 256 overlaps the header or the section table"}, + {entry(2), kBulk, false, "section kind 261 is repeated"}, + }; + for (const auto& c : cases) { + std::string bytes = good; + if (c.wide) { + put_u64(bytes, c.offset, c.value); + } else { + put_u32(bytes, c.offset, static_cast(c.value)); + } + reseal(bytes); + expect_format_error(bytes, c.message); + } +} + +TEST(Format2ContainerTest, RejectsTruncatedTrailingAndCorruptFiles) { + const std::string good = valid_file(); + expect_format_error( + good + std::string(64, '\0'), + "IVF index file is 1408 bytes, but its sections end at 1344" + ); + // Truncated inside the padding before the empty last section. + expect_format_error(good.substr(0, 1336), "section kind 263 lies outside the file"); + + std::string bytes = good; + bytes[256 + 2] ^= 1; // inside the checksummed parameters + expect_format_error(bytes, "section kind 256 checksum mismatch (corrupt file)"); + + bytes = good; + bytes[320 + 2] ^= 1; // bulk data is not checksummed + std::istringstream stream(bytes); + EXPECT_NO_THROW(open_reader(stream)); +} + +} // namespace From 27f5954e0724ed69ce12cc975bb5a63aec0eed7d Mon Sep 17 00:00:00 2001 From: gouyt13clear Date: Sat, 7 Nov 2026 16:16:43 +0800 Subject: [PATCH 03/11] feat(ivf): save and load index format 2 IVF-RaBitQ save writes index format 2 (#212): one file whose sections hold the rotator blob, the routing parameters and data, the codes, the rerank data and the IDs. HNSW centroid routing becomes a section, so save no longer writes a .hnsw sidecar. Load dispatches on the magic and keeps reading every earlier layout, including their sidecars. - ivf::Initializer replaces save/load on file streams with write(std::ostream&) and read(std::istream&, bytes). - The vendored hnswlib gains stream overloads of saveIndex and loadIndex; the path overloads delegate to them. - The rotator blob codec and a zlib-compatible CRC-32 move to private core sources for C++ readers and writers. - The format-2 reader reads each bulk section front to back, and the HNSW graph check tracks a byte budget instead of querying positions. - Freeze IVF files written by v0.8.3 (RABQIDX1 v2, Flat RaBitQ and HNSW routing with sidecars) as release-produced fixtures. - Document the IVF sections, the HNSW graph layout and the IVF reader rules in the specification, and note the changes for users. --- CMakeLists.txt | 5 + docs/development.md | 41 +- docs/docs/index/ivf.md | 34 +- docs/docs/index/usage.md | 18 +- docs/docs/quick_start.md | 6 +- docs/docs/releases/unreleased.md | 16 + docs/formats.md | 50 +- docs/mkdocs.yml | 1 + .../index/ivf/detail/hnsw_sidecar.hpp | 45 +- include/rabitqlib/index/ivf/initializer.hpp | 49 +- include/rabitqlib/index/ivf/ivf.hpp | 21 +- include/rabitqlib/third/hnswlib/hnswalg.h | 32 +- python_bindings/__init__.pyi | 7 +- python_bindings/ivf_bindings.cpp | 9 +- src/core/blob.cpp | 232 +++++++++ src/core/blob.hpp | 45 ++ src/core/crc32.hpp | 12 + src/index/format2.cpp | 54 +- src/index/format2.hpp | 87 +++- src/index/ivf.cpp | 467 +++++++++++++++--- src/index/ivf_initializer.cpp | 12 +- tests/CMakeLists.txt | 7 + tests/common/format2_file.hpp | 158 ++++++ tests/fixtures/persistence/.gitattributes | 1 + tests/fixtures/persistence/README.md | 8 +- .../pre-ivf-per-cluster-storage/README.md | 10 +- .../persistence/v0.8.3/CMakeLists.txt | 13 + tests/fixtures/persistence/v0.8.3/README.md | 41 ++ .../fixtures/persistence/v0.8.3/generate.cpp | 111 +++++ .../v0.8.3/ivf1_flat_matrix_l2.expected | 17 + .../v0.8.3/ivf1_flat_matrix_l2.index | Bin 0 -> 31718 bytes .../persistence/v0.8.3/ivf32_hnsw_ip.expected | 17 + .../persistence/v0.8.3/ivf32_hnsw_ip.index | Bin 0 -> 39070 bytes .../v0.8.3/ivf32_hnsw_ip.index.hnsw | Bin 0 -> 2208 bytes .../persistence/v0.8.3/ivf4_flat_l2.expected | 17 + .../persistence/v0.8.3/ivf4_flat_l2.index | Bin 0 -> 12526 bytes .../persistence/v0.8.3/ivf4_hnsw_l2.expected | 17 + .../persistence/v0.8.3/ivf4_hnsw_l2.index | Bin 0 -> 10990 bytes .../v0.8.3/ivf4_hnsw_l2.index.hnsw | Bin 0 -> 2208 bytes .../v0.8.3/ivf7_flatrabitq_ip.expected | 17 + .../v0.8.3/ivf7_flatrabitq_ip.index | Bin 0 -> 18358 bytes .../fixtures/persistence/v0.8.3/manifest.json | 22 + tests/python/format2_file.py | 89 ++++ tests/python/test_historical_persistence.py | 46 +- tests/python/test_input_boundaries.py | 7 +- tests/python/test_ivf.py | 68 ++- tests/python/test_ivf_initializers.py | 70 ++- tests/python/test_portable_indexes.py | 12 +- .../index/flat_rabitq_initializer_test.cpp | 46 +- tests/unit/rabitqlib/index/format2_test.cpp | 27 +- .../index/historical_persistence_test.cpp | 85 +++- .../rabitqlib/index/hnsw_sidecar_test.cpp | 31 +- .../unit/rabitqlib/index/initializer_test.cpp | 112 ++--- tests/unit/rabitqlib/index/ivf_test.cpp | 413 +++++++++++++++- tests/unit/rabitqlib/utils/blob_test.cpp | 206 ++++++++ 55 files changed, 2491 insertions(+), 420 deletions(-) create mode 100644 docs/docs/releases/unreleased.md create mode 100644 src/core/blob.cpp create mode 100644 src/core/blob.hpp create mode 100644 src/core/crc32.hpp create mode 100644 tests/common/format2_file.hpp create mode 100644 tests/fixtures/persistence/v0.8.3/CMakeLists.txt create mode 100644 tests/fixtures/persistence/v0.8.3/README.md create mode 100644 tests/fixtures/persistence/v0.8.3/generate.cpp create mode 100644 tests/fixtures/persistence/v0.8.3/ivf1_flat_matrix_l2.expected create mode 100644 tests/fixtures/persistence/v0.8.3/ivf1_flat_matrix_l2.index create mode 100644 tests/fixtures/persistence/v0.8.3/ivf32_hnsw_ip.expected create mode 100644 tests/fixtures/persistence/v0.8.3/ivf32_hnsw_ip.index create mode 100644 tests/fixtures/persistence/v0.8.3/ivf32_hnsw_ip.index.hnsw create mode 100644 tests/fixtures/persistence/v0.8.3/ivf4_flat_l2.expected create mode 100644 tests/fixtures/persistence/v0.8.3/ivf4_flat_l2.index create mode 100644 tests/fixtures/persistence/v0.8.3/ivf4_hnsw_l2.expected create mode 100644 tests/fixtures/persistence/v0.8.3/ivf4_hnsw_l2.index create mode 100644 tests/fixtures/persistence/v0.8.3/ivf4_hnsw_l2.index.hnsw create mode 100644 tests/fixtures/persistence/v0.8.3/ivf7_flatrabitq_ip.expected create mode 100644 tests/fixtures/persistence/v0.8.3/ivf7_flatrabitq_ip.index create mode 100644 tests/fixtures/persistence/v0.8.3/manifest.json create mode 100644 tests/python/format2_file.py create mode 100644 tests/unit/rabitqlib/utils/blob_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2dcc28ac..3d8afc4e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -84,6 +84,7 @@ endfunction() set(RABITQ_CORE_SOURCES src/core/api.cpp + src/core/blob.cpp src/core/fast_scale_table.cpp src/core/provisional.cpp src/utils/cpu_features.cpp @@ -262,6 +263,10 @@ if(RABITQ_BUILD_INDEX) ) add_library(rabitqlib::index ALIAS rabitq_index) target_link_libraries(rabitq_index PUBLIC rabitq_fastscan) + # Index files record the library version that wrote them. + target_compile_definitions(rabitq_index PRIVATE + RABITQ_LIBRARY_VERSION="${PROJECT_VERSION}" + ) set_target_properties(rabitq_index PROPERTIES EXPORT_NAME index POSITION_INDEPENDENT_CODE ON diff --git a/docs/development.md b/docs/development.md index 5800fe56..d8c43aef 100644 --- a/docs/development.md +++ b/docs/development.md @@ -505,12 +505,13 @@ in place of extra-bit codes. #### IVF formats and initialization -New IVF files have a magic/version header, an explicit padded dimension, and a resolved initializer type (v2); v1 files infer the historical -Flat/HNSW choice at 20,000 clusters, independently of current auto selection. -New IVF auto routing uses Flat below 5,000 clusters, Flat RaBitQ below 60,000, -and HNSW otherwise. Flat RaBitQ persists its mean and codes in the main file; -HNSW retains its sidecar. Legacy quantized files and raw v1 files infer -64-dimension padding. +New IVF files use index format 2 (`docs/formats.md`): the resolved initializer type +is in `ivf_params`, and the routing data, including the HNSW graph, is a section of +the one file. Earlier `RABQIDX1` v2 files store the initializer type; v1 files infer +the historical Flat/HNSW choice at 20,000 clusters, independently of current auto +selection. Those layouts keep HNSW routing in the `.hnsw` sidecar. New IVF auto +routing uses Flat below 5,000 clusters, Flat RaBitQ below 60,000, and HNSW otherwise. +Legacy quantized files and raw v1 files infer 64-dimension padding. #### Search defaults @@ -532,7 +533,7 @@ and buffer changes must also preserve the [ID marker](#search-buffer-id-marker) Filesystem path strings are UTF-8 on Windows and native path bytes on POSIX. Use `io_impl::filesystem_path` from `utils/path.hpp` before opening files or querying -filesystem metadata, including IVF's HNSW sidecar. It converts UTF-8 to native Unicode +filesystem metadata, including the HNSW sidecar of earlier IVF files. It converts UTF-8 to native Unicode on Windows and preserves existing path bytes on Linux/macOS. For path-only changes, check path conversion and the affected I/O operations; the @@ -552,13 +553,15 @@ that corrupt or mismatched files are reported before allocation, without treatin crafted file as a supported input. Do not silently reinterpret an old file. Add a magic/version discriminator, use fixed-width -serialized fields for new formats (the current IVF, HNSW and QG bodies still store several -`size_t` fields; change those only with a format version bump), validate sizes before -allocation, check every read, and retain a -compatibility test fixture or an explicit rejection path. Review each affected format; -changes to shared serialization helpers require auditing every format using them. -IVF-RaBitQ, HNSW-RaBitQ, and SymphonyQG have separate formats. SymphonyQG includes a versioned quantized format plus a legacy -raw-format fallback; preserve both unless a breaking change is explicitly requested. +serialized fields for new formats (the HNSW and QG bodies still store several `size_t` fields; +change those only with a format version bump), validate sizes before allocation, check every +read, and retain a compatibility test fixture or an explicit rejection path. Review each affected +format; changes to shared serialization helpers require auditing every format using them. +IVF-RaBitQ writes index format 2 ([`docs/formats.md`](formats.md#index-format-2), container in +`src/index/format2.*`) and still reads its earlier layouts. HNSW-RaBitQ and SymphonyQG have +separate formats. SymphonyQG includes a versioned quantized format plus a legacy raw-format +fallback; preserve both unless a breaking change is explicitly requested. When a writer moves to +a new format, freeze the last files of the old writer as release-produced fixtures first. ### Change Python bindings @@ -840,8 +843,8 @@ Centroids, data, and queries compared by the same estimator must be in the same Rotator state is part of persisted index state. Loading an index must restore the exact rotation used at construction; generating a new random rotator produces silently incorrect distances. -Honor stored padded dimensions instead of recomputing them from current defaults. IVF `RABQIDX1` -v1 and v2 files explicitly store this field; preserve it for both versions. Only legacy unversioned +Honor stored padded dimensions instead of recomputing them from current defaults. Index format 2 +and IVF `RABQIDX1` v1 and v2 files explicitly store this field; preserve it. Only legacy unversioned quantized IVF files and old raw v1 files omit it and infer historical 64-dimension padding. Unreleased pad16 files whose stored dimension is not divisible by 32 are rejected. @@ -875,9 +878,9 @@ updates or persistence. These markers affect live search as well as saved files. IVF-RaBitQ `remove` stores a removed point as `f_add = +inf` inside the existing batch data, so it needs no discriminator. No index written before `remove` existed has an infinite `f_add`, so nothing in an old file is reinterpreted. In the legacy file formats readable by release 0.5.0, that reader also -excludes removed points. This compatibility of the removal marker does not extend to the current -writer's `RABQIDX1` v2 format: older releases cannot read current saves, even when resaving a loaded -legacy index. Keep this convention when touching IVF-RaBitQ code: +excludes removed points. This compatibility of the removal marker does not extend to index +format 2, which the current writer uses: releases up to 0.8 cannot read current saves, even when +resaving a loaded legacy index. Keep this convention when touching IVF-RaBitQ code: - Every estimator and scan path must turn `f_add = +inf` into an infinite estimated distance and an infinite lower bound, and never into NaN. A NaN distance would enter the result buffer. diff --git a/docs/docs/index/ivf.md b/docs/docs/index/ivf.md index 86e11790..fdef96c7 100644 --- a/docs/docs/index/ivf.md +++ b/docs/docs/index/ivf.md @@ -457,8 +457,8 @@ The removal marker lives in the ordinary batch data and needs no additional format change; removal survives `save` and `load`. No file written before `remove` existed has an infinite `f_add`, so nothing in an old file is reinterpreted. Release 0.5.0 also excludes removed points when reading its -supported legacy formats. However, current saves always use `RABQIDX1` v2, -including when resaving a loaded legacy index. Older releases cannot read these +supported legacy formats. However, current saves always use index format 2, +including when resaving a loaded legacy index. Releases up to 0.8 cannot read these files, regardless of whether they contain removals. ## Persistence @@ -468,19 +468,23 @@ Save a constructed index with: ```c++ ivf.save(output_index_file); ``` -New files use the `RABQIDX1` magic, version 2, storage flags, an explicit -padded dimension, and a uint32 initializer type (1 = Flat, 2 = Flat RaBitQ, -3 = HNSW) before the index metadata. `Auto` is resolved before saving. Loading -restores the routing type, storage mode, rotation state, and padded dimension. -Raw files also contain the original vectors, so querying does not require an external dataset. - -Version-1 `RABQIDX1` files remain readable with their stored padding. Legacy -unversioned quantized files and raw v1 files retain their original 64-dimension -padding. All three historical formats infer routing with the original 20,000 -cluster threshold. HNSW routing still requires the accompanying `.hnsw` file. -Flat RaBitQ loads its saved codes without random re-quantization. Saving a loaded -legacy index writes the new header without changing its padded dimension or encoded data. Older library -versions cannot read the new format. +Files are written in +[index format 2](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/docs/formats.md#index-format-2): +one file with a header, a section table, and sections for the rotation, the routing +data, the codes and the IDs. HNSW routing is a section of that file, so there is no +separate sidecar. `Auto` is resolved before saving. Loading restores the routing type, +storage mode, rotation state, and padded dimension. Raw files also contain the original +vectors, so querying does not require an external dataset. Releases up to 0.8 cannot +read format 2. Indexes of more than 65,536 dimensions cannot be saved. + +Files from earlier releases remain readable. `RABQIDX1` version 2 files store their +routing type; version-1 `RABQIDX1` files keep their stored padding; legacy unversioned +quantized files and raw v1 files retain their original 64-dimension padding. These three +older layouts infer routing with the original 20,000 cluster threshold. All of these +layouts keep HNSW routing in the accompanying `.hnsw` file, which loading reads. +Flat RaBitQ loads its saved codes without random re-quantization. Saving a loaded file +writes format 2 without changing its padded dimension or encoded data; a `.hnsw` file +left beside it is not removed and is no longer used. ## Implementation details diff --git a/docs/docs/index/usage.md b/docs/docs/index/usage.md index f96c5d2f..77e54321 100644 --- a/docs/docs/index/usage.md +++ b/docs/docs/index/usage.md @@ -126,7 +126,7 @@ A conflicting call immediately raises It does not wait or queue an update. Access protection includes Python input conversion and is released when the operation returns or raises an exception. Different index objects can operate independently. Concurrent saves must use -different output paths, including any sidecar files. +different output paths. Search parameters such as `ef`, `nprobe`, precision, and `num_threads` belong to each call. When using a Python thread pool, consider `num_threads=1` to avoid @@ -156,20 +156,22 @@ C++ paths use UTF-8 on Windows and native path bytes on POSIX. Python `save`/`load` accept strings, `pathlib.Path` and `os.PathLike` objects; existing `bytes` and `bytearray` paths remain supported. POSIX paths preserve native bytes, but non-UTF-8 filenames require filesystem support. Windows byte paths use -UTF-8. Embedded null characters are rejected before file access. IVF HNSW sidecars -use the same path conversion. +UTF-8. Embedded null characters are rejected before file access. The HNSW sidecars +of IVF files from earlier releases use the same path conversion. `save` writes a temporary file beside the destination and renames it into place, so a failed save leaves an existing index unchanged. A replaced file keeps its permission bits, but not its owner or other hard links. Symbolic links and other non-regular -destinations are written in place. IVF renames its HNSW sidecar before the index file; -a failure between the two renames can leave a new sidecar beside the old index. +destinations are written in place. IVF-RaBitQ writes one file that includes its HNSW +routing graph. ## Index file compatibility -The compatibility suite loads frozen files written by v0.5.2, compares search -IDs and distances, and verifies save/load migration and rejection of truncated -or unsupported versions. See the +The compatibility suite loads frozen files written by v0.5.2 and v0.8.3, compares +search IDs and distances, and verifies save/load migration and rejection of truncated +or unsupported versions. IVF-RaBitQ saves in +[index format 2](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/docs/formats.md#index-format-2), +which releases up to 0.8 cannot read. See the [fixture provenance and coverage](https://github.com/VectorDB-NTU/RaBitQ-Library/tree/main/tests/fixtures/persistence). Compatibility is format-specific; preserving old readers' ability to open new files is not guaranteed. Consult each index's format documentation before diff --git a/docs/docs/quick_start.md b/docs/docs/quick_start.md index 99032928..c2be40f4 100644 --- a/docs/docs/quick_start.md +++ b/docs/docs/quick_start.md @@ -122,9 +122,9 @@ with TemporaryDirectory() as directory: np.testing.assert_allclose(loaded_distances, distances) ``` -The loaded index owns its data and restores the saved rotation. IVF-RaBitQ indexes using -HNSW centroid routing also need the accompanying `.hnsw` file; keep it with the -main index. This small example uses Flat routing and has no sidecar. +The loaded index owns its data and restores the saved rotation. IVF-RaBitQ saves one +file, including its HNSW centroid routing; files from releases up to 0.8 with HNSW +routing also need the accompanying `.hnsw` file. See [file compatibility](index/usage.md#index-file-compatibility) before downgrading. ## Next steps diff --git a/docs/docs/releases/unreleased.md b/docs/docs/releases/unreleased.md new file mode 100644 index 00000000..1768230d --- /dev/null +++ b/docs/docs/releases/unreleased.md @@ -0,0 +1,16 @@ +# Unreleased + +## Index files + +- IVF-RaBitQ `save` writes [index format 2](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/docs/formats.md#index-format-2), a single file with a header, a section table and checksummed metadata. HNSW centroid routing is stored inside it, so `save` no longer writes a `.hnsw` sidecar. A sidecar left by an earlier save is not removed. +- Releases up to 0.8 cannot read these files. Keep a copy saved by 0.8 if you may downgrade. +- Files written by every earlier release still load, including the `.hnsw` sidecars of HNSW-routed files; saving one writes format 2 with the same rotation, codes and results. +- Loading a damaged or mismatched format-2 file raises `RuntimeError` with a message naming the problem, for example `IVF index file is 6916 bytes, but its sections end at 6852` for trailing bytes. +- `save` rejects IVF-RaBitQ indexes of more than 65,536 dimensions. +- Errors in an HNSW routing graph name the graph rather than the sidecar: `Truncated HNSW centroid graph` and `HNSW centroid graph changed while loading` replace their `sidecar` forms. A missing sidecar of an earlier file still raises `Cannot open HNSW centroid sidecar`. +- A format-2 file that uses read-only-compatible features unknown to this release loads for search only: `add`, `reserve`, `remove` and `save` raise until `construct` rebuilds the index. + +## C++ changes + +- `ivf::Initializer` and its subclasses replace `save(std::ofstream&, const char*)` and `load(std::ifstream&, const char*)` with `write(std::ostream&)` and `read(std::istream&, uint64_t bytes)`. The HNSW initializer no longer writes a sidecar itself. +- The vendored hnswlib adds `saveIndex(std::ostream&)` and `loadIndex(std::istream&, size_t bytes, ...)`; the path overloads are unchanged. diff --git a/docs/formats.md b/docs/formats.md index c4e3f55b..b7f97059 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -93,9 +93,11 @@ The body is what `Rotator::save()` writes, and `body_bytes` must equal it The body defines the rotation; `seed` only records where it came from. A seeded FHT/Kac rotation is identical on every platform, while a matrix drawn from a seed is reproducible only with the same standard library and build, which is why the matrix itself is stored. + + ## Index files, format 2 (draft) -> **Status: draft for 0.9 (#211, #212).** The container layer (header, section table, checksums) is implemented in `src/index/format2.*`, but no index reads or writes format 2 yet. Field numbers, section kinds and the open questions at the end may still change; until the format ships, no file in this layout exists. +> **Status: draft for 0.9 (#211, #212).** The container (`src/index/format2.*`) is implemented, and IVF-RaBitQ reads and writes format 2; HNSW-RaBitQ and SymphonyQG do not yet, and no index stores the cosine metric or a seed yet. Field numbers, section kinds and the open questions at the end may still change until a release writes format 2. Format 2 is one container for IVF-RaBitQ, HNSW-RaBitQ and SymphonyQG, and later for the SSD-resident IVF index from RaBitQ-SSD. It replaces the per-index layouts: IVF-RaBitQ `RABQIDX1` versions 1 and 2, the unversioned HNSW-RaBitQ layout, SymphonyQG's quantized version 1 and its raw layout, and the unversioned hnswlib sidecar `.hnsw` of IVF-RaBitQ. Every released layout stays readable (see [Earlier layouts](#earlier-layouts)); `save` always writes format 2. @@ -173,7 +175,7 @@ The three masks follow the ext4 superblock: they say what an older reader may do | Mask | A reader that does not know a set bit | | --- | --- | | `incompat_features` | Rejects the file; reading it would give wrong results | -| `ro_compat_features` | May load and search; `add`, `remove` and `save` raise, because they would leave the unknown data stale | +| `ro_compat_features` | May load and search; operations that change or save the index (`add`, `reserve`, `remove`, `save`) raise, because they would leave the unknown data stale. Rebuilding the index with `construct` replaces that data, so the rebuilt index is writable | | `compat_features` | Ignores the bit; `save` clears it | The error names the unknown bits and `writer_version`, such as "index file uses incompatible features 0x4 that this release does not know; written by rabitqlib 0.10.0". @@ -202,13 +204,13 @@ Kinds 1–255 are shared, 256–511 are IVF-RaBitQ, 512–767 HNSW-RaBitQ, 768 | Kind | Name | Used by | Content | | --- | --- | --- | --- | -| 1 | `rotator` | All | A [rotator blob](#rotator-kind-1-version-1), byte for byte what `Rotator.to_bytes()` writes. Its `dim` and `padded_dim` must equal the header's | +| 1 | `rotator` | All | A [rotator blob](#rotator-kind-1-version-1), byte for byte what `Rotator.to_bytes()` writes. Its `dim` and `padded_dim` must equal the header's. Not checksummed by the container: the blob carries its own CRC-32 | | 2 | `companions` | IVF-SSD | The [companion table](#companion-files) | | 256 | `ivf_params` | IVF | `num_clusters` u64; `initializer` u32 (1 = flat, 2 = FlatRaBitQ, 3 = HNSW); reserved u32 | | 257 | `ivf_cluster_sizes` | IVF | `num_clusters` × u64; the sum equals `num_points` | -| 258 | `ivf_centroids` | IVF, flat and FlatRaBitQ | `num_clusters × padded_dim` f32, rotated | -| 259 | `ivf_flat_rabitq` | IVF, FlatRaBitQ | The global centroid (`padded_dim` f32), then the centroids' [FastScan blocks](#fastscan-blocks-version-1) | -| 260 | `ivf_hnsw_routing` | IVF, HNSW | The centroid graph, byte for byte the current `.hnsw` sidecar (hnswlib layout) | +| 258 | `ivf_flat_routing` | IVF, flat | The rotated centroids, `num_clusters × padded_dim` f32 | +| 259 | `ivf_flat_rabitq_routing` | IVF, FlatRaBitQ | The rotated centroids as above, the global centroid (their mean, `padded_dim` f32), then `ceil(num_clusters / 32)` [FastScan blocks](#fastscan-blocks-version-1) of the centroids, quantized against the global centroid under the index metric | +| 260 | `ivf_hnsw_routing` | IVF, HNSW | The [HNSW routing graph](#hnsw-routing-graph), byte for byte the `.hnsw` sidecar of earlier releases | | 261 | `ivf_codes` | IVF | Per cluster in cluster order, `ceil(size / 32)` [FastScan blocks](#fastscan-blocks-version-1). Removed points have `add = +inf` | | 262 | `ivf_rerank` | IVF, `code_bits > 1` or raw | Per point in cluster order: a [single-code record](#single-code-record) with `b = code_bits - 1`, or `dim` raw values of `raw_type` | | 263 | `ivf_ids` | IVF | Per point in cluster order, `id_bits`-wide IDs | @@ -221,6 +223,8 @@ Kinds 1–255 are shared, 256–511 are IVF-RaBitQ, 512–767 HNSW-RaBitQ, 768 | 769 | `qg_centroid` | SymphonyQG, `code_bits > 0` | `padded_dim` f32 | | 770 | `qg_records` | SymphonyQG | `num_points` [records](#symphonyqg-records) | +An IVF file has exactly one routing section, the one that `ivf_params` names; a rerank section exactly when `code_bits > 1` or raw vectors are stored. + The HNSW per-point length prefix of the current layout becomes `hnsw_levels`, so a reader knows every size before reading the link lists. ### Record layouts @@ -262,6 +266,28 @@ Let `L = 4 * (base_max_degree + 1)` and `U = 4 * (upper_max_degree + 1)`. A point at level `l > 0` has `l` upper-layer link lists in `hnsw_upper_links`, for levels 1 to `l`, each `U` bytes: a u16 count, 2 reserved bytes, then `upper_max_degree` neighbor IDs. +#### HNSW routing graph + +The centroid graph of IVF-RaBitQ's HNSW routing is the layout hnswlib's `saveIndex` writes. Unlike the rest of format 2 it is not defined field by field by this library, so it is fixed here as written on the supported 64-bit little-endian platforms (`size_t` is 8 bytes, `int` and `unsigned` 4): + +| Offset | Size | Type | Field | Rule | +| --- | --- | --- | --- | --- | +| 0 | 8 | u64 | `offset_level0` | 0 | +| 8 | 8 | u64 | `max_elements` | At least `count` | +| 16 | 8 | u64 | `count` | `num_clusters` | +| 24 | 8 | u64 | `record_bytes` | `label_offset + 8` | +| 32 | 8 | u64 | `label_offset` | `data_offset + 4 * padded_dim` | +| 40 | 8 | u64 | `data_offset` | `4 + 4 * max_m0` | +| 48 | 4 | i32 | `max_level` | The entry point's level, at least 0 | +| 52 | 4 | u32 | `entry_point` | Below `count` | +| 56 | 8 | u64 | `max_m` | `m` | +| 64 | 8 | u64 | `max_m0` | `2 * m` | +| 72 | 8 | u64 | `m` | 2 to 10,000 | +| 80 | 8 | f64 | `level_multiplier` | Finite and positive | +| 88 | 8 | u64 | `ef_construction` | At least `m` | + +Then `count` base records of `record_bytes`: a u16 neighbor count of at most `max_m0`, a flags byte whose bit 0 (hnswlib's deletion mark) must be clear, a reserved byte, `max_m0` u32 neighbor IDs, the rotated centroid (`padded_dim` f32) and its label (u64). Then, per point in order, a u32 byte length followed by that many bytes of upper-layer link lists: `level` lists of `4 + 4 * max_m` bytes each, laid out as in the base record. Every neighbor ID is below `count` and has at least the level of the list that holds it. + #### SymphonyQG records Every vertex has `max_degree` neighbor slots, a multiple of 32. Let `V` be the size of the vector part: `4 * dim` for raw vectors, or the [single-code record](#single-code-record) size with `b = code_bits` otherwise. @@ -274,6 +300,18 @@ Every vertex has `max_degree` neighbor slots, a multiple of 32. Let `V` be the s Unused lanes of a partial neighbor block have zero factors, and their IDs are unspecified. +### IVF-RaBitQ files + +Besides the rules above, an IVF-RaBitQ reader requires: + +- `num_points` from 1 to 2^31, and every stored ID below it; +- `code_bits` from 1 to 9, with `raw_type = float32` only when `code_bits = 1`, and `raw_type` none or float32; +- `id_bits = 32`; +- in `ivf_params`, `num_clusters` from 1 to 2^31, `initializer` 1 to 3, and the reserved field zero; +- cluster sizes that add up to `num_points`. + +This release also rejects the cosine metric, which no IVF-RaBitQ file stores yet. + ### Companion files Only IVF-SSD uses them. `.ssd` holds the page-packed inverted lists, read with `O_DIRECT` and too large to keep in the main file. The `companions` section lists each one: diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index a1dfd332..7b40b1af 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -14,6 +14,7 @@ nav: - C++ Integration: cpp_integration.md # Group by minor version: nested groups stay collapsed except the current page's. - Release notes: + - Unreleased: releases/unreleased.md - "0.8": - "0.8.3": releases/0.8.3.md - "0.8.2": releases/0.8.2.md diff --git a/include/rabitqlib/index/ivf/detail/hnsw_sidecar.hpp b/include/rabitqlib/index/ivf/detail/hnsw_sidecar.hpp index 32bde339..0b3f07b6 100644 --- a/include/rabitqlib/index/ivf/detail/hnsw_sidecar.hpp +++ b/include/rabitqlib/index/ivf/detail/hnsw_sidecar.hpp @@ -5,37 +5,39 @@ #include #include #include -#include #include +#include #include #include -#include #include "rabitqlib/third/hnswlib/hnswlib.h" -#include "rabitqlib/utils/path.hpp" namespace rabitqlib::ivf::detail { -// The HNSW initializer stores its graph at the index path plus this suffix. Saving, -// loading and atomic replacement must all agree on it. +// Releases before index format 2 stored the HNSW routing graph at the index path plus +// this suffix; loading those files reads it from there. inline constexpr char kHnswSidecarSuffix[] = ".hnsw"; -// hnswlib's native sidecar has no version tag. Check the existing writer's -// header and record lengths before its loader uses offsets or allocates links. -// As with the main index file, callers must not mutate the file during loading. -inline void validate_hnsw_sidecar_file( - const std::string& path, size_t expected_count, size_t vector_bytes +// hnswlib's native layout has no version tag. Check the header and record lengths of +// the `bytes` bytes at the current position of `input` before hnswlib's loader uses +// offsets or allocates links, then restore the position. As with the index file, +// callers must not mutate the data during loading. +inline void validate_hnsw_graph( + std::istream& input, uint64_t bytes, size_t expected_count, size_t vector_bytes ) { - std::ifstream input(io_impl::filesystem_path(path), std::ios::binary | std::ios::ate); - if (!input) { - throw std::runtime_error("Cannot open HNSW centroid sidecar"); + const std::streampos begin = input.tellg(); + if (begin == std::streampos(std::streamoff(-1))) { + throw std::runtime_error("Cannot read HNSW centroid graph"); } - const auto end = input.tellg(); - input.seekg(0); + // Stay within the graph's bytes, as a budget rather than by querying the position: + // in an index file, other data follows them. + auto remaining = static_cast(bytes); const auto read = [&](auto& value) { - if (!input.read(reinterpret_cast(&value), sizeof(value))) { - throw std::runtime_error("Truncated HNSW centroid sidecar"); + if (remaining < static_cast(sizeof(value)) || + !input.read(reinterpret_cast(&value), sizeof(value))) { + throw std::runtime_error("Truncated HNSW centroid graph"); } + remaining -= static_cast(sizeof(value)); }; size_t level0_offset = 0, capacity = 0, count = 0, level0_record_bytes = 0; size_t label_offset = 0, data_offset = 0; @@ -78,8 +80,7 @@ inline void validate_hnsw_sidecar_file( if (max_level < 0 || entry >= count) { throw std::runtime_error("Invalid HNSW centroid entry point or maximum level"); } - auto remaining = end - input.tellg(); - if (remaining < 0 || count > static_cast(remaining) / level0_record_bytes) { + if (count > static_cast(remaining) / level0_record_bytes) { throw std::runtime_error("Truncated HNSW centroid base records"); } const auto level0_records_bytes = @@ -89,11 +90,7 @@ inline void validate_hnsw_sidecar_file( size_t highest_level = 0, entry_level = 0; for (size_t i = 0; i < count; ++i) { unsigned int link_bytes = 0; // Native type written by hnswlib::saveIndex. - if (remaining < static_cast(sizeof(link_bytes))) { - throw std::runtime_error("Truncated HNSW centroid sidecar"); - } read(link_bytes); - remaining -= static_cast(sizeof(link_bytes)); if (link_bytes % upper_layer_links_bytes != 0 || link_bytes > static_cast(remaining)) { throw std::runtime_error("Invalid HNSW centroid upper-link length"); @@ -114,6 +111,8 @@ inline void validate_hnsw_sidecar_file( entry_level != highest_level) { throw std::runtime_error("Invalid HNSW centroid level structure"); } + input.clear(); + input.seekg(begin); } inline void validate_hnsw_centroid_graph(const hnswlib::HierarchicalNSW& graph) { diff --git a/include/rabitqlib/index/ivf/initializer.hpp b/include/rabitqlib/index/ivf/initializer.hpp index 618d6731..aa18bddf 100644 --- a/include/rabitqlib/index/ivf/initializer.hpp +++ b/include/rabitqlib/index/ivf/initializer.hpp @@ -3,10 +3,12 @@ #include #include #include +#include #include -#include #include +#include #include +#include #include #include #include @@ -61,8 +63,11 @@ class Initializer { virtual void centroids_distances( const float* query, size_t nprobe, std::vector>& candidates ) const = 0; - virtual void load(std::ifstream& input, const char* filename) = 0; - virtual void save(std::ofstream& output, const char* filename) const = 0; + // Write the routing data that read() restores. + virtual void write(std::ostream& output) const = 0; + // Read `bytes` bytes that write() wrote, from the current position of `input`. + // Data that does not fit this initializer raises std::runtime_error. + virtual void read(std::istream& input, uint64_t bytes) = 0; }; inline Initializer::~Initializer() = default; @@ -113,8 +118,8 @@ class FlatInitializer : public Initializer { std::copy_n(centroid_candidates.begin(), num_probes, candidates.begin()); } - // The flat initializer stores its centroids inline in the index file stream. - void save(std::ofstream& output, const char* /*filename*/) const override { + // The flat initializer stores only its centroids. + void write(std::ostream& output) const override { output.write( reinterpret_cast(centroids_.data()), static_cast(sizeof(float) * dim_ * num_cluster_) @@ -124,10 +129,12 @@ class FlatInitializer : public Initializer { } } - void load(std::ifstream& input, const char* /*filename*/) override { + void read(std::istream& input, uint64_t bytes) override { + if (bytes != sizeof(float) * dim_ * num_cluster_) { + throw std::runtime_error("IVF Flat initializer data has the wrong size"); + } input.read( - reinterpret_cast(centroids_.data()), - static_cast(sizeof(float) * dim_ * num_cluster_) + reinterpret_cast(centroids_.data()), static_cast(bytes) ); if (!input) { throw std::runtime_error("IVF Flat initializer cannot be read"); @@ -153,8 +160,8 @@ class FlatRaBitQInitializer : public Initializer { void centroids_distances( const float* query, size_t nprobe, std::vector>& candidates ) const override; - void save(std::ofstream& output, const char* filename) const override; - void load(std::ifstream& input, const char* filename) override; + void write(std::ostream& output) const override; + void read(std::istream& input, uint64_t bytes) override; }; // Keep centroid routing on the same runtime-selected distance kernels as flat IVF. @@ -291,25 +298,23 @@ class HNSWInitializer : public Initializer { } } - // The HNSW initializer stores its graph in a separate hnswlib sidecar file. - void save(std::ofstream& /*output*/, const char* filename) const override { - alg_hnsw_->saveIndex(std::string(filename) + detail::kHnswSidecarSuffix); - } + // The HNSW initializer stores its graph in hnswlib's own layout, which earlier + // releases wrote to the sidecar file .hnsw. + void write(std::ostream& output) const override { alg_hnsw_->saveIndex(output); } - void load(std::ifstream& /*input*/, const char* filename) override { - const std::string sidecar = std::string(filename) + detail::kHnswSidecarSuffix; - detail::validate_hnsw_sidecar_file(sidecar, num_cluster_, space_->get_data_size()); - alg_hnsw_->loadIndex(sidecar, space_, num_cluster_); + void read(std::istream& input, uint64_t bytes) override { + detail::validate_hnsw_graph(input, bytes, num_cluster_, space_->get_data_size()); + alg_hnsw_->loadIndex(input, static_cast(bytes), space_, num_cluster_); const auto& graph = *alg_hnsw_; - // validate_hnsw_sidecar_file already checked these header fields, and loadIndex - // reads them again from a second open of the file. Recheck the loaded values so - // a sidecar replaced in between cannot give centroid() an out-of-record offset. + // validate_hnsw_graph already checked these header fields, and loadIndex reads + // them again. Recheck the loaded values so that data changed in between cannot + // give centroid() an out-of-record offset. if (graph.offsetData_ > graph.label_offset_ || graph.label_offset_ > graph.size_data_per_element_ || graph.label_offset_ - graph.offsetData_ != space_->get_data_size() || graph.size_data_per_element_ - graph.label_offset_ != sizeof(hnswlib::labeltype)) { - throw std::runtime_error("HNSW centroid sidecar changed while loading"); + throw std::runtime_error("HNSW centroid graph changed while loading"); } detail::validate_hnsw_centroid_graph(graph); rebuild_label_lookup(); diff --git a/include/rabitqlib/index/ivf/ivf.hpp b/include/rabitqlib/index/ivf/ivf.hpp index 745ed983..ec75e19b 100644 --- a/include/rabitqlib/index/ivf/ivf.hpp +++ b/include/rabitqlib/index/ivf/ivf.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -90,7 +91,10 @@ class IVF { size_t num_clusters_ = 0; bool raw_reranking_ = false; // raw vectors replace extra-bit codes bool ready_ = false; // centroids set, construction or loading completed - size_t ex_bits_ = 0; // total bits = ex_bits_ + 1; 0 with raw reranking + // Loaded from a file with read-only-compatible features this release does not know: + // search works, but add, reserve, remove and save refuse the index. + bool read_only_ = false; + size_t ex_bits_ = 0; // total bits = ex_bits_ + 1; 0 with raw reranking RotatorType rotator_type_ = RotatorType::FhtKacRotator; std::unique_ptr> rotator_; std::vector clusters_; @@ -190,9 +194,22 @@ class IVF { const float* query, size_t k, size_t nprobe, const PID* result_ids ) const; - // Write the index file, and any initializer sidecar, at exactly this path. + // Write the index file in format 2 at exactly this path. void write_file(const char* filename) const; + // Load a format-2 file into this freshly created index. + void read_format2(std::istream& input); + + // Bytes of Flat or FlatRaBitQ routing data; HNSW routing has no fixed size. + [[nodiscard]] size_t flat_routing_bytes() const; + + // Check the stored IDs, create the cluster views, count removed points and mark the + // index ready: the end of loading in every format. + void complete_load(const std::vector& cluster_sizes); + + // Refuse an operation that would modify or save a read-only index. + void check_writable(const char* operation) const; + public: explicit IVF() noexcept; // dim is the original input dimension; total_bits is 1-9 for quantized diff --git a/include/rabitqlib/third/hnswlib/hnswalg.h b/include/rabitqlib/third/hnswlib/hnswalg.h index c8ff85d1..d6ccf26a 100644 --- a/include/rabitqlib/third/hnswlib/hnswalg.h +++ b/include/rabitqlib/third/hnswlib/hnswalg.h @@ -688,8 +688,14 @@ class HierarchicalNSW : public AlgorithmInterface { if (!output.is_open()) throw std::runtime_error("Cannot open HNSW index file for writing"); output.exceptions(std::ios::failbit | std::ios::badbit); - std::streampos position; + saveIndex(output); + output.close(); + } + + // RaBitQ: the stream overloads let an index embed the graph in a section of its own + // file. The bytes are those of the path overload, which delegates to them. + void saveIndex(std::ostream &output) { writeBinaryPOD(output, offsetLevel0_); writeBinaryPOD(output, max_elements_); writeBinaryPOD(output, cur_element_count); @@ -713,7 +719,6 @@ class HierarchicalNSW : public AlgorithmInterface { if (linkListSize) output.write(linkLists_[i], linkListSize); } - output.close(); } @@ -723,11 +728,20 @@ class HierarchicalNSW : public AlgorithmInterface { if (!input.is_open()) throw std::runtime_error("Cannot open file"); - clear(); // get file size: input.seekg(0, input.end); - std::streampos total_filesize = input.tellg(); + const std::streamoff total_filesize = input.tellg(); input.seekg(0, input.beg); + loadIndex(input, static_cast(total_filesize), s, max_elements_i); + input.close(); + } + + + // RaBitQ: read a graph of `total_bytes` bytes that starts at the current position. + void loadIndex(std::istream &input, size_t total_bytes, SpaceInterface *s, size_t max_elements_i = 0) { + clear(); + const std::streampos begin = input.tellg(); + const std::streampos end = begin + static_cast(total_bytes); readBinaryPOD(input, offsetLevel0_); readBinaryPOD(input, max_elements_); @@ -758,7 +772,7 @@ class HierarchicalNSW : public AlgorithmInterface { /// Optional - check if index is ok: input.seekg(cur_element_count * size_data_per_element_, input.cur); for (size_t i = 0; i < cur_element_count; i++) { - if (input.tellg() < 0 || input.tellg() >= total_filesize) { + if (input.tellg() < begin || input.tellg() >= end) { throw std::runtime_error("Index seems to be corrupted or unsupported"); } @@ -770,13 +784,13 @@ class HierarchicalNSW : public AlgorithmInterface { } // throw exception if it either corrupted or old index - if (input.tellg() != total_filesize) + if (input.tellg() != end) throw std::runtime_error("Index seems to be corrupted or unsupported"); input.clear(); /// Optional check end - input.seekg(pos, input.beg); + input.seekg(pos); data_level0_memory_ = (char *) malloc(max_elements * size_data_per_element_); if (data_level0_memory_ == nullptr) @@ -819,10 +833,6 @@ class HierarchicalNSW : public AlgorithmInterface { if (allow_replace_deleted_) deleted_elements.insert(i); } } - - input.close(); - - return; } diff --git a/python_bindings/__init__.pyi b/python_bindings/__init__.pyi index 62d4f0b5..3cdffe8b 100644 --- a/python_bindings/__init__.pyi +++ b/python_bindings/__init__.pyi @@ -434,7 +434,7 @@ class IvfIndex: Notes ----- A trained index without vectors raises RuntimeError; add vectors first. - IVF with HNSW centroid routing also writes its required sidecar file. + The one file includes HNSW centroid routing; releases up to 0.8 cannot read it. """ @staticmethod @@ -451,6 +451,11 @@ class IvfIndex: ------- index : IvfIndex A trained index ready for search. + + Notes + ----- + Files from releases up to 0.8 with HNSW centroid routing also need the + accompanying ``.hnsw`` file. """ @property diff --git a/python_bindings/ivf_bindings.cpp b/python_bindings/ivf_bindings.cpp index 779eae5d..3aa38cab 100644 --- a/python_bindings/ivf_bindings.cpp +++ b/python_bindings/ivf_bindings.cpp @@ -747,7 +747,7 @@ path : str, bytes, bytearray, or os.PathLike Notes ----- A trained index without vectors raises RuntimeError; add vectors first. -IVF with HNSW centroid routing also writes its required sidecar file.)doc" +The one file includes HNSW centroid routing; releases up to 0.8 cannot read it.)doc" ) .def_static( "load", @@ -764,7 +764,12 @@ path : str, bytes, bytearray, or os.PathLike Returns ------- index : IvfIndex - A trained index ready for search.)doc" + A trained index ready for search. + +Notes +----- +Files from releases up to 0.8 with HNSW centroid routing also need the +accompanying ``.hnsw`` file.)doc" ) .def( "__repr__", diff --git a/src/core/blob.cpp b/src/core/blob.cpp new file mode 100644 index 00000000..3d8132aa --- /dev/null +++ b/src/core/blob.cpp @@ -0,0 +1,232 @@ +#include "blob.hpp" + +#include +#include +#include +#include + +#include "crc32.hpp" + +namespace rabitqlib::detail { +namespace { + +constexpr std::array kBlobMagic = {'R', 'A', 'B', 'Q', 'B', 'L', 'O', 'B'}; +constexpr size_t kCommonHeaderBytes = 40; +constexpr size_t kChecksumOffset = 32; +constexpr uint32_t kRotatorKind = 1; +constexpr uint32_t kRotatorVersion = 1; +constexpr uint32_t kSeedKnown = 1; +constexpr uint32_t kMethodFhtKac = 1; +constexpr uint32_t kMethodMatrix = 2; +constexpr uint32_t kScalarFloat32 = 1; +constexpr uint32_t kScalarFloat64 = 2; +constexpr size_t kMaxRotatorDim = rotator_impl::FhtKacRotator::kMaxDim; + +constexpr std::array make_crc_table() { + std::array table{}; + for (uint32_t i = 0; i < 256; ++i) { + uint32_t value = i; + for (int bit = 0; bit < 8; ++bit) { + value = (value & 1U) != 0 ? 0xEDB88320U ^ (value >> 1) : value >> 1; + } + table[i] = value; + } + return table; +} + +constexpr std::array kCrcTable = make_crc_table(); + +void put_u32(uint8_t* out, uint32_t value) { + for (size_t i = 0; i < 4; ++i) { + out[i] = static_cast(value >> (8 * i)); + } +} + +void put_u64(uint8_t* out, uint64_t value) { + for (size_t i = 0; i < 8; ++i) { + out[i] = static_cast(value >> (8 * i)); + } +} + +uint32_t get_u32(const uint8_t* in) { + uint32_t value = 0; + for (size_t i = 0; i < 4; ++i) { + value |= static_cast(in[i]) << (8 * i); + } + return value; +} + +uint64_t get_u64(const uint8_t* in) { + uint64_t value = 0; + for (size_t i = 0; i < 8; ++i) { + value |= static_cast(in[i]) << (8 * i); + } + return value; +} + +// The blob checksum: the whole blob with its crc32 field taken as zero. +uint32_t blob_checksum(const uint8_t* blob, size_t size) { + constexpr std::array kZero{}; + uint32_t crc = crc32(blob, kChecksumOffset); + crc = crc32(kZero.data(), kZero.size(), crc); + return crc32(blob + kChecksumOffset + 4, size - kChecksumOffset - 4, crc); +} + +// The same rules and messages as rabitqlib.quant's Rotator. +void check_dimensions(RotatorType type, size_t dim, size_t padded_dim) { + if (dim == 0 || dim > kMaxRotatorDim) { + throw std::invalid_argument( + "dim must be in [1, " + std::to_string(kMaxRotatorDim) + "]; got " + + std::to_string(dim) + ); + } + if (type == RotatorType::FhtKacRotator && dim < 64) { + throw std::invalid_argument( + "dim must be at least 64 for method 'fht_kac'; got " + std::to_string(dim) + ); + } + if (padded_dim % 32 != 0 || padded_dim < dim || padded_dim > kMaxRotatorDim) { + throw std::invalid_argument( + "padded_dim must be a multiple of 32 in [dim, " + + std::to_string(kMaxRotatorDim) + "]; got " + std::to_string(padded_dim) + ); + } +} + +size_t body_bytes(RotatorType type, size_t dim, size_t padded_dim) { + return type == RotatorType::FhtKacRotator ? padded_dim / 2 + : sizeof(float) * dim * padded_dim; +} + +} // namespace + +uint32_t crc32(const void* data, size_t size, uint32_t crc) { + const auto* bytes = static_cast(data); + crc = ~crc; + for (size_t i = 0; i < size; ++i) { + crc = kCrcTable[(crc ^ bytes[i]) & 0xFFU] ^ (crc >> 8); + } + return ~crc; +} + +size_t rotator_blob_bytes(RotatorType type, size_t dim, size_t padded_dim) { + return kRotatorBlobHeaderBytes + body_bytes(type, dim, padded_dim); +} + +void encode_rotator_blob( + const Rotator& rotator, + RotatorType type, + size_t dim, + std::optional seed, + uint8_t* out +) { + const size_t padded_dim = rotator.size(); + check_dimensions(type, dim, padded_dim); + const size_t body = body_bytes(type, dim, padded_dim); + if (rotator.dump_bytes() != body) { + throw std::invalid_argument("rotator does not match its type and dimensions"); + } + const size_t size = kRotatorBlobHeaderBytes + body; + std::memset(out, 0, kRotatorBlobHeaderBytes); + std::memcpy(out, kBlobMagic.data(), kBlobMagic.size()); + put_u32(out + 8, kRotatorKind); + put_u32(out + 12, kRotatorVersion); + put_u32(out + 16, static_cast(kRotatorBlobHeaderBytes)); + put_u32(out + 20, seed.has_value() ? kSeedKnown : 0); + put_u64(out + 24, body); + put_u32(out + 40, type == RotatorType::FhtKacRotator ? kMethodFhtKac : kMethodMatrix); + put_u32(out + 44, kScalarFloat32); + put_u32(out + 48, static_cast(dim)); + put_u32(out + 52, static_cast(padded_dim)); + put_u32(out + 56, seed.value_or(0)); + rotator.save(reinterpret_cast(out + kRotatorBlobHeaderBytes)); + put_u32(out + kChecksumOffset, blob_checksum(out, size)); +} + +DecodedRotator decode_rotator_blob(const uint8_t* data, size_t size) { + if (size < kCommonHeaderBytes) { + throw std::invalid_argument("data is too short for a rabitqlib blob"); + } + if (std::memcmp(data, kBlobMagic.data(), kBlobMagic.size()) != 0) { + throw std::invalid_argument("data is not a rabitqlib blob (bad magic)"); + } + const uint32_t kind = get_u32(data + 8); + if (kind != kRotatorKind) { + throw std::invalid_argument( + "data is not a rotator blob; got kind " + std::to_string(kind) + ); + } + const uint32_t version = get_u32(data + 12); + if (version != kRotatorVersion) { + throw std::invalid_argument( + "data has unsupported rotator format version " + std::to_string(version) + + "; this release reads version " + std::to_string(kRotatorVersion) + ); + } + if (get_u32(data + 16) != kRotatorBlobHeaderBytes) { + throw std::invalid_argument( + "data has an invalid header size for rotator version 1" + ); + } + const uint64_t body = get_u64(data + 24); + if (size < kRotatorBlobHeaderBytes || body != size - kRotatorBlobHeaderBytes) { + throw std::invalid_argument( + "data length does not match its header (truncated or padded)" + ); + } + if (get_u32(data + kChecksumOffset) != blob_checksum(data, size)) { + throw std::invalid_argument("data checksum mismatch (corrupt blob)"); + } + const uint32_t flags = get_u32(data + 20); + if ((flags & ~kSeedKnown) != 0) { + throw std::invalid_argument("data has unknown flags"); + } + if (get_u32(data + 36) != 0 || get_u32(data + 60) != 0) { + throw std::invalid_argument("data has non-zero reserved fields"); + } + const uint32_t method = get_u32(data + 40); + if (method != kMethodFhtKac && method != kMethodMatrix) { + throw std::invalid_argument( + "data has an unknown rotator method " + std::to_string(method) + ); + } + const uint32_t scalar = get_u32(data + 44); + if (scalar == kScalarFloat64) { + throw std::invalid_argument( + "data stores a float64 rotator, which version 1 cannot load" + ); + } + if (scalar != kScalarFloat32) { + throw std::invalid_argument( + "data has an unknown scalar type " + std::to_string(scalar) + ); + } + + DecodedRotator result; + result.type = + method == kMethodFhtKac ? RotatorType::FhtKacRotator : RotatorType::MatrixRotator; + result.dim = get_u32(data + 48); + result.padded_dim = get_u32(data + 52); + try { + check_dimensions(result.type, result.dim, result.padded_dim); + } catch (const std::invalid_argument& error) { + throw std::invalid_argument( + std::string("data has invalid rotator dimensions: ") + error.what() + ); + } + const uint32_t seed = get_u32(data + 56); + if ((flags & kSeedKnown) != 0) { + result.seed = seed; + } else if (seed != 0) { + throw std::invalid_argument("data has a seed without the seed_known flag"); + } + if (body != body_bytes(result.type, result.dim, result.padded_dim)) { + throw std::invalid_argument("data body size does not match the rotator parameters"); + } + result.rotator = + rotator_impl::make_rotator_for_loading(result.dim, result.type, result.padded_dim); + result.rotator->load(reinterpret_cast(data + kRotatorBlobHeaderBytes)); + return result; +} + +} // namespace rabitqlib::detail diff --git a/src/core/blob.hpp b/src/core/blob.hpp new file mode 100644 index 00000000..c4c514d2 --- /dev/null +++ b/src/core/blob.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include + +#include "rabitqlib/core/rotator.hpp" +#include "rabitqlib/core/types.hpp" + +// The self-describing blobs of docs/formats.md ("Blobs"), for C++ readers and writers +// such as index files; an implementation detail, not installed. rabitqlib.quant keeps +// its own rotator blob code, which uses zlib's faster checksum; both are tested against +// the same golden files. +namespace rabitqlib::detail { + +inline constexpr size_t kRotatorBlobHeaderBytes = 64; + +// The size of the rotator blob for these parameters, which must be valid. +size_t rotator_blob_bytes(RotatorType type, size_t dim, size_t padded_dim); + +// Write the blob of `rotator`, which has the given type and dimensions, to `out`, +// which holds rotator_blob_bytes(type, dim, rotator.size()) bytes. Dimensions the +// format does not allow raise std::invalid_argument. +void encode_rotator_blob( + const Rotator& rotator, + RotatorType type, + size_t dim, + std::optional seed, + uint8_t* out +); + +struct DecodedRotator { + RotatorType type = RotatorType::FhtKacRotator; + size_t dim = 0; + size_t padded_dim = 0; + std::optional seed; + std::unique_ptr> rotator; +}; + +// Validate and decode a rotator blob of `size` bytes. Invalid data raises +// std::invalid_argument with the messages rabitqlib.quant uses, which start with "data". +DecodedRotator decode_rotator_blob(const uint8_t* data, size_t size); + +} // namespace rabitqlib::detail diff --git a/src/core/crc32.hpp b/src/core/crc32.hpp new file mode 100644 index 00000000..69ad4975 --- /dev/null +++ b/src/core/crc32.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +namespace rabitqlib::detail { + +// CRC-32 as zlib.crc32 computes it; pass an earlier result as `crc` to continue it. +// Byte-wise: meant for headers and metadata, not bulk data. +uint32_t crc32(const void* data, size_t size, uint32_t crc = 0); + +} // namespace rabitqlib::detail diff --git a/src/index/format2.cpp b/src/index/format2.cpp index c5b1ff98..8f557382 100644 --- a/src/index/format2.cpp +++ b/src/index/format2.cpp @@ -8,28 +8,18 @@ #include #include +#include "../core/crc32.hpp" + namespace rabitqlib::index_detail::format2 { namespace { +using rabitqlib::detail::crc32; + constexpr uint32_t kHeaderFlagSeedKnown = 1U << 0; constexpr uint32_t kKnownSectionFlags = kSectionRequired | kSectionChecksummed; constexpr size_t kChecksumOffset = 108; constexpr size_t kChunkBytes = size_t{1} << 16; -constexpr std::array make_crc_table() { - std::array table{}; - for (uint32_t i = 0; i < 256; ++i) { - uint32_t value = i; - for (int bit = 0; bit < 8; ++bit) { - value = (value & 1U) != 0 ? 0xEDB88320U ^ (value >> 1) : value >> 1; - } - table[i] = value; - } - return table; -} - -constexpr std::array kCrcTable = make_crc_table(); - void put_u32(uint8_t* out, uint32_t value) { for (size_t i = 0; i < 4; ++i) { out[i] = static_cast(value >> (8 * i)); @@ -191,15 +181,6 @@ void write_zeros(std::ostream& output, uint64_t count) { } // namespace -uint32_t crc32(const void* data, size_t size, uint32_t crc) { - const auto* bytes = static_cast(data); - crc = ~crc; - for (size_t i = 0; i < size; ++i) { - crc = kCrcTable[(crc ^ bytes[i]) & 0xFFU] ^ (crc >> 8); - } - return ~crc; -} - void SectionSink::write(const void* data, size_t size) { if (size > remaining_) { throw std::invalid_argument("index section writer wrote more bytes than declared"); @@ -339,11 +320,13 @@ void write_file( } } -bool has_magic(std::istream& input) { +bool has_magic(std::istream& input, IndexKind kind) { std::streambuf& buffer = *input.rdbuf(); const auto start = buffer.pubseekoff(0, std::ios::cur, std::ios::in); if (start == std::streampos(std::streamoff(-1))) { - throw FormatError("index file input must be seekable"); + throw FormatError( + std::string(component(kind)) + " index file input must be seekable" + ); } std::array magic{}; const std::streamsize count = buffer.sgetn(magic.data(), magic.size()); @@ -576,6 +559,27 @@ void Reader::read(const Section& section, uint64_t offset, void* out, size_t siz ); } +SectionInput Reader::open(const Section& section) { + const std::string name = component_ + " section kind " + std::to_string(section.kind); + const auto offset = static_cast(section.offset); + if (input_.pubseekpos(offset, std::ios::in) != std::streampos(offset)) { + throw FormatError(name + " cannot be read"); + } + return SectionInput(input_, name, section.size); +} + +void SectionInput::read(void* out, size_t size) { + if (size > remaining_) { + throw std::out_of_range(name_ + " read past its end"); + } + if (size != 0 && + input_.sgetn(static_cast(out), static_cast(size)) != + static_cast(size)) { + throw FormatError(name_ + " cannot be read"); + } + remaining_ -= size; +} + void Reader::read_at(uint64_t offset, void* out, size_t size, const std::string& what) { if (size == 0) { return; diff --git a/src/index/format2.hpp b/src/index/format2.hpp index 62cb8911..705e7031 100644 --- a/src/index/format2.hpp +++ b/src/index/format2.hpp @@ -46,6 +46,9 @@ enum class RawType : uint32_t { None = 0, Float32 = 1, Uint8 = 2 }; inline constexpr uint32_t kSectionRequired = 1U << 0; inline constexpr uint32_t kSectionChecksummed = 1U << 1; +// The section kind every index shares: its rotator blob. +inline constexpr uint32_t kRotatorSection = 1; + struct Header { uint64_t incompat_features = 0; uint64_t ro_compat_features = 0; @@ -70,9 +73,6 @@ struct Section { uint32_t crc32 = 0; }; -// CRC-32 as zlib.crc32 computes it; pass an earlier result as `crc` to continue it. -uint32_t crc32(const void* data, size_t size, uint32_t crc = 0); - class SectionSink; // One section to write: `write` must pass exactly `size` bytes to the sink. Writers @@ -113,10 +113,85 @@ class SectionSink { uint64_t remaining_; }; +// An output stream that passes its bytes to a section sink, for section writers that +// take a std::ostream. A sink error propagates as an exception. +class SinkStream : public std::ostream { + public: + explicit SinkStream(SectionSink& sink) : std::ostream(nullptr), buffer_(sink) { + rdbuf(&buffer_); + exceptions(std::ios::badbit); + } + + private: + class Buffer : public std::streambuf { + public: + explicit Buffer(SectionSink& sink) : sink_(sink) {} + + private: + int_type overflow(int_type c) override { + if (!traits_type::eq_int_type(c, traits_type::eof())) { + const char value = traits_type::to_char_type(c); + sink_.write(&value, 1); + } + return traits_type::not_eof(c); + } + std::streamsize xsputn(const char* data, std::streamsize count) override { + sink_.write(data, static_cast(count)); + return count; + } + + SectionSink& sink_; + }; + + Buffer buffer_; +}; + +// An output stream that only counts its bytes, to size a section before writing it. +class CountingStream : public std::ostream { + public: + CountingStream() : std::ostream(nullptr) { rdbuf(&buffer_); } + [[nodiscard]] uint64_t count() const { return buffer_.count; } + + private: + class Buffer : public std::streambuf { + public: + uint64_t count = 0; + + private: + int_type overflow(int_type c) override { + count += traits_type::eq_int_type(c, traits_type::eof()) ? 0 : 1; + return traits_type::not_eof(c); + } + std::streamsize xsputn(const char* /*data*/, std::streamsize n) override { + count += static_cast(n); + return n; + } + }; + + Buffer buffer_; +}; + // Whether the input starts with the format-2 magic. It reads through the stream // buffer, so the stream's state and exception mask do not matter, and restores the -// position. Input that cannot seek raises FormatError. -bool has_magic(std::istream& input); +// position. Input that cannot seek raises FormatError, named for `kind`. +bool has_magic(std::istream& input, IndexKind kind); + +// Reads one section front to back in pieces, seeking only once. Other reads of the +// same input move its position, so finish one section before reading another. +class SectionInput { + public: + // Read the next `size` bytes of the section. + void read(void* out, size_t size); + + private: + friend class Reader; + SectionInput(std::streambuf& input, const std::string& name, uint64_t size) + : input_(input), name_(name), remaining_(size) {} + + std::streambuf& input_; + std::string name_; + uint64_t remaining_; +}; class Reader { public: @@ -142,6 +217,8 @@ class Reader { [[nodiscard]] const Section* find(uint32_t kind) const; // Read `size` bytes starting `offset` bytes into `section`. void read(const Section& section, uint64_t offset, void* out, size_t size); + // Read `section` from its start in consecutive pieces; for large sections. + SectionInput open(const Section& section); private: void read_at(uint64_t offset, void* out, size_t size, const std::string& what); diff --git a/src/index/ivf.cpp b/src/index/ivf.cpp index abc91769..961a7dcf 100644 --- a/src/index/ivf.cpp +++ b/src/index/ivf.cpp @@ -20,7 +20,9 @@ #include #include +#include "../core/blob.hpp" #include "file_replacement.hpp" +#include "format2.hpp" #include "ivf_search.hpp" #include "rabitqlib/clustering/detail/kmeans.hpp" #include "rabitqlib/clustering/qgkmeans.hpp" @@ -59,6 +61,58 @@ constexpr const char* kPointCountConstructOnly = "IVF index created with a point count must be built by construct"; // Scratch for the rows that flat routing in add rotates at a time. constexpr size_t kRoutingBlockBytes = size_t{128} << 20; + +// Index format 2 sections of IVF-RaBitQ (docs/formats.md, "Section kinds"). +constexpr uint32_t kParamsSection = 256; +constexpr uint32_t kClusterSizesSection = 257; +constexpr uint32_t kFlatRoutingSection = 258; +constexpr uint32_t kFlatRaBitQRoutingSection = 259; +constexpr uint32_t kHnswRoutingSection = 260; +constexpr uint32_t kCodesSection = 261; +constexpr uint32_t kRerankSection = 262; +constexpr uint32_t kIdsSection = 263; +constexpr size_t kParamsBytes = 16; +// The rotator blob stores dimensions in 32 bits, up to the FHT/Kac limit. +constexpr size_t kMaxSavedDim = rotator_impl::FhtKacRotator::kMaxDim; + +uint32_t routing_section(InitializerType type) { + switch (type) { + case InitializerType::Flat: + return kFlatRoutingSection; + case InitializerType::FlatRaBitQ: + return kFlatRaBitQRoutingSection; + default: + return kHnswRoutingSection; + } +} + +void put_u32(uint8_t* out, uint32_t value) { + for (size_t i = 0; i < 4; ++i) { + out[i] = static_cast(value >> (8 * i)); + } +} + +void put_u64(uint8_t* out, uint64_t value) { + for (size_t i = 0; i < 8; ++i) { + out[i] = static_cast(value >> (8 * i)); + } +} + +uint32_t get_u32(const uint8_t* in) { + uint32_t value = 0; + for (size_t i = 0; i < 4; ++i) { + value |= static_cast(in[i]) << (8 * i); + } + return value; +} + +uint64_t get_u64(const uint8_t* in) { + uint64_t value = 0; + for (size_t i = 0; i < 8; ++i) { + value |= static_cast(in[i]) << (8 * i); + } + return value; +} } // namespace IVF::IVF() noexcept = default; @@ -147,6 +201,7 @@ void IVF::swap(IVF& other) noexcept { swap(num_clusters_, other.num_clusters_); swap(raw_reranking_, other.raw_reranking_); swap(ready_, other.ready_); + swap(read_only_, other.read_only_); swap(ex_bits_, other.ex_bits_); swap(rotator_type_, other.rotator_type_); swap(rotator_, other.rotator_); @@ -393,6 +448,7 @@ void IVF::construct( void IVF::allocate_storage(const std::vector& cluster_sizes) { ready_ = false; + read_only_ = false; num_removed_ = 0; free_storage(); clusters_.clear(); @@ -477,6 +533,7 @@ void IVF::add( "IVF index must be trained, constructed or loaded before add" ); } + check_writable("add"); if (num_points == 0) { return; } @@ -781,6 +838,7 @@ void IVF::reserve(PID cluster_id, size_t num_points) { if (!ready_) { throw std::logic_error("IVF index must be constructed or loaded before reserve"); } + check_writable("reserve"); if (cluster_id >= num_clusters_) { throw std::invalid_argument("IVF cluster ID is out of range"); } @@ -819,6 +877,7 @@ size_t IVF::remove(const PID* ids_to_remove, size_t num_points) { if (!ready_) { throw std::logic_error("IVF index must be constructed or loaded before remove"); } + check_writable("remove"); if (num_points == 0) { return 0; } @@ -869,82 +928,116 @@ void IVF::save(const char* filename) const { if (filename == nullptr || filename[0] == '\0') { throw std::invalid_argument("IVF save filename must not be empty"); } - - // An HNSW initializer writes its sidecar next to the path it is given. - const auto write = [this](const std::string& path) { write_file(path.c_str()); }; - if (initializer_type_ == InitializerType::HNSW) { - index_detail::save_replacing(filename, {detail::kHnswSidecarSuffix}, write); - } else { - index_detail::save_replacing(filename, {}, write); + check_writable("save"); + if (dim_ > kMaxSavedDim) { + throw std::invalid_argument( + "IVF cannot save an index of more than " + std::to_string(kMaxSavedDim) + + " dimensions" + ); } + index_detail::save_replacing(filename, {}, [this](const std::string& path) { + write_file(path.c_str()); + }); } void IVF::write_file(const char* filename) const { + namespace f2 = index_detail::format2; std::ofstream output(io_impl::filesystem_path(filename), std::ios::binary); if (!output.is_open()) { throw std::runtime_error("IVF cannot open index file for writing"); } output.exceptions(std::ios::failbit | std::ios::badbit); - const uint32_t flags = raw_reranking_ ? 1 : 0; - const uint64_t stored_padded_dim = padded_dim_; - output.write(reinterpret_cast(&kFormatMagic), sizeof(kFormatMagic)); - output.write(reinterpret_cast(&kFormatVersion), sizeof(kFormatVersion)); - output.write(reinterpret_cast(&flags), sizeof(flags)); - output.write( - reinterpret_cast(&stored_padded_dim), sizeof(stored_padded_dim) - ); - const auto stored_initializer = static_cast(initializer_type_); - output.write( - reinterpret_cast(&stored_initializer), sizeof(stored_initializer) + f2::Header header; + header.index_kind = f2::IndexKind::Ivf; + header.metric = metric_type_ == METRIC_IP ? f2::Metric::InnerProduct : f2::Metric::L2; + header.dim = dim_; + header.padded_dim = padded_dim_; + header.code_bits = raw_reranking_ ? 1 : static_cast(ex_bits_ + 1); + header.raw_type = raw_reranking_ ? f2::RawType::Float32 : f2::RawType::None; + header.id_bits = 32; + header.num_points = max_elements_; + + std::vector rotator( + rabitqlib::detail::rotator_blob_bytes(rotator_type_, dim_, padded_dim_) ); - - // Save metadata - output.write(reinterpret_cast(&max_elements_), sizeof(size_t)); - output.write(reinterpret_cast(&dim_), sizeof(size_t)); - output.write(reinterpret_cast(&num_clusters_), sizeof(size_t)); - output.write(reinterpret_cast(&ex_bits_), sizeof(size_t)); - output.write(reinterpret_cast(&rotator_type_), sizeof(rotator_type_)); - output.write(reinterpret_cast(&metric_type_), sizeof(metric_type_)); - - // Save the number of points in each cluster - std::vector cluster_sizes; - cluster_sizes.reserve(num_clusters_); - for (const auto& cluster : clusters_) { - cluster_sizes.push_back(cluster.num()); - } - output.write( - reinterpret_cast(cluster_sizes.data()), - static_cast(sizeof(size_t) * num_clusters_) + rabitqlib::detail::encode_rotator_blob( + *rotator_, rotator_type_, dim_, std::nullopt, rotator.data() ); - - // Save rotator - rotator_->save(output); - - // Save data: initializer payload, then all batch data, all ex data and all IDs, - // each in cluster order - initializer_->save(output, filename); + std::array params{}; + put_u64(params.data(), num_clusters_); + put_u32(params.data() + 8, static_cast(initializer_type_)); + std::vector cluster_sizes(num_clusters_); + std::vector sizes(sizeof(uint64_t) * num_clusters_); + uint64_t codes_bytes = 0; + uint64_t rerank_bytes = 0; for (size_t i = 0; i < num_clusters_; ++i) { - output.write( - cluster_storage_[i].batch_data(), - static_cast(batch_data_bytes(cluster_sizes[i])) - ); + cluster_sizes[i] = clusters_[i].num(); + put_u64(sizes.data() + (sizeof(uint64_t) * i), cluster_sizes[i]); + codes_bytes += batch_data_bytes(cluster_sizes[i]); + rerank_bytes += has_rerank_data() ? rerank_data_bytes(cluster_sizes[i]) : 0; } + f2::CountingStream routing_size; + initializer_->write(routing_size); + + const auto bytes = [](const auto& buffer) { + return + [&buffer](f2::SectionSink& sink) { sink.write(buffer.data(), buffer.size()); }; + }; + std::vector sections = { + {f2::kRotatorSection, f2::kSectionRequired, rotator.size(), bytes(rotator)}, + {kParamsSection, + f2::kSectionRequired | f2::kSectionChecksummed, + params.size(), + bytes(params)}, + {kClusterSizesSection, + f2::kSectionRequired | f2::kSectionChecksummed, + sizes.size(), + bytes(sizes)}, + {routing_section(initializer_type_), + f2::kSectionRequired, + routing_size.count(), + [this](f2::SectionSink& sink) { + f2::SinkStream stream(sink); + initializer_->write(stream); + }}, + {kCodesSection, + f2::kSectionRequired, + codes_bytes, + [&](f2::SectionSink& sink) { + for (size_t i = 0; i < num_clusters_; ++i) { + sink.write( + cluster_storage_[i].batch_data(), batch_data_bytes(cluster_sizes[i]) + ); + } + }}, + }; if (has_rerank_data()) { - for (size_t i = 0; i < num_clusters_; ++i) { - output.write( - cluster_storage_[i].rerank_data(), - static_cast(rerank_data_bytes(cluster_sizes[i])) - ); - } - } - for (size_t i = 0; i < num_clusters_; ++i) { - output.write( - reinterpret_cast(cluster_storage_[i].ids()), - static_cast(ids_bytes(cluster_sizes[i])) + sections.push_back( + {kRerankSection, + f2::kSectionRequired, + rerank_bytes, + [&](f2::SectionSink& sink) { + for (size_t i = 0; i < num_clusters_; ++i) { + sink.write( + cluster_storage_[i].rerank_data(), + rerank_data_bytes(cluster_sizes[i]) + ); + } + }} ); } - + sections.push_back( + {kIdsSection, + f2::kSectionRequired, + ids_bytes(max_elements_), + [&](f2::SectionSink& sink) { + for (size_t i = 0; i < num_clusters_; ++i) { + sink.write(cluster_storage_[i].ids(), ids_bytes(cluster_sizes[i])); + } + }} + ); + f2::write_file(output, header, sections, RABITQ_LIBRARY_VERSION); output.close(); } @@ -956,7 +1049,14 @@ void IVF::load(const char* filename) { if (!input.is_open()) { throw std::runtime_error("Cannot open IVF index file"); } + if (index_detail::format2::has_magic(input, index_detail::format2::IndexKind::Ivf)) { + IVF loaded; + loaded.read_format2(input); + swap(loaded); + return; + } + // An earlier layout: RABQIDX1, the raw-vector v1 layout or the unversioned one. IVF loaded; input.exceptions(std::ios::failbit | std::ios::badbit); input.seekg(0, std::ios::end); @@ -1088,7 +1188,22 @@ void IVF::load(const char* filename) { // Load data: initializer payload, then all batch data, all ex data and all IDs, // each in cluster order loaded.allocate_storage(cluster_sizes); - loaded.initializer_->load(input, filename); + if (loaded.initializer_type_ == InitializerType::HNSW) { + // These layouts kept the routing graph in a sidecar file beside the index. + const std::string sidecar = std::string(filename) + detail::kHnswSidecarSuffix; + std::ifstream graph( + io_impl::filesystem_path(sidecar), std::ios::binary | std::ios::ate + ); + if (!graph) { + throw std::runtime_error("Cannot open HNSW centroid sidecar"); + } + const auto graph_bytes = + static_cast(static_cast(graph.tellg())); + graph.seekg(0); + loaded.initializer_->read(graph, graph_bytes); + } else { + loaded.initializer_->read(input, loaded.flat_routing_bytes()); + } for (size_t i = 0; i < loaded.num_clusters_; ++i) { input.read( loaded.cluster_storage_[i].batch_data(), @@ -1109,37 +1224,239 @@ void IVF::load(const char* filename) { static_cast(ids_bytes(cluster_sizes[i])) ); } - for (size_t i = 0; i < loaded.num_clusters_; ++i) { - for (PID id : loaded.cluster_storage_[i].id_storage) { - if (id >= loaded.max_elements_ || id >= buffer::kSearchBufferCheckedMask) { + if (remaining != 0) { + throw std::runtime_error("IVF index file has unexpected trailing data"); + } + loaded.complete_load(cluster_sizes); + + input.close(); + swap(loaded); +} + +void IVF::read_format2(std::istream& input) { + namespace f2 = index_detail::format2; + const std::vector known_kinds = { + f2::kRotatorSection, + kParamsSection, + kClusterSizesSection, + kFlatRoutingSection, + kFlatRaBitQRoutingSection, + kHnswRoutingSection, + kCodesSection, + kRerankSection, + kIdsSection}; + f2::Reader reader(input, f2::IndexKind::Ivf, known_kinds, [](const f2::Header& header) { + if (header.metric == f2::Metric::Cosine) { + throw f2::FormatError( + "IVF index file uses the cosine metric, which this release does not support" + ); + } + if (header.id_bits != 32) { + throw f2::FormatError( + "IVF index file has 64-bit IDs; IVF-RaBitQ stores 32 bits" + ); + } + if (header.raw_type == f2::RawType::Uint8) { + throw f2::FormatError( + "IVF index file has uint8 raw vectors, which IVF-RaBitQ lacks" + ); + } + if (header.code_bits == 0 || + (header.raw_type == f2::RawType::Float32 && header.code_bits != 1)) { + throw f2::FormatError( + "IVF index file has " + std::to_string(header.code_bits) + + " code bits, which its raw vectors do not allow" + ); + } + if (header.num_points == 0 || + header.num_points > buffer::kSearchBufferMaxPointCount) { + throw f2::FormatError( + "IVF index file has an invalid point count " + + std::to_string(header.num_points) + ); + } + }); + const f2::Header& header = reader.header(); + metric_type_ = header.metric == f2::Metric::InnerProduct ? METRIC_IP : METRIC_L2; + dim_ = static_cast(header.dim); + padded_dim_ = static_cast(header.padded_dim); + raw_reranking_ = header.raw_type == f2::RawType::Float32; + ex_bits_ = raw_reranking_ ? 0 : header.code_bits - 1; + max_elements_ = static_cast(header.num_points); + + // A section of this kind, whose size, when given, must match before it is read. + const auto require = [&reader](uint32_t kind, std::optional size) { + const f2::Section* section = reader.find(kind); + if (section == nullptr) { + throw f2::FormatError( + "IVF index file has no section kind " + std::to_string(kind) + ); + } + if (size.has_value() && section->size != *size) { + throw f2::FormatError( + "IVF index file section kind " + std::to_string(kind) + " has " + + std::to_string(section->size) + " bytes; expected " + std::to_string(*size) + ); + } + return section; + }; + const auto read_all = [&reader](const f2::Section& section) { + std::vector bytes(static_cast(section.size)); + reader.read(section, 0, bytes.data(), bytes.size()); + return bytes; + }; + + const std::vector params = read_all(*require(kParamsSection, kParamsBytes)); + const uint64_t num_clusters = get_u64(params.data()); + const uint32_t initializer = get_u32(params.data() + 8); + if (num_clusters == 0 || num_clusters > buffer::kSearchBufferMaxPointCount) { + throw f2::FormatError( + "IVF index file has an invalid cluster count " + std::to_string(num_clusters) + ); + } + if (initializer < static_cast(InitializerType::Flat) || + initializer > static_cast(InitializerType::HNSW) || + get_u32(params.data() + 12) != 0) { + throw f2::FormatError("IVF index file has invalid routing parameters"); + } + num_clusters_ = static_cast(num_clusters); + initializer_type_ = static_cast(initializer); + + const std::vector sizes = + read_all(*require(kClusterSizesSection, sizeof(uint64_t) * num_clusters_)); + std::vector cluster_sizes(num_clusters_); + uint64_t total = 0; + uint64_t codes_bytes = 0; + uint64_t rerank_bytes = 0; + for (size_t i = 0; i < num_clusters_; ++i) { + const uint64_t size = get_u64(sizes.data() + (sizeof(uint64_t) * i)); + if (size > max_elements_ - total) { + throw f2::FormatError("IVF index file cluster sizes exceed its point count"); + } + total += size; + cluster_sizes[i] = static_cast(size); + codes_bytes += batch_data_bytes(cluster_sizes[i]); + rerank_bytes += has_rerank_data() ? rerank_data_bytes(cluster_sizes[i]) : 0; + } + if (total != max_elements_) { + throw f2::FormatError( + "IVF index file cluster sizes do not add up to its point count" + ); + } + + const std::vector blob = read_all(*require(f2::kRotatorSection, std::nullopt)); + rabitqlib::detail::DecodedRotator rotator; + try { + rotator = rabitqlib::detail::decode_rotator_blob(blob.data(), blob.size()); + } catch (const std::invalid_argument& error) { + throw f2::FormatError( + std::string("IVF index file has an invalid rotator (") + error.what() + ")" + ); + } + if (rotator.dim != dim_ || rotator.padded_dim != padded_dim_) { + throw f2::FormatError("IVF index file rotator does not match its dimensions"); + } + rotator_type_ = rotator.type; + rotator_ = std::move(rotator.rotator); + + const uint32_t routing_kind = routing_section(initializer_type_); + for (const uint32_t kind : + {kFlatRoutingSection, kFlatRaBitQRoutingSection, kHnswRoutingSection}) { + if (kind != routing_kind && reader.find(kind) != nullptr) { + throw f2::FormatError("IVF index file has routing data of another initializer"); + } + } + const f2::Section* routing = require( + routing_kind, + initializer_type_ == InitializerType::HNSW + ? std::nullopt + : std::optional(flat_routing_bytes()) + ); + const f2::Section* codes = require(kCodesSection, codes_bytes); + const f2::Section* rerank = nullptr; + if (has_rerank_data()) { + rerank = require(kRerankSection, rerank_bytes); + } else if (reader.find(kRerankSection) != nullptr) { + throw f2::FormatError( + "IVF index file has rerank data that its code bits do not use" + ); + } + const f2::Section* ids = require(kIdsSection, ids_bytes(max_elements_)); + + allocate_storage(cluster_sizes); + std::istream routing_input(input.rdbuf()); + routing_input.seekg(static_cast(routing->offset)); + initializer_->read(routing_input, routing->size); + // Read each bulk section front to back: a seek per cluster would discard the file + // buffer every time. + f2::SectionInput codes_input = reader.open(*codes); + for (size_t i = 0; i < num_clusters_; ++i) { + codes_input.read( + cluster_storage_[i].batch_data(), batch_data_bytes(cluster_sizes[i]) + ); + } + if (rerank != nullptr) { + f2::SectionInput rerank_input = reader.open(*rerank); + for (size_t i = 0; i < num_clusters_; ++i) { + rerank_input.read( + cluster_storage_[i].rerank_data(), rerank_data_bytes(cluster_sizes[i]) + ); + } + } + f2::SectionInput ids_input = reader.open(*ids); + for (size_t i = 0; i < num_clusters_; ++i) { + ids_input.read(cluster_storage_[i].ids(), ids_bytes(cluster_sizes[i])); + } + complete_load(cluster_sizes); + read_only_ = reader.read_only(); +} + +size_t IVF::flat_routing_bytes() const { + const size_t centroids = sizeof(float) * num_clusters_ * padded_dim_; + if (initializer_type_ == InitializerType::FlatRaBitQ) { + return centroids + (sizeof(float) * padded_dim_) + + (div_round_up(num_clusters_, fastscan::kBatchSize) * batch_bytes()); + } + return centroids; +} + +void IVF::complete_load(const std::vector& cluster_sizes) { + for (size_t i = 0; i < num_clusters_; ++i) { + for (PID id : cluster_storage_[i].id_storage) { + if (id >= max_elements_ || id >= buffer::kSearchBufferCheckedMask) { throw std::runtime_error("Invalid point ID in IVF index file"); } } } - loaded.create_cluster_views(cluster_sizes); + create_cluster_views(cluster_sizes); // Removal markers are already persisted, including in historical files. // Count only logical rows: FastScan padding lanes are not stored points. - for (const auto& cluster : loaded.clusters_) { + for (const auto& cluster : clusters_) { for (size_t first = 0; first < cluster.num(); first += fastscan::kBatchSize) { ConstBatchDataMap batch( - cluster.batch_data() + - (first / fastscan::kBatchSize) * loaded.batch_bytes(), - loaded.padded_dim_ + cluster.batch_data() + (first / fastscan::kBatchSize) * batch_bytes(), + padded_dim_ ); const size_t batch_size = std::min(fastscan::kBatchSize, cluster.num() - first); for (size_t lane = 0; lane < batch_size; ++lane) { - loaded.num_removed_ += batch.f_add()[lane] == kRemovedFAdd; + num_removed_ += batch.f_add()[lane] == kRemovedFAdd; } } } - if (remaining != 0) { - throw std::runtime_error("IVF index file has unexpected trailing data"); - } - loaded.ready_ = true; + ready_ = true; +} - input.close(); - swap(loaded); +void IVF::check_writable(const char* operation) const { + if (read_only_) { + throw std::logic_error( + std::string( + "IVF index was loaded from a file with features this release " + "cannot update, so it cannot " + ) + + operation + ); + } } void IVF::search( diff --git a/src/index/ivf_initializer.cpp b/src/index/ivf_initializer.cpp index cbaf7e00..1a10f8c3 100644 --- a/src/index/ivf_initializer.cpp +++ b/src/index/ivf_initializer.cpp @@ -2,10 +2,12 @@ #include #include #include +#include #include -#include #include +#include #include +#include #include #include @@ -163,7 +165,7 @@ void FlatRaBitQInitializer::centroids_distances( } } -void FlatRaBitQInitializer::save(std::ofstream& output, const char* /*filename*/) const { +void FlatRaBitQInitializer::write(std::ostream& output) const { output.write( reinterpret_cast(centroids_.data()), static_cast(centroids_.size() * sizeof(float)) @@ -181,7 +183,11 @@ void FlatRaBitQInitializer::save(std::ofstream& output, const char* /*filename*/ } } -void FlatRaBitQInitializer::load(std::ifstream& input, const char* /*filename*/) { +void FlatRaBitQInitializer::read(std::istream& input, uint64_t bytes) { + if (bytes != sizeof(float) * (centroids_.size() + global_centroid_.size()) + + batch_data_.size()) { + throw std::runtime_error("IVF Flat RaBitQ initializer data has the wrong size"); + } input.read( reinterpret_cast(centroids_.data()), static_cast(centroids_.size() * sizeof(float)) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 879b224f..62ff0125 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -40,9 +40,16 @@ add_executable(rabitq_tests ${UNIT_TESTS} ) target_include_directories(rabitq_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/common) +target_include_directories(rabitq_tests PRIVATE ${PROJECT_SOURCE_DIR}/src/core) +target_compile_definitions(rabitq_tests PRIVATE + RABITQ_TEST_LIBRARY_VERSION="${PROJECT_VERSION}" +) if(RABITQ_BUILD_INDEX) target_include_directories(rabitq_tests PRIVATE ${PROJECT_SOURCE_DIR}/src/index) endif() +set_source_files_properties(unit/rabitqlib/utils/blob_test.cpp + PROPERTIES COMPILE_DEFINITIONS + "RABITQ_QUANT_FIXTURE_ROOT=\"${CMAKE_CURRENT_SOURCE_DIR}/python/fixtures/quant\"") set_source_files_properties(unit/rabitqlib/index/historical_persistence_test.cpp PROPERTIES COMPILE_DEFINITIONS "RABITQ_TEST_FIXTURE_ROOT=\"${CMAKE_CURRENT_SOURCE_DIR}/fixtures/persistence\"") diff --git a/tests/common/format2_file.hpp b/tests/common/format2_file.hpp new file mode 100644 index 00000000..99a29587 --- /dev/null +++ b/tests/common/format2_file.hpp @@ -0,0 +1,158 @@ +#ifndef RABITQ_TEST_FORMAT2_FILE_HPP +#define RABITQ_TEST_FORMAT2_FILE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "crc32.hpp" +#include "format2.hpp" + +namespace rabitq_test { +namespace format2 = rabitqlib::index_detail::format2; + +// Every IVF-RaBitQ section kind, for reading back an IVF file in full. +inline const std::vector kIvfSections = { + 1, 256, 257, 258, 259, 260, 261, 262, 263}; + +// A whole format-2 file: the header and each known section with its bytes, in file order. +struct Format2Contents { + format2::Header header; + std::string writer_version; + std::vector> sections; + + // The bytes of a section; an error if the file has none. + std::string& bytes(uint32_t kind) { + for (auto& [section, data] : sections) { + if (section.kind == kind) { + return data; + } + } + throw std::runtime_error("test file has no such section"); + } + + void remove(uint32_t kind) { + sections.erase( + std::remove_if( + sections.begin(), + sections.end(), + [kind](const auto& entry) { return entry.first.kind == kind; } + ), + sections.end() + ); + } +}; + +inline Format2Contents read_format2_file( + const std::string& path, + format2::IndexKind kind, + const std::vector& known_kinds +) { + Format2Contents contents; + std::ifstream input(path, std::ios::binary); + format2::Reader reader(input, kind, known_kinds); + contents.header = reader.header(); + contents.writer_version = reader.writer_version(); + for (const uint32_t known : known_kinds) { + if (const format2::Section* section = reader.find(known)) { + std::string bytes(section->size, '\0'); + reader.read(*section, 0, bytes.data(), bytes.size()); + contents.sections.emplace_back(*section, std::move(bytes)); + } + } + std::sort( + contents.sections.begin(), + contents.sections.end(), + [](const auto& a, const auto& b) { return a.first.offset < b.first.offset; } + ); + return contents; +} + +// Write `contents` as a structurally valid file, so that a test can damage what a +// section holds rather than the container. +inline void write_format2_file(const std::string& path, const Format2Contents& contents) { + std::vector writes; + for (const auto& [section, bytes] : contents.sections) { + writes.push_back( + {section.kind, + section.flags, + bytes.size(), + [&data = bytes](format2::SectionSink& sink) { + sink.write(data.data(), data.size()); + }} + ); + } + std::ofstream output(path, std::ios::binary | std::ios::trunc); + output.exceptions(std::ios::failbit | std::ios::badbit); + format2::write_file(output, contents.header, writes, contents.writer_version); +} + +// The bytes of one section of a format-2 file. +inline std::string read_format2_section( + const std::string& path, + format2::IndexKind kind, + const std::vector& known_kinds, + uint32_t section_kind +) { + return read_format2_file(path, kind, known_kinds).bytes(section_kind); +} + +// Rewrite a format-2 file with one section's bytes replaced and every other part kept. +inline void replace_format2_section( + const std::string& path, + format2::IndexKind kind, + const std::vector& known_kinds, + uint32_t section_kind, + const std::string& replacement +) { + Format2Contents contents = read_format2_file(path, kind, known_kinds); + contents.bytes(section_kind) = replacement; + write_format2_file(path, contents); +} + +// Overwrite a little-endian header field of `size` bytes and recompute the header +// checksum; the writer refuses some values tests need, such as unknown feature bits. +inline void patch_format2_header( + const std::string& path, size_t offset, uint64_t value, size_t size +) { + std::string bytes; + { + std::ifstream input(path, std::ios::binary); + bytes.assign(std::istreambuf_iterator(input), {}); + } + for (size_t i = 0; i < size; ++i) { + bytes[offset + i] = static_cast(value >> (8 * i)); + } + const auto u32 = [&bytes](size_t at) { + uint32_t result = 0; + for (size_t i = 0; i < 4; ++i) { + result |= static_cast(static_cast(bytes[at + i])) << (8 * i); + } + return result; + }; + uint64_t table_offset = 0; + for (size_t i = 0; i < 8; ++i) { + table_offset |= static_cast(static_cast(bytes[96 + i])) + << (8 * i); + } + std::string header = bytes.substr(0, u32(12)); + header.replace(108, 4, 4, '\0'); + uint32_t crc = rabitqlib::detail::crc32(header.data(), header.size()); + crc = rabitqlib::detail::crc32(bytes.data() + table_offset, size_t{32} * u32(104), crc); + for (size_t i = 0; i < 4; ++i) { + bytes[108 + i] = static_cast(crc >> (8 * i)); + } + std::ofstream output(path, std::ios::binary | std::ios::trunc); + output.write(bytes.data(), static_cast(bytes.size())); +} + +} // namespace rabitq_test + +#endif // RABITQ_TEST_FORMAT2_FILE_HPP diff --git a/tests/fixtures/persistence/.gitattributes b/tests/fixtures/persistence/.gitattributes index df5ad8ed..afe6b1ab 100644 --- a/tests/fixtures/persistence/.gitattributes +++ b/tests/fixtures/persistence/.gitattributes @@ -1,3 +1,4 @@ *.index -text +*.hnsw -text *.expected text eol=lf manifest.json text eol=lf diff --git a/tests/fixtures/persistence/README.md b/tests/fixtures/persistence/README.md index ee820cf2..74da83f0 100644 --- a/tests/fixtures/persistence/README.md +++ b/tests/fixtures/persistence/README.md @@ -28,9 +28,11 @@ interchange format for arbitrary architectures. Python also tests synthetic IVF- v1 headers around the frozen historical payload, unknown versions, and invalid padding. That header test is distinct from release-produced fixtures. -The small IVF-RaBitQ fixtures use Flat centroid routing. Current round-trip and failure -tests exercise explicit HNSW routing and its sidecar; historical auto-selected -HNSW routing at 20,000 clusters is not represented by these small fixtures. +The small IVF-RaBitQ fixtures use Flat centroid routing. The [v0.8.3](v0.8.3/README.md) +set adds the last layout before index format 2, including Flat RaBitQ and HNSW routing +with its `.hnsw` sidecar. Historical auto-selected HNSW routing at 20,000 clusters is +not represented by fixtures; Python tests cover it with headers built around the +v0.8.3 layout. ## Deliberate regeneration diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/README.md b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/README.md index 3e7c742d..89b1ea05 100644 --- a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/README.md +++ b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/README.md @@ -13,10 +13,12 @@ single-point cluster, a full FastScan batch and a partial tail batch. Point 0 is removed. The `.expected` files hold reference results of a search that probes every cluster. -The tests load each file, compare search results, and require that saving it -again reproduces the original bytes. Because the clusters differ in size, a -reader or writer that orders the batch data, extra-bit data and IDs differently -from the shared-buffer layout fails these checks. +The tests load each file and compare search results. Until index format 2, they also +required that saving it again reproduces the original bytes; saving now writes format +2, and the tests require that the migrated file gives the same results and that saving +it again reproduces it. Because the clusters differ in size, a reader that orders the +batch data, extra-bit data and IDs differently from the shared-buffer layout fails +these checks. ## Deliberate regeneration diff --git a/tests/fixtures/persistence/v0.8.3/CMakeLists.txt b/tests/fixtures/persistence/v0.8.3/CMakeLists.txt new file mode 100644 index 00000000..4ddc10df --- /dev/null +++ b/tests/fixtures/persistence/v0.8.3/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.20) +project(rabitq_v083_fixtures LANGUAGES CXX) + +if(NOT IS_DIRECTORY "${RABITQ_HISTORICAL_SOURCE}") + message(FATAL_ERROR "Set RABITQ_HISTORICAL_SOURCE to the pinned v0.8.3 checkout") +endif() +set(RABITQ_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(RABITQ_BUILD_SAMPLES OFF CACHE BOOL "" FORCE) +set(RABITQ_BUILD_PYTHON_BINDINGS OFF CACHE BOOL "" FORCE) +set(RABITQ_ENABLE_NATIVE_OPTIMIZATION OFF CACHE BOOL "" FORCE) +add_subdirectory("${RABITQ_HISTORICAL_SOURCE}" historical) +add_executable(generate_fixtures generate.cpp) +target_link_libraries(generate_fixtures PRIVATE rabitqlib::index) diff --git a/tests/fixtures/persistence/v0.8.3/README.md b/tests/fixtures/persistence/v0.8.3/README.md new file mode 100644 index 00000000..72bd9bcd --- /dev/null +++ b/tests/fixtures/persistence/v0.8.3/README.md @@ -0,0 +1,41 @@ +# IVF-RaBitQ fixtures from v0.8.3 + +These files were written by release **v0.8.3**, commit +`dabfdb8c200a6f155df4edd0b9c825e6728b14a1`, using [generate.cpp](generate.cpp). +[manifest.json](manifest.json) records the producer and SHA-256 hashes. +Keep these files unchanged; tests never regenerate them with the current writer. + +They freeze the last IVF-RaBitQ layout before index format 2: `RABQIDX1` version 2, +which stores the centroid-routing type, and the hnswlib sidecar `.hnsw` that +HNSW routing writes beside the index. The five cases are: + +| File | Bits | Metric | Rotator | Routing | +| --- | --- | --- | --- | --- | +| `ivf4_flat_l2` | 4 | L2 | FHT/Kac | Flat | +| `ivf7_flatrabitq_ip` | 7 | inner product | FHT/Kac | FlatRaBitQ | +| `ivf4_hnsw_l2` | 4 | L2 | FHT/Kac | HNSW, with sidecar | +| `ivf32_hnsw_ip` | raw (`nbits=32`) | inner product | FHT/Kac | HNSW, with sidecar | +| `ivf1_flat_matrix_l2` | 1 | L2 | matrix | Flat | + +Each index holds 130 points in 65 original dimensions (96 padded) across four clusters +of 0, 1, 32 and 97 points, with point 0 removed, as in +[pre-ivf-per-cluster-storage](../pre-ivf-per-cluster-storage/README.md). The +`.expected` files hold reference results of a search that probes every cluster. + +## Deliberate regeneration + +Build the generator only against an isolated copy of the release: + +```bash +mkdir -p build/fixtures/v0.8.3 +git archive v0.8.3 | tar -x -C build/fixtures/v0.8.3 +cmake -S tests/fixtures/persistence/v0.8.3 -B build/fixtures/v0.8.3-generator \ + -DRABITQ_HISTORICAL_SOURCE="$PWD/build/fixtures/v0.8.3" \ + -DCMAKE_BUILD_TYPE=Release +cmake --build build/fixtures/v0.8.3-generator --target generate_fixtures +``` + +Run `generate_fixtures` with a new output directory and `OMP_NUM_THREADS=1`. +Rotation is seeded from `random_device`, so the bytes differ on every run; each index, +its sidecar and its `.expected` file form a fixed set. Record new hashes in the manifest +before adopting them. diff --git a/tests/fixtures/persistence/v0.8.3/generate.cpp b/tests/fixtures/persistence/v0.8.3/generate.cpp new file mode 100644 index 00000000..ff2441d5 --- /dev/null +++ b/tests/fixtures/persistence/v0.8.3/generate.cpp @@ -0,0 +1,111 @@ +// Build only against the pinned v0.8.3 source, never the current checkout. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rabitqlib/core/metric.hpp" +#include "rabitqlib/core/types.hpp" +#include "rabitqlib/index/ivf/ivf.hpp" +#include "rabitqlib/index/ivf/types.hpp" +#include "rabitqlib/index/types.hpp" + +int main(int argc, char** argv) { + if (argc != 2) { + throw std::invalid_argument("Pass the fixture output directory"); + } + using namespace rabitqlib; + // Cluster sizes 0, 1, 32 and 97 cover empty, single, full-batch and tail clusters. + constexpr size_t kCount = 130, kDim = 65, kClusters = 4, kQueries = 3, kTopK = 5; + const std::filesystem::path directory(argv[1]); + std::filesystem::create_directories(directory); + uint32_t state = 713; + const auto next = [&state] { + state = state * 1664525U + 1013904223U; + return static_cast(static_cast((state >> 8) % 2001) - 1000) / 256.0F; + }; + std::vector data(kCount * kDim), queries(kQueries * kDim); + std::vector centroids(kClusters * kDim); + for (auto& value : data) { + value = next(); + } + for (auto& value : queries) { + value = next(); + } + for (auto& value : centroids) { + value = next(); + } + std::vector clusters(kCount, 3); + clusters[0] = 1; + for (size_t i = 1; i < 33; ++i) { + clusters[i] = 2; + } + + const struct { + const char* name; + size_t total_bits; + MetricType metric; + RotatorType rotator; + ivf::InitializerType initializer; + } cases[] = { + {"ivf4_flat_l2", + 4, + METRIC_L2, + RotatorType::FhtKacRotator, + ivf::InitializerType::Flat}, + {"ivf7_flatrabitq_ip", + 7, + METRIC_IP, + RotatorType::FhtKacRotator, + ivf::InitializerType::FlatRaBitQ}, + {"ivf4_hnsw_l2", + 4, + METRIC_L2, + RotatorType::FhtKacRotator, + ivf::InitializerType::HNSW}, + {"ivf32_hnsw_ip", + 32, + METRIC_IP, + RotatorType::FhtKacRotator, + ivf::InitializerType::HNSW}, + {"ivf1_flat_matrix_l2", + 1, + METRIC_L2, + RotatorType::MatrixRotator, + ivf::InitializerType::Flat}, + }; + for (const auto& c : cases) { + const std::string name = c.name; + const auto path = (directory / (name + ".index")).string(); + std::vector ids(kQueries * kTopK); + std::vector distances(ids.size()); + ivf::IVF index( + kCount, kDim, kClusters, c.total_bits, c.metric, c.rotator, c.initializer + ); + index.construct(data.data(), centroids.data(), clusters.data(), false, 1); + const PID removed = 0; + index.remove(&removed, 1); + index.save(path.c_str()); + index.search_batch( + queries.data(), kQueries, kTopK, kClusters, ids.data(), distances.data() + ); + std::ofstream expected(directory / (name + ".expected"), std::ios::binary); + expected.exceptions(std::ios::failbit | std::ios::badbit); + expected << std::setprecision(std::numeric_limits::max_digits10); + expected << kCount << ' ' << kDim << ' ' << kQueries << ' ' << kTopK << '\n'; + for (size_t i = 0; i < queries.size(); ++i) { + expected << queries[i] << (i + 1 == queries.size() ? '\n' : ' '); + } + for (size_t i = 0; i < ids.size(); ++i) { + expected << ids[i] << ' ' << distances[i] << '\n'; + } + expected.close(); + } + return 0; +} diff --git a/tests/fixtures/persistence/v0.8.3/ivf1_flat_matrix_l2.expected b/tests/fixtures/persistence/v0.8.3/ivf1_flat_matrix_l2.expected new file mode 100644 index 00000000..c7b534ad --- /dev/null +++ b/tests/fixtures/persistence/v0.8.3/ivf1_flat_matrix_l2.expected @@ -0,0 +1,17 @@ +130 65 3 5 +0.7734375 -1.265625 2.28125 -3.40234375 -1.609375 0.91796875 2.7109375 -0.20703125 0.31640625 0.62890625 1.296875 2.8203125 -1.46484375 -0.171875 -1.53515625 2.46875 3.79296875 -3.61328125 0.52734375 -3.21484375 2.94921875 2.4453125 0.7265625 -1.44921875 2.546875 -2.00390625 -2.8125 -1.609375 -2.1953125 -3.625 2.00390625 -1.5 -0.57421875 3.296875 -2.828125 -3.30078125 -3.03515625 3.25 -2.89453125 0.54296875 1.21875 -2.16015625 -3.34375 2.84765625 -2.24609375 -1.34765625 -0.1484375 -3.7265625 -3.87109375 -0.2890625 1.7421875 -3.08203125 0.66015625 -1.3359375 1.62109375 2.5 -2.8828125 1.203125 -2.82421875 2.0703125 1.05078125 -2.21875 -0.7578125 -1.1796875 1.86328125 -1.00390625 -2.02734375 -2.69140625 3.0703125 -0.796875 -0.78515625 -2.34375 3.60546875 -1.81640625 -1.3671875 -0.80078125 3.63671875 2.00390625 -3.828125 1.8125 1.34765625 0.5234375 0.8359375 3.71484375 1.19140625 0.54296875 -0.06640625 3.25390625 1.68359375 -1.59375 3.03515625 1.22265625 -3.2734375 0.05859375 -0.84765625 -0.96484375 1.046875 -2.58984375 -0.3828125 -1.765625 0.00390625 3.5 0.1484375 0.234375 -1.9609375 -0.375 -3.37109375 -0.40234375 1.34765625 -3.12109375 1 -1.16015625 1.83984375 2.46484375 1.25 -1.3125 -3.7734375 -0.9765625 -1.390625 -0.16015625 -1.6484375 -2.6015625 2.40234375 3.03515625 2.64453125 -1.49609375 -1.9765625 -2.3515625 -1.57421875 -0.18359375 1.37109375 1.4765625 2.46875 -0.75390625 -0.6171875 1.484375 2.1328125 -0.73046875 -1.640625 3.1875 0.6484375 2.80859375 2.79296875 -1.578125 -3.72265625 -3.0546875 -3.57421875 -3.1953125 -3.51171875 -0.9921875 1.171875 0.7734375 -3.08984375 -0.07421875 1.25390625 1.55078125 -2.6171875 -0.5703125 0.21484375 -0.90234375 -0.65234375 0.44140625 1.62109375 0.76171875 2.58984375 -2.8046875 0.90625 -3.09375 -1.3515625 0.5625 1.84765625 1.8515625 2.03125 1.51171875 -3.25 -1.32421875 0.94140625 -1.0390625 2.3984375 -3.0546875 -3.3828125 3.109375 0.01171875 -1.3203125 -0.328125 -0.94140625 0.4296875 2.43359375 -1.5390625 -2.58203125 2.53515625 -2.578125 0.0703125 2.79296875 -1.49609375 +83 361.387726 +36 367.132172 +46 399.009552 +50 400.204742 +65 405.736511 +31 288.149902 +112 353.039429 +129 425.505188 +40 444.491913 +95 452.522827 +84 289.657135 +72 334.618195 +87 337.608826 +50 342.41745 +100 356.274933 diff --git a/tests/fixtures/persistence/v0.8.3/ivf1_flat_matrix_l2.index b/tests/fixtures/persistence/v0.8.3/ivf1_flat_matrix_l2.index new file mode 100644 index 0000000000000000000000000000000000000000..269d496648d1c6cb9f5fbd44bc0a4590a4fe007f GIT binary patch literal 31718 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/persistence/v0.8.3/ivf32_hnsw_ip.expected b/tests/fixtures/persistence/v0.8.3/ivf32_hnsw_ip.expected new file mode 100644 index 00000000..d0f55dd7 --- /dev/null +++ b/tests/fixtures/persistence/v0.8.3/ivf32_hnsw_ip.expected @@ -0,0 +1,17 @@ +130 65 3 5 +0.7734375 -1.265625 2.28125 -3.40234375 -1.609375 0.91796875 2.7109375 -0.20703125 0.31640625 0.62890625 1.296875 2.8203125 -1.46484375 -0.171875 -1.53515625 2.46875 3.79296875 -3.61328125 0.52734375 -3.21484375 2.94921875 2.4453125 0.7265625 -1.44921875 2.546875 -2.00390625 -2.8125 -1.609375 -2.1953125 -3.625 2.00390625 -1.5 -0.57421875 3.296875 -2.828125 -3.30078125 -3.03515625 3.25 -2.89453125 0.54296875 1.21875 -2.16015625 -3.34375 2.84765625 -2.24609375 -1.34765625 -0.1484375 -3.7265625 -3.87109375 -0.2890625 1.7421875 -3.08203125 0.66015625 -1.3359375 1.62109375 2.5 -2.8828125 1.203125 -2.82421875 2.0703125 1.05078125 -2.21875 -0.7578125 -1.1796875 1.86328125 -1.00390625 -2.02734375 -2.69140625 3.0703125 -0.796875 -0.78515625 -2.34375 3.60546875 -1.81640625 -1.3671875 -0.80078125 3.63671875 2.00390625 -3.828125 1.8125 1.34765625 0.5234375 0.8359375 3.71484375 1.19140625 0.54296875 -0.06640625 3.25390625 1.68359375 -1.59375 3.03515625 1.22265625 -3.2734375 0.05859375 -0.84765625 -0.96484375 1.046875 -2.58984375 -0.3828125 -1.765625 0.00390625 3.5 0.1484375 0.234375 -1.9609375 -0.375 -3.37109375 -0.40234375 1.34765625 -3.12109375 1 -1.16015625 1.83984375 2.46484375 1.25 -1.3125 -3.7734375 -0.9765625 -1.390625 -0.16015625 -1.6484375 -2.6015625 2.40234375 3.03515625 2.64453125 -1.49609375 -1.9765625 -2.3515625 -1.57421875 -0.18359375 1.37109375 1.4765625 2.46875 -0.75390625 -0.6171875 1.484375 2.1328125 -0.73046875 -1.640625 3.1875 0.6484375 2.80859375 2.79296875 -1.578125 -3.72265625 -3.0546875 -3.57421875 -3.1953125 -3.51171875 -0.9921875 1.171875 0.7734375 -3.08984375 -0.07421875 1.25390625 1.55078125 -2.6171875 -0.5703125 0.21484375 -0.90234375 -0.65234375 0.44140625 1.62109375 0.76171875 2.58984375 -2.8046875 0.90625 -3.09375 -1.3515625 0.5625 1.84765625 1.8515625 2.03125 1.51171875 -3.25 -1.32421875 0.94140625 -1.0390625 2.3984375 -3.0546875 -3.3828125 3.109375 0.01171875 -1.3203125 -0.328125 -0.94140625 0.4296875 2.43359375 -1.5390625 -2.58203125 2.53515625 -2.578125 0.0703125 2.79296875 -1.49609375 +128 -94.8744507 +92 -82.2540436 +46 -80.8800812 +50 -71.6611023 +5 -69.8279419 +112 -73.9705811 +128 -73.9433899 +11 -71.4328156 +14 -64.1144104 +31 -59.65448 +32 -72.5804443 +106 -66.686676 +3 -59.7108917 +87 -57.427536 +127 -53.9758759 diff --git a/tests/fixtures/persistence/v0.8.3/ivf32_hnsw_ip.index b/tests/fixtures/persistence/v0.8.3/ivf32_hnsw_ip.index new file mode 100644 index 0000000000000000000000000000000000000000..2929eafe6feea9cce897ed370b03b92099c34460 GIT binary patch literal 39070 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 Ck2KN% literal 0 HcmV?d00001 diff --git a/tests/fixtures/persistence/v0.8.3/ivf32_hnsw_ip.index.hnsw b/tests/fixtures/persistence/v0.8.3/ivf32_hnsw_ip.index.hnsw new file mode 100644 index 0000000000000000000000000000000000000000..59f96bf27f618a07c5993c47aeb7bfd7a34891c7 GIT binary patch literal 2208 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/persistence/v0.8.3/ivf4_hnsw_l2.index.hnsw b/tests/fixtures/persistence/v0.8.3/ivf4_hnsw_l2.index.hnsw new file mode 100644 index 0000000000000000000000000000000000000000..06c069f576b9579f78c0ed16df71cb5818dab0d3 GIT binary patch literal 2208 zcmc&#Yfx2H6#fLwCc(LZ7-{JhwakhLlhO2^wWKjIbP&tZYVv48HGz^nFehzihA&Kf zi(6^OK^b=rDN>-_&1yt_Yn$pvK6 zdeFEI(Zgqj*9ub_`f+BMqvduTOztp7!Wl=!7K)m(mQ>vb;->t>>WNa~hD%K7yZOu+ z-Nen>Mz#0e%$Shqgu)A?C}S?;!Z#~8CPviCN~iYVNaGun&n78TwseRwYRj(|0Y3ID1|Nm19YT$c>K~bc_hXn=A8qXCwCi-%Nk}0HO-w+H)VASL~m@&Yvr7-4Q zWpLzSLd!8nS$J{kMnPpijXvWorl*Hv(@KpilL-xxf?Jk3sEs5f?;;#JDOej2goX#e zw`tsPmtfkvgl|qbs5z#Q)6wF$hH%kyf{S}F%0JNfqMO0#biz3s>pPvHVuDEO#~70# zNHKzi4Wd3Q67|Uor1}Si9~fxVW5-7g?;8G24u*C$ z=sBG6SOg)QLKv}x@MW=s0n;_=Ki3$1L{PVlz~?nqtRM^>z-m`HV~0<0AYZVfkWic@ zZEfN2UeM~d>!MvJ|CwF8RO57C#=0nj`%46O&ms)#VGx^P>nxt}fUVU;Tc1~C6LxhG z+&Ed(x!H`_R{Is=;I~@K@mh<~41(@tP@Aby)@=2^PEZu5)xk)E^EHI+8wD>~PRBoP zIr)mz8!-mkU$r<#I=H1=&@Y!T^Gb$SPOA5_7Gn{t$TFzuLdafBFk^$jYXMZ05jNO5 z+djx(;!&G(Mo^s37+65SR1I%m04;FfKNY~-p9%Na8s4|TsJ)iAz8>N1XMzzgiQ1HI z)LjV%*UvC8s|8+zJ-O6n$`JCgQiJ@ z0}m1Et1Z9#EpFWzBO@3&%LSb3V0dqPr(8z7(kQ&Z<}6~gJg<>frjhWBJ^xpy?P>X& z|5m?UAOD#K+J5A%+C<)oouqQNJ6p2jSe4i5qU(B@yv`{`#fG(S{bj7C6zaUaK6dMm zkQcjIOnP5(b*Z2+F6iF6p1kw};{F^Iyc*@;sRtdEbHFhlcOd`Pqf)flYN2*o1s6t> zceaZ7`W!O9Um)(%9mL&6iQ2J~_@R+jFUg_~&nK0)O4LWo#Ynut9m5D|O9Kd>(Wppq zP?_LhY%f9hDXsGN*jn=mzt>URN8ctjeyQVyg+e11%8koz^6jBIijdc<>c z$gOOYq$AT!Xy-oR_d7{m?_{oB70ur40%DnI-=BBLzob_DA=_iV=}j%i9pk&T6K|=u z8u(#RfG716e6l*&5;KjHCX6!v-Sy;-@Ucq0Nszx)n}K2R8Y0=`pAei1YmBkFoIcp$ fszNby-W5~1M^xHHgsTr}Jld?)rH|0R^?*MC*|e6p literal 0 HcmV?d00001 diff --git a/tests/fixtures/persistence/v0.8.3/ivf7_flatrabitq_ip.expected b/tests/fixtures/persistence/v0.8.3/ivf7_flatrabitq_ip.expected new file mode 100644 index 00000000..3f2607f1 --- /dev/null +++ b/tests/fixtures/persistence/v0.8.3/ivf7_flatrabitq_ip.expected @@ -0,0 +1,17 @@ +130 65 3 5 +0.7734375 -1.265625 2.28125 -3.40234375 -1.609375 0.91796875 2.7109375 -0.20703125 0.31640625 0.62890625 1.296875 2.8203125 -1.46484375 -0.171875 -1.53515625 2.46875 3.79296875 -3.61328125 0.52734375 -3.21484375 2.94921875 2.4453125 0.7265625 -1.44921875 2.546875 -2.00390625 -2.8125 -1.609375 -2.1953125 -3.625 2.00390625 -1.5 -0.57421875 3.296875 -2.828125 -3.30078125 -3.03515625 3.25 -2.89453125 0.54296875 1.21875 -2.16015625 -3.34375 2.84765625 -2.24609375 -1.34765625 -0.1484375 -3.7265625 -3.87109375 -0.2890625 1.7421875 -3.08203125 0.66015625 -1.3359375 1.62109375 2.5 -2.8828125 1.203125 -2.82421875 2.0703125 1.05078125 -2.21875 -0.7578125 -1.1796875 1.86328125 -1.00390625 -2.02734375 -2.69140625 3.0703125 -0.796875 -0.78515625 -2.34375 3.60546875 -1.81640625 -1.3671875 -0.80078125 3.63671875 2.00390625 -3.828125 1.8125 1.34765625 0.5234375 0.8359375 3.71484375 1.19140625 0.54296875 -0.06640625 3.25390625 1.68359375 -1.59375 3.03515625 1.22265625 -3.2734375 0.05859375 -0.84765625 -0.96484375 1.046875 -2.58984375 -0.3828125 -1.765625 0.00390625 3.5 0.1484375 0.234375 -1.9609375 -0.375 -3.37109375 -0.40234375 1.34765625 -3.12109375 1 -1.16015625 1.83984375 2.46484375 1.25 -1.3125 -3.7734375 -0.9765625 -1.390625 -0.16015625 -1.6484375 -2.6015625 2.40234375 3.03515625 2.64453125 -1.49609375 -1.9765625 -2.3515625 -1.57421875 -0.18359375 1.37109375 1.4765625 2.46875 -0.75390625 -0.6171875 1.484375 2.1328125 -0.73046875 -1.640625 3.1875 0.6484375 2.80859375 2.79296875 -1.578125 -3.72265625 -3.0546875 -3.57421875 -3.1953125 -3.51171875 -0.9921875 1.171875 0.7734375 -3.08984375 -0.07421875 1.25390625 1.55078125 -2.6171875 -0.5703125 0.21484375 -0.90234375 -0.65234375 0.44140625 1.62109375 0.76171875 2.58984375 -2.8046875 0.90625 -3.09375 -1.3515625 0.5625 1.84765625 1.8515625 2.03125 1.51171875 -3.25 -1.32421875 0.94140625 -1.0390625 2.3984375 -3.0546875 -3.3828125 3.109375 0.01171875 -1.3203125 -0.328125 -0.94140625 0.4296875 2.43359375 -1.5390625 -2.58203125 2.53515625 -2.578125 0.0703125 2.79296875 -1.49609375 +128 -94.6611481 +92 -82.9637375 +46 -81.7006149 +13 -72.5498047 +50 -71.9551773 +112 -74.3031998 +128 -74.0310822 +11 -72.1495819 +14 -64.1889343 +71 -59.4696617 +32 -72.5847778 +106 -67.2913208 +3 -59.6324234 +87 -56.3743286 +127 -54.9878922 diff --git a/tests/fixtures/persistence/v0.8.3/ivf7_flatrabitq_ip.index b/tests/fixtures/persistence/v0.8.3/ivf7_flatrabitq_ip.index new file mode 100644 index 0000000000000000000000000000000000000000..da3ca3b952425793a274ccc85de3bc41f419b33d GIT binary patch literal 18358 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/persistence/v0.8.3/manifest.json b/tests/fixtures/persistence/v0.8.3/manifest.json new file mode 100644 index 00000000..103e47ff --- /dev/null +++ b/tests/fixtures/persistence/v0.8.3/manifest.json @@ -0,0 +1,22 @@ +{ + "source_tag": "v0.8.3", + "source_commit": "dabfdb8c200a6f155df4edd0b9c825e6728b14a1", + "writer": "tests/fixtures/persistence/v0.8.3/generate.cpp", + "producer": "Linux x86-64, g++ 14.3.0, Release, one thread, native optimization OFF", + "byte_order": "little", + "size_t_bytes": 8, + "sha256": { + "v0.8.3/ivf1_flat_matrix_l2.expected": "f1616ce0a6f59a57984e62eda1a16b92a54f603434e163ce5a2f33c336a0a252", + "v0.8.3/ivf1_flat_matrix_l2.index": "c864c367f4eaac449984694402d42ba81706391c61405f3fcdcf233a7f4d92f4", + "v0.8.3/ivf32_hnsw_ip.expected": "8280cf8c79e396c94ddb0a3bf77d69f45f7261b1ea71e90e5522395286376f89", + "v0.8.3/ivf32_hnsw_ip.index": "61935ec5f4a5c3b2aeb990ccb9bafa25e7323658c65a20f0344e79e0c8df7194", + "v0.8.3/ivf32_hnsw_ip.index.hnsw": "320c644a727fa94f7e5ced3821d8fcca6562c22904acfbd158fc5b3f6c3c5eb4", + "v0.8.3/ivf4_flat_l2.expected": "c2d41f4b7c18332d94f01ebdf493658b8f468b5c517bc9961cbfea2ffdb33dbe", + "v0.8.3/ivf4_flat_l2.index": "d324a9df508c32fee0a862868d3f7b2fcd7acc999db4e7bd5761b4f6cd1d07be", + "v0.8.3/ivf4_hnsw_l2.expected": "4235ed49a3d008e34b646358ec0de0649c63122b3f5f60e813df8a0d494c3a88", + "v0.8.3/ivf4_hnsw_l2.index": "88b5b7d1b7e5be99506421cd36ad6bb1785490446f55788d5b39a1069d5b304c", + "v0.8.3/ivf4_hnsw_l2.index.hnsw": "1797edb3eb1fb16a20684d8873cbc2c0713bdf1623ff31f366e8f016f5e616cf", + "v0.8.3/ivf7_flatrabitq_ip.expected": "c602bd98139b597532067094b001ff992d4498ea07ad7ddaf6762b8261f67d83", + "v0.8.3/ivf7_flatrabitq_ip.index": "54c11bcb813395eed099c8e8f65cdd96d97ca1ed684703e66abe27fd06887929" + } +} diff --git a/tests/python/format2_file.py b/tests/python/format2_file.py new file mode 100644 index 00000000..341b6812 --- /dev/null +++ b/tests/python/format2_file.py @@ -0,0 +1,89 @@ +"""Read, patch and convert index format 2 files in tests (docs/formats.md).""" + +from __future__ import annotations + +import struct +import zlib + +MAGIC = b"RABQINDX" +ROTATOR = 1 +IVF_PARAMS = 256 +IVF_CLUSTER_SIZES = 257 +IVF_ROUTING = {1: 258, 2: 259, 3: 260} # by stored initializer: flat, flat_rabitq, hnsw +IVF_HNSW_ROUTING = 260 +IVF_CODES = 261 +IVF_RERANK = 262 +IVF_IDS = 263 + + +def sections(data: bytes) -> dict[int, tuple[int, int]]: + """Map each section kind of a format-2 file to its (offset, size).""" + table, count = struct.unpack_from(" bytes: + offset, size = sections(data)[kind] + return bytes(data[offset : offset + size]) + + +def padded_dim(data: bytes) -> int: + return struct.unpack_from(" None: + """Recompute section and header checksums after a same-size edit in place.""" + table, count = struct.unpack_from(" tuple[bytes, bytes | None]: + """Convert an IVF-RaBitQ format-2 file to the layout releases up to 0.8 wrote. + + Returns the RABQIDX1 version 2 index and, for HNSW routing, its ``.hnsw`` + sidecar. Tests use it to build earlier layouts that no fixture covers. + """ + assert data[:8] == MAGIC + present = sections(data) + metric = struct.unpack_from(" #include #include -#include +#include #include -#include #include +#include #include #include #include #include #include +#include "format2_file.hpp" #include "rabitqlib/core/metric.hpp" #include "rabitqlib/core/rotator.hpp" #include "rabitqlib/index/ivf/initializer.hpp" @@ -127,11 +128,15 @@ TEST(IvfInitializerTest, ReloadChangesRoutingAndFailedLoadPreservesExistingIndex EXPECT_EQ(actual_ids, expected_ids); EXPECT_EQ(actual_distances, expected_distances); { - std::fstream file(path, std::ios::binary | std::ios::in | std::ios::out); - // A persisted v2 file must contain a concrete initializer, never Auto. - file.seekp(24); - const uint32_t invalid = 0; - file.write(reinterpret_cast(&invalid), sizeof(invalid)); + // A persisted file must contain a concrete initializer, never Auto. + using rabitq_test::format2::IndexKind; + std::string params = rabitq_test::read_format2_section( + path, IndexKind::Ivf, rabitq_test::kIvfSections, 256 + ); + std::memset(params.data() + 8, 0, sizeof(uint32_t)); + rabitq_test::replace_format2_section( + path, IndexKind::Ivf, rabitq_test::kIvfSections, 256, params + ); } EXPECT_THROW(target.load(path.c_str()), std::runtime_error); EXPECT_EQ(target.initializer_type(), type); @@ -223,19 +228,18 @@ TEST(IvfFlatRaBitQInitializerTest, PersistsCodesAndSupportsConcurrentSearch) { std::vector centroids(kDim * kCount), queries(8 * kDim); std::generate(centroids.begin(), centroids.end(), [&] { return normal(generator); }); std::generate(queries.begin(), queries.end(), [&] { return normal(generator); }); - const rabitq_test::TempFile path_file("flat_rabitq_initializer.bin"); - const std::string& path = path_file.path(); for (auto metric : {METRIC_L2, METRIC_IP}) { FlatRaBitQInitializer index(kDim, kCount, metric), loaded(kDim, kCount, metric); index.add_vectors(centroids.data(), 2); + std::stringstream stored; + index.write(stored); + const std::string bytes = stored.str(); { - std::ofstream out(path, std::ios::binary); - index.save(out, nullptr); - } - { - std::ifstream input(path, std::ios::binary); - loaded.load(input, nullptr); + FlatRaBitQInitializer wrong_size(kDim, kCount, metric); + std::stringstream copy(bytes); + EXPECT_THROW(wrong_size.read(copy, bytes.size() - 1), std::runtime_error); } + loaded.read(stored, bytes.size()); for (size_t i = 0; i < kCount; ++i) { EXPECT_TRUE( std::equal(index.centroid(i), index.centroid(i) + kDim, loaded.centroid(i)) @@ -259,15 +263,9 @@ TEST(IvfFlatRaBitQInitializerTest, PersistsCodesAndSupportsConcurrentSearch) { EXPECT_FLOAT_EQ(actual[i][j].distance, expected[i][j].distance); } } - std::ifstream before(path, std::ios::binary); - const std::string bytes{std::istreambuf_iterator(before), {}}; - before.close(); - { - std::ofstream out(path, std::ios::binary); - loaded.save(out, nullptr); - } - std::ifstream after(path, std::ios::binary); - EXPECT_EQ((std::string{std::istreambuf_iterator(after), {}}), bytes); + std::ostringstream resaved; + loaded.write(resaved); + EXPECT_EQ(resaved.str(), bytes); } } } // namespace diff --git a/tests/unit/rabitqlib/index/format2_test.cpp b/tests/unit/rabitqlib/index/format2_test.cpp index 3308c85d..362b7b3b 100644 --- a/tests/unit/rabitqlib/index/format2_test.cpp +++ b/tests/unit/rabitqlib/index/format2_test.cpp @@ -13,8 +13,11 @@ #include #include +#include "crc32.hpp" + namespace { namespace f2 = rabitqlib::index_detail::format2; +using rabitqlib::detail::crc32; constexpr uint32_t kParams = 256; constexpr uint32_t kBulk = 261; @@ -57,8 +60,8 @@ void reseal(std::string& bytes) { const uint32_t count = get_u32(bytes, 104); std::string header = bytes.substr(0, header_bytes); put_u32(header, 108, 0); - uint32_t crc = f2::crc32(header.data(), header.size()); - crc = f2::crc32(bytes.data() + table_offset, size_t{32} * count, crc); + uint32_t crc = crc32(header.data(), header.size()); + crc = crc32(bytes.data() + table_offset, size_t{32} * count, crc); put_u32(bytes, 108, crc); } @@ -164,18 +167,6 @@ class SequentialOutput : public std::streambuf { } }; -TEST(Format2Crc32Test, MatchesZlib) { - EXPECT_EQ(f2::crc32("", 0), 0U); - EXPECT_EQ(f2::crc32("123456789", 9), 0xCBF43926U); - std::vector data(1024); - for (size_t i = 0; i < data.size(); ++i) { - data[i] = static_cast(i); - } - EXPECT_EQ(f2::crc32(data.data(), data.size()), 0xB70B4C26U); // zlib.crc32 - const uint32_t first = f2::crc32(data.data(), 100); - EXPECT_EQ(f2::crc32(data.data() + 100, data.size() - 100, first), 0xB70B4C26U); -} - TEST(Format2ContainerTest, RoundTripsHeaderAndSections) { const std::string bytes = valid_file(); std::istringstream stream(bytes); @@ -241,7 +232,7 @@ TEST(Format2ContainerTest, WritesAndReadsWithoutSeeking) { SequentialInput input_buffer(valid_file()); std::istream input(&input_buffer); - EXPECT_THROW(f2::has_magic(input), f2::FormatError); + EXPECT_THROW(f2::has_magic(input, f2::IndexKind::Ivf), f2::FormatError); try { f2::Reader reader(input, f2::IndexKind::Ivf, kKnown); ADD_FAILURE() << "expected FormatError"; @@ -252,14 +243,14 @@ TEST(Format2ContainerTest, WritesAndReadsWithoutSeeking) { TEST(Format2ContainerTest, DetectsTheMagicAndRestoresThePosition) { std::istringstream good(valid_file()); - EXPECT_TRUE(f2::has_magic(good)); + EXPECT_TRUE(f2::has_magic(good, f2::IndexKind::Ivf)); EXPECT_EQ(good.tellg(), std::streampos(0)); std::istringstream legacy(std::string("RABQIDX1") + std::string(200, '\0')); - EXPECT_FALSE(f2::has_magic(legacy)); + EXPECT_FALSE(f2::has_magic(legacy, f2::IndexKind::Ivf)); std::istringstream short_file("RABQ"); short_file.exceptions(std::ios::failbit | std::ios::badbit); - EXPECT_FALSE(f2::has_magic(short_file)); + EXPECT_FALSE(f2::has_magic(short_file, f2::IndexKind::Ivf)); EXPECT_EQ(short_file.tellg(), std::streampos(0)); } diff --git a/tests/unit/rabitqlib/index/historical_persistence_test.cpp b/tests/unit/rabitqlib/index/historical_persistence_test.cpp index e8d8156b..7be5605d 100644 --- a/tests/unit/rabitqlib/index/historical_persistence_test.cpp +++ b/tests/unit/rabitqlib/index/historical_persistence_test.cpp @@ -34,7 +34,7 @@ struct HistoricalFixtureCase { size_t padded_dim; size_t count = 33; size_t ivf_nprobe = 1; - bool resave_identical = false; // the current writer must reproduce the file + ivf::InitializerType initializer = ivf::InitializerType::Flat; }; void PrintTo(const HistoricalFixtureCase& value, std::ostream* output) { @@ -94,13 +94,20 @@ TEST_P(HistoricalPersistenceTest, LoadsSearchesResavesAndPreservesStateAfterFail search(); compare(); index.save(output.string().c_str()); - if (GetParam().resave_identical) { - EXPECT_TRUE(read_bytes(output) == read_bytes(path)); + const bool format2 = name.find("ivf") == 0; // the indexes that save format 2 + if (format2) { + EXPECT_EQ(read_bytes(output).substr(0, 8), "RABQINDX"); } index.load(output.string().c_str()); check_counts(); search(); compare(); + if (format2) { + // Reading format 2 back and saving again reproduces the file. + const std::string saved = read_bytes(output); + index.save(output.string().c_str()); + EXPECT_TRUE(read_bytes(output) == saved); + } std::filesystem::resize_file(output, std::filesystem::file_size(output) - 1); EXPECT_THROW(index.load(output.string().c_str()), std::runtime_error); check_counts(); @@ -112,7 +119,7 @@ TEST_P(HistoricalPersistenceTest, LoadsSearchesResavesAndPreservesStateAfterFail ivf::IVF index; exercise(index, [&] { EXPECT_EQ(index.padded_dim(), expected_padded_dim); - EXPECT_EQ(index.initializer_type(), ivf::InitializerType::Flat); + EXPECT_EQ(index.initializer_type(), GetParam().initializer); index.search_batch( queries.data(), nq, k, GetParam().ivf_nprobe, ids.data(), distances.data() ); @@ -164,10 +171,76 @@ INSTANTIATE_TEST_SUITE_P( PreIvfPerClusterStorage, HistoricalPersistenceTest, ::testing::Values( - HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf4_l2", 96, 130, 4, true}, - HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf32_l2", 96, 130, 4, true} + HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf4_l2", 96, 130, 4}, + HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf32_l2", 96, 130, 4} ), [](const auto& info) { return info.param.name; } ); + +// The last IVF layout before format 2: RABQIDX1 version 2 and the HNSW sidecar. +INSTANTIATE_TEST_SUITE_P( + V083, + HistoricalPersistenceTest, + ::testing::Values( + HistoricalFixtureCase{"v0.8.3", "ivf4_flat_l2", 96, 130, 4}, + HistoricalFixtureCase{ + "v0.8.3", "ivf7_flatrabitq_ip", 96, 130, 4, ivf::InitializerType::FlatRaBitQ}, + HistoricalFixtureCase{ + "v0.8.3", "ivf4_hnsw_l2", 96, 130, 4, ivf::InitializerType::HNSW}, + HistoricalFixtureCase{ + "v0.8.3", "ivf32_hnsw_ip", 96, 130, 4, ivf::InitializerType::HNSW}, + HistoricalFixtureCase{"v0.8.3", "ivf1_flat_matrix_l2", 96, 130, 4} + ), + [](const auto& info) { return info.param.name; } +); +// Files before format 2 keep the HNSW routing graph in .hnsw; loading reads it +// from there, through the same path conversion as the index file. +TEST(HistoricalPersistenceTest, LoadsTheHnswSidecarOfEarlierLayouts) { + namespace fs = std::filesystem; + const fs::path fixture = + fs::path(RABITQ_TEST_FIXTURE_ROOT) / "v0.8.3" / "ivf4_hnsw_l2.index"; + const fs::path directory = + fs::temp_directory_path() / + fs::u8path(rabitq_test::process_unique("historical_sidecar_\u6d4b\u8bd5")); + fs::create_directories(directory); + const fs::path index_path = directory / fs::u8path(u8"\u7d22\u5f15.index"); + const fs::path sidecar = fs::path(index_path.native() + fs::path(".hnsw").native()); + fs::copy_file(fixture, index_path); + const std::string path = index_path.u8string(); + + ivf::IVF index; + try { + index.load(path.c_str()); + ADD_FAILURE() << "an HNSW-routed file without its sidecar was accepted"; + } catch (const std::runtime_error& error) { + EXPECT_STREQ(error.what(), "Cannot open HNSW centroid sidecar"); + } + + // A graph of another dimension does not fit the index. + { + std::vector centroids(4 * 128, 0.5F); + ivf::HNSWInitializer other(128, 4); + other.add_vectors(centroids.data(), 1); + std::ofstream output(sidecar, std::ios::binary); + other.write(output); + } + try { + index.load(path.c_str()); + ADD_FAILURE() << "a mismatched sidecar was accepted"; + } catch (const std::runtime_error& error) { + EXPECT_STREQ(error.what(), "HNSW centroid layout does not match IVF dimension"); + } + + fs::copy_file( + fs::path(fixture.native() + fs::path(".hnsw").native()), + sidecar, + fs::copy_options::overwrite_existing + ); + ASSERT_NO_THROW(index.load(path.c_str())); + EXPECT_EQ(index.initializer_type(), ivf::InitializerType::HNSW); + EXPECT_EQ(index.num_points(), 129U); + fs::remove_all(directory); +} + } // namespace } // namespace rabitqlib diff --git a/tests/unit/rabitqlib/index/hnsw_sidecar_test.cpp b/tests/unit/rabitqlib/index/hnsw_sidecar_test.cpp index 3937f905..1ab304ab 100644 --- a/tests/unit/rabitqlib/index/hnsw_sidecar_test.cpp +++ b/tests/unit/rabitqlib/index/hnsw_sidecar_test.cpp @@ -6,15 +6,14 @@ #include #include #include -#include #include -#include #include #include #include #include #include +#include "format2_file.hpp" #include "rabitqlib/core/metric.hpp" #include "rabitqlib/core/rotator.hpp" #include "rabitqlib/index/ivf/initializer.hpp" @@ -75,8 +74,11 @@ struct Fixture { ); index.construct(data.data(), data.data(), ids.data(), false, 1); index.save(path.c_str()); - std::ifstream file(path + ".hnsw", std::ios::binary); - original.assign(std::istreambuf_iterator(file), {}); + // The routing graph is the HNSW-routing section of the format-2 index file. + const std::string graph = rabitq_test::read_format2_section( + path, rabitq_test::format2::IndexKind::Ivf, rabitq_test::kIvfSections, 260 + ); + original.assign(graph.begin(), graph.end()); level0_record_bytes = read_at(original, 3 * sizeof(size_t)); max_m = read_at(original, kMaxMOffset); max_m0 = read_at(original, kMaxMOffset + sizeof(size_t)); @@ -104,15 +106,20 @@ struct Fixture { ~Fixture() { std::error_code ignored; std::filesystem::remove(path, ignored); - std::filesystem::remove(path + ".hnsw", ignored); } + // Store `bytes` as the index file's routing graph. void save(const std::vector& bytes) const { - std::ofstream file(path + ".hnsw", std::ios::binary | std::ios::trunc); - file.write(bytes.data(), static_cast(bytes.size())); + rabitq_test::replace_format2_section( + path, + rabitq_test::format2::IndexKind::Ivf, + rabitq_test::kIvfSections, + 260, + std::string(bytes.begin(), bytes.end()) + ); } }; -TEST(IvfHnswSidecarGraphTest, PublicLoadRejectsCorruptStructureAndCanRetry) { +TEST(IvfHnswRoutingGraphTest, PublicLoadRejectsCorruptStructureAndCanRetry) { for (const auto metric : {METRIC_L2, METRIC_IP}) { Fixture fixture(metric); const auto reject = [&](const char* name, auto corrupt, const char* message) { @@ -123,7 +130,7 @@ TEST(IvfHnswSidecarGraphTest, PublicLoadRejectsCorruptStructureAndCanRetry) { ivf::IVF loaded; try { loaded.load(fixture.path.c_str()); - FAIL() << "damaged sidecar was accepted"; + FAIL() << "damaged routing graph was accepted"; } catch (const std::runtime_error& error) { EXPECT_STREQ(error.what(), message); } @@ -257,7 +264,7 @@ TEST(IvfHnswSidecarGraphTest, PublicLoadRejectsCorruptStructureAndCanRetry) { reject( "truncated upper length", [&](auto& b) { b.resize(fixture.upper_offsets.back() + 2); }, - "Truncated HNSW centroid sidecar" + "Truncated HNSW centroid graph" ); reject( "truncated upper links", @@ -287,7 +294,7 @@ TEST(IvfHnswSidecarGraphTest, PublicLoadRejectsCorruptStructureAndCanRetry) { reject( "truncated header", [](auto& b) { b.resize(4); }, - "Truncated HNSW centroid sidecar" + "Truncated HNSW centroid graph" ); reject( "trailing byte", @@ -297,7 +304,7 @@ TEST(IvfHnswSidecarGraphTest, PublicLoadRejectsCorruptStructureAndCanRetry) { } } -TEST(IvfHnswSidecarGraphTest, DisconnectedGraphCannotReadEmptyResults) { +TEST(IvfHnswRoutingGraphTest, DisconnectedGraphCannotReadEmptyResults) { for (const auto metric : {METRIC_L2, METRIC_IP}) { Fixture fixture(metric); auto bytes = fixture.original; diff --git a/tests/unit/rabitqlib/index/initializer_test.cpp b/tests/unit/rabitqlib/index/initializer_test.cpp index 93e20555..d25432c2 100644 --- a/tests/unit/rabitqlib/index/initializer_test.cpp +++ b/tests/unit/rabitqlib/index/initializer_test.cpp @@ -10,10 +10,10 @@ #include #include #include -#include #include #include #include +#include #include #include #include @@ -24,58 +24,33 @@ #include "rabitqlib/index/types.hpp" #include "rabitqlib/utils/space.hpp" #include "rabitqlib/utils/tools.hpp" -#include "temp_file.hpp" namespace rabitqlib::ivf { namespace { -TEST(IvfHnswInitializerTest, PersistsUtf8SidecarPaths) { - const auto directory = std::filesystem::temp_directory_path() / - std::filesystem::u8path(u8"rabitq_\u6d4b\u8bd5_\U0001f680"); - std::filesystem::create_directories(directory); - const auto path = (directory / std::filesystem::u8path(u8"\u7d22\u5f15")).u8string(); +TEST(IvfHnswInitializerTest, RoundTripsThroughAStreamAndChecksItsSize) { const std::array centroids{0.0F, 0.0F, 3.0F, 4.0F}; HNSWInitializer initializer(2, 2); initializer.add_vectors(centroids.data(), 1); - std::ofstream ignored_output; - initializer.save(ignored_output, path.c_str()); - EXPECT_TRUE(std::filesystem::is_regular_file(std::filesystem::u8path(path + ".hnsw"))); - HNSWInitializer loaded(2, 2); - std::ifstream ignored_input; - loaded.load(ignored_input, path.c_str()); - std::vector> candidates(1); - loaded.centroids_distances(centroids.data() + 2, 1, candidates); - EXPECT_EQ(candidates[0].id, 1U); - EXPECT_FLOAT_EQ(candidates[0].distance, 0.0F); - std::filesystem::remove(std::filesystem::u8path(path + ".hnsw")); - std::filesystem::remove(directory); -} - -TEST(IvfHnswInitializerTest, ReportsSidecarOpenFailureAndCanRetry) { - const std::string path = ::testing::TempDir() + "rabitq_hnsw_save_failure"; - const std::filesystem::path sidecar(path + ".hnsw"); - std::filesystem::create_directory(sidecar); - const std::array centroids{0.0F, 0.0F, 3.0F, 4.0F}; - HNSWInitializer initializer(2, 2); - initializer.add_vectors(centroids.data(), 1); - std::ofstream ignored_output; - try { - initializer.save(ignored_output, path.c_str()); - FAIL() << "Saving the HNSW sidecar to a directory must fail"; - } catch (const std::runtime_error& error) { - EXPECT_STREQ(error.what(), "Cannot open HNSW index file for writing"); - } - std::filesystem::remove(sidecar); + std::stringstream graph; + initializer.write(graph); + const std::string bytes = graph.str(); - initializer.save(ignored_output, path.c_str()); HNSWInitializer loaded(2, 2); - std::ifstream ignored_input; - loaded.load(ignored_input, path.c_str()); + loaded.read(graph, bytes.size()); std::vector> candidates(1); loaded.centroids_distances(centroids.data() + 2, 1, candidates); EXPECT_EQ(candidates[0].id, 1U); EXPECT_FLOAT_EQ(candidates[0].distance, 0.0F); - std::filesystem::remove(sidecar); + + // The graph is read from the current position and only within its byte count. + std::stringstream shifted("xyz" + bytes); + shifted.seekg(3); + HNSWInitializer offset(2, 2); + EXPECT_NO_THROW(offset.read(shifted, bytes.size())); + std::stringstream short_input(bytes); + HNSWInitializer truncated(2, 2); + EXPECT_THROW(truncated.read(short_input, bytes.size() - 1), std::runtime_error); } TEST(IvfHnswInitializerTest, ReportsBufferedWriteFailure) { @@ -222,16 +197,13 @@ TEST(IvfHnswInitializerTest, ParallelInsertionPreservesCentroidLabelsAfterReload centroids[label * kDim + dim] = static_cast(label * kDim + dim); } } - const rabitq_test::TempFile path_file("rabitq_initializer_labels"); - const std::string& path = path_file.path(); HNSWInitializer initializer(kDim, kCount); initializer.add_vectors(centroids.data(), 4); - std::ofstream ignored_output; - initializer.save(ignored_output, path.c_str()); + std::stringstream graph; + initializer.write(graph); HNSWInitializer loaded(kDim, kCount); - std::ifstream ignored_input; - loaded.load(ignored_input, path.c_str()); + loaded.read(graph, graph.str().size()); for (size_t label = 0; label < kCount; ++label) { for (size_t dim = 0; dim < kDim; ++dim) { EXPECT_FLOAT_EQ( @@ -251,16 +223,13 @@ TEST(IvfHnswInitializerTest, ReloadsInnerProductGraphForRouting) { std::vector centroids(kCount * kDim, 0.0F); centroids[0] = 0.9F; centroids[kDim] = 100.0F; - const rabitq_test::TempFile path_file("rabitq_initializer_ip"); - const std::string& path = path_file.path(); HNSWInitializer initializer(kDim, kCount, METRIC_IP); initializer.add_vectors(centroids.data(), 4); - std::ofstream ignored_output; - initializer.save(ignored_output, path.c_str()); + std::stringstream graph; + initializer.write(graph); HNSWInitializer loaded(kDim, kCount, METRIC_IP); - std::ifstream ignored_input; - loaded.load(ignored_input, path.c_str()); + loaded.read(graph, graph.str().size()); std::array query{}; query[0] = 1.0F; std::vector> candidates(1); @@ -393,31 +362,28 @@ TEST(ParallelForTest, JoinsWorkersBeforeRethrowingFunctionException) { TEST(IvfInitializerTest, FlatInitializerReportsStreamFailures) { constexpr size_t kDim = 8; constexpr size_t kCount = 3; + constexpr size_t kBytes = sizeof(float) * kDim * kCount; std::vector centroids(kCount * kDim, 1.5F); FlatInitializer flat(kDim, kCount); flat.add_vectors(centroids.data(), 1); - const rabitq_test::TempFile path_file("rabitq_flat_initializer.bin"); - const std::string& path = path_file.path(); - { - std::ofstream output(path, std::ios::binary); - ASSERT_NO_THROW(flat.save(output, path.c_str())); - } - { - // A stream that failed to open cannot be written. - std::ofstream unopened(path + ".missing/initializer.bin", std::ios::binary); - EXPECT_THROW(flat.save(unopened, path.c_str()), std::runtime_error); - } - { - std::ifstream input(path, std::ios::binary); - FlatInitializer restored(kDim, kCount); - ASSERT_NO_THROW(restored.load(input, path.c_str())); - } - std::filesystem::resize_file( - std::filesystem::u8path(path), sizeof(float) * kDim * kCount - 1 - ); - std::ifstream truncated(path, std::ios::binary); + std::stringstream stored; + ASSERT_NO_THROW(flat.write(stored)); + ASSERT_EQ(stored.str().size(), kBytes); + + std::ostringstream broken; + broken.setstate(std::ios::badbit); + EXPECT_THROW(flat.write(broken), std::runtime_error); + + FlatInitializer restored(kDim, kCount); + ASSERT_NO_THROW(restored.read(stored, kBytes)); + EXPECT_FLOAT_EQ(restored.centroid(2)[kDim - 1], 1.5F); + + FlatInitializer wrong_size(kDim, kCount); + std::stringstream copy(stored.str()); + EXPECT_THROW(wrong_size.read(copy, kBytes - 1), std::runtime_error); + std::stringstream truncated(stored.str().substr(0, kBytes - 1)); FlatInitializer partial(kDim, kCount); - EXPECT_THROW(partial.load(truncated, path.c_str()), std::runtime_error); + EXPECT_THROW(partial.read(truncated, kBytes), std::runtime_error); } } // namespace diff --git a/tests/unit/rabitqlib/index/ivf_test.cpp b/tests/unit/rabitqlib/index/ivf_test.cpp index cfce20c4..25a027e4 100644 --- a/tests/unit/rabitqlib/index/ivf_test.cpp +++ b/tests/unit/rabitqlib/index/ivf_test.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -17,10 +18,13 @@ #include #include #include +#include #include #include #include +#include "blob.hpp" +#include "format2_file.hpp" #include "ivf_search.hpp" #include "rabitqlib/clustering/qgkmeans.hpp" #include "rabitqlib/clustering/rabitqkmeans.hpp" @@ -544,7 +548,12 @@ TEST(IvfPersistenceTest, RejectsPointIdsUsingTheSearchBufferMarker) { file.close(); IVF loaded; - EXPECT_THROW(loaded.load(path.c_str()), std::runtime_error); + try { + loaded.load(path.c_str()); + ADD_FAILURE() << "a marked point ID was accepted"; + } catch (const std::runtime_error& error) { + EXPECT_STREQ(error.what(), "Invalid point ID in IVF index file"); + } } // ── add / remove ──────────────────────────────────────────────────────────────────────── @@ -1687,7 +1696,9 @@ TEST(IvfPersistenceTest, RejectsTrailingBytesAndReportsUnopenableDestination) { loaded.load(path.c_str()); ADD_FAILURE() << "trailing bytes were accepted"; } catch (const std::runtime_error& error) { - EXPECT_STREQ(error.what(), "IVF index file has unexpected trailing data"); + const std::string message = error.what(); + EXPECT_EQ(message.rfind("IVF index file is ", 0), 0U) << message; + EXPECT_NE(message.find(" bytes, but its sections end at "), std::string::npos); } // The failed load leaves the previously loaded index intact. EXPECT_EQ(loaded.num_points(), kCount); @@ -2016,10 +2027,393 @@ TEST(IvfTrainTest, DefaultsUseSphericalKMeansForInnerProduct) { } } +uint32_t get_size(const std::string& bytes, size_t offset) { + uint32_t value = 0; + std::memcpy(&value, bytes.data() + offset, sizeof(value)); + return value; +} + +// ── index format 2 ────────────────────────────────────────────────────────────────── + +using rabitq_test::format2::IndexKind; +namespace f2 = rabitq_test::format2; + +// An index of 33 points over four clusters, with point 3 removed, saved at `path`. +std::unique_ptr saved_format2_index( + const std::string& path, + size_t total_bits, + MetricType metric, + InitializerType initializer, + RotatorType rotator = RotatorType::FhtKacRotator +) { + constexpr size_t kCount = 33; + const auto points = random_rows(kCount, 951); + const auto centroids = random_rows(kDynNumClusters, 952); + const auto assigned = nearest_centroids(centroids, points.data(), kCount, metric); + auto index = std::make_unique( + kCount, kDynDim, kDynNumClusters, total_bits, metric, rotator, initializer + ); + index->construct(points.data(), centroids.data(), assigned.data(), false, 1); + const PID removed = 3; + index->remove(&removed, 1); + index->save(path.c_str()); + return index; +} + +TEST(IvfFormat2Test, WritesOneFileWithTheSpecifiedHeaderAndSections) { + const rabitq_test::TempFile path_file("rabitq_ivf_format2_layout.index"); + const std::string& path = path_file.path(); + const struct { + size_t total_bits; + MetricType metric; + InitializerType initializer; + f2::Metric stored_metric; + uint32_t code_bits; + f2::RawType raw_type; + uint32_t routing; + bool rerank; + } cases[] = { + {5, + METRIC_L2, + InitializerType::HNSW, + f2::Metric::L2, + 5, + f2::RawType::None, + 260, + true}, + {1, + METRIC_IP, + InitializerType::Flat, + f2::Metric::InnerProduct, + 1, + f2::RawType::None, + 258, + false}, + {32, + METRIC_L2, + InitializerType::FlatRaBitQ, + f2::Metric::L2, + 1, + f2::RawType::Float32, + 259, + true}, + }; + for (const auto& c : cases) { + const auto index = saved_format2_index(path, c.total_bits, c.metric, c.initializer); + EXPECT_FALSE(std::filesystem::exists(path + ".hnsw")); + const auto contents = + rabitq_test::read_format2_file(path, IndexKind::Ivf, rabitq_test::kIvfSections); + const f2::Header& header = contents.header; + EXPECT_EQ(header.metric, c.stored_metric); + EXPECT_EQ(header.dim, kDynDim); + EXPECT_EQ(header.padded_dim, 96U); + EXPECT_EQ(header.code_bits, c.code_bits); + EXPECT_EQ(header.raw_type, c.raw_type); + EXPECT_EQ(header.id_bits, 32U); + EXPECT_FALSE(header.seed.has_value()); + EXPECT_EQ(header.num_points, 33U); + std::vector kinds; + for (const auto& entry : contents.sections) { + kinds.push_back(entry.first.kind); + } + std::vector expected = {1, 256, 257, c.routing, 261}; + if (c.rerank) { + expected.push_back(262); + } + expected.push_back(263); + EXPECT_EQ(kinds, expected); + + IVF loaded; + loaded.load(path.c_str()); + EXPECT_EQ(loaded.initializer_type(), c.initializer); + EXPECT_EQ(loaded.num_points(), 32U); + std::vector expected_ids(5), ids(5); + std::vector expected_distances(5), distances(5); + const auto query = random_rows(1, 953); + index->search( + query.data(), 5, kDynNumClusters, expected_ids.data(), expected_distances.data() + ); + loaded.search(query.data(), 5, kDynNumClusters, ids.data(), distances.data()); + EXPECT_EQ(ids, expected_ids); + EXPECT_EQ(distances, expected_distances); + } +} + +TEST(IvfFormat2Test, RejectsContentsThatDoNotFitAnIvfIndex) { + const rabitq_test::TempFile path_file("rabitq_ivf_format2_reject.index"); + const std::string& path = path_file.path(); + saved_format2_index(path, 5, METRIC_L2, InitializerType::HNSW); + const auto original = + rabitq_test::read_format2_file(path, IndexKind::Ivf, rabitq_test::kIvfSections); + IVF loaded; + loaded.load(path.c_str()); + + const auto expect_rejected = + [&](const char* name, auto edit, const std::string& message) { + SCOPED_TRACE(name); + auto contents = original; + edit(contents); + rabitq_test::write_format2_file(path, contents); + try { + loaded.load(path.c_str()); + ADD_FAILURE() << "accepted"; + } catch (const std::runtime_error& error) { + EXPECT_EQ(std::string(error.what()), message); + } + EXPECT_EQ(loaded.num_points(), 32U); // the failed load keeps the loaded index + }; + const auto set_u32 = [](std::string& bytes, size_t offset, uint32_t value) { + std::memcpy(bytes.data() + offset, &value, sizeof(value)); + }; + expect_rejected( + "cosine", + [](auto& c) { c.header.metric = f2::Metric::Cosine; }, + "IVF index file uses the cosine metric, which this release does not support" + ); + expect_rejected( + "64-bit IDs", + [](auto& c) { c.header.id_bits = 64; }, + "IVF index file has 64-bit IDs; IVF-RaBitQ stores 32 bits" + ); + expect_rejected( + "uint8 raw vectors", + [](auto& c) { c.header.raw_type = f2::RawType::Uint8; }, + "IVF index file has uint8 raw vectors, which IVF-RaBitQ lacks" + ); + expect_rejected( + "raw vectors with extra bits", + [](auto& c) { c.header.raw_type = f2::RawType::Float32; }, + "IVF index file has 5 code bits, which its raw vectors do not allow" + ); + expect_rejected( + "no points", + [](auto& c) { c.header.num_points = 0; }, + "IVF index file has an invalid point count 0" + ); + expect_rejected( + "missing codes", + [](auto& c) { c.remove(261); }, + "IVF index file has no section kind 261" + ); + expect_rejected( + "short parameters", + [](auto& c) { c.bytes(256).pop_back(); }, + "IVF index file section kind 256 has 15 bytes; expected 16" + ); + expect_rejected( + "unknown routing", + [&](auto& c) { set_u32(c.bytes(256), 8, 9); }, + "IVF index file has invalid routing parameters" + ); + expect_rejected( + "cluster sizes above the point count", + [&](auto& c) { set_u32(c.bytes(257), 0, get_size(c.bytes(257), 0) + 1); }, + "IVF index file cluster sizes exceed its point count" + ); + expect_rejected( + "cluster sizes below the point count", + [&](auto& c) { + std::string& sizes = c.bytes(257); + for (size_t offset = 0; offset < sizes.size(); offset += sizeof(uint64_t)) { + if (get_size(sizes, offset) != 0) { + set_u32(sizes, offset, get_size(sizes, offset) - 1); + break; + } + } + }, + "IVF index file cluster sizes do not add up to its point count" + ); + expect_rejected( + "routing of another initializer", + [](auto& c) { + f2::Section flat; + flat.kind = 258; + flat.flags = f2::kSectionRequired; + c.sections.insert( + c.sections.begin() + 4, {flat, std::string(4 * 4 * 96, '\0')} + ); + }, + "IVF index file has routing data of another initializer" + ); + expect_rejected( + "corrupt rotator", + [](auto& c) { c.bytes(1)[70] ^= 1; }, + "IVF index file has an invalid rotator (data checksum mismatch (corrupt blob))" + ); + expect_rejected( + "rotator of another dimension", + [](auto& c) { + const std::unique_ptr> other( + choose_rotator(64, RotatorType::FhtKacRotator, 96, 1) + ); + std::string blob( + rabitqlib::detail::rotator_blob_bytes(RotatorType::FhtKacRotator, 64, 96), + '\0' + ); + rabitqlib::detail::encode_rotator_blob( + *other, + RotatorType::FhtKacRotator, + 64, + std::nullopt, + reinterpret_cast(blob.data()) + ); + c.bytes(1) = blob; + }, + "IVF index file rotator does not match its dimensions" + ); + expect_rejected( + "point ID out of range", + [&](auto& c) { set_u32(c.bytes(263), 0, 33); }, + "Invalid point ID in IVF index file" + ); + expect_rejected( + "missing rerank data", + [](auto& c) { c.remove(262); }, + "IVF index file has no section kind 262" + ); + expect_rejected( + "short IDs", + [](auto& c) { c.bytes(263).resize(128); }, + "IVF index file section kind 263 has 128 bytes; expected 132" + ); + expect_rejected( + "reserved routing field", + [&](auto& c) { set_u32(c.bytes(256), 12, 1); }, + "IVF index file has invalid routing parameters" + ); + + // One-bit codes have no rerank data, so a rerank section is rejected. + saved_format2_index(path, 1, METRIC_L2, InitializerType::Flat); + auto one_bit = + rabitq_test::read_format2_file(path, IndexKind::Ivf, rabitq_test::kIvfSections); + f2::Section rerank; + rerank.kind = 262; + rerank.flags = f2::kSectionRequired; + one_bit.sections.insert(one_bit.sections.end() - 1, {rerank, std::string(64, '\0')}); + rabitq_test::write_format2_file(path, one_bit); + try { + loaded.load(path.c_str()); + ADD_FAILURE() << "accepted"; + } catch (const std::runtime_error& error) { + EXPECT_STREQ( + error.what(), "IVF index file has rerank data that its code bits do not use" + ); + } +} + +TEST(IvfFormat2Test, UnknownReadOnlyFeaturesAllowSearchButNoChanges) { + const rabitq_test::TempFile path_file("rabitq_ivf_format2_read_only.index"); + const std::string& path = path_file.path(); + const auto index = saved_format2_index(path, 5, METRIC_L2, InitializerType::Flat); + rabitq_test::patch_format2_header(path, 24, 1, 8); // an unknown ro_compat bit + IVF loaded; + ASSERT_NO_THROW(loaded.load(path.c_str())); + const auto query = random_rows(1, 954); + std::vector expected_ids(5), ids(5); + std::vector distances(5); + index->search(query.data(), 5, kDynNumClusters, expected_ids.data(), distances.data()); + loaded.search(query.data(), 5, kDynNumClusters, ids.data(), distances.data()); + EXPECT_EQ(ids, expected_ids); + + const auto expect_refused = [](const auto& operation, const std::string& name) { + try { + operation(); + ADD_FAILURE() << name << " was allowed"; + } catch (const std::logic_error& error) { + EXPECT_EQ( + std::string(error.what()), + "IVF index was loaded from a file with features this release cannot " + "update, so it cannot " + + name + ); + } + }; + const auto row = random_rows(1, 955); + const PID cluster = 0; + const PID removed = 0; + expect_refused([&] { loaded.add(row.data(), 1, &cluster, false, 1); }, "add"); + expect_refused([&] { loaded.reserve(0, 64); }, "reserve"); + expect_refused([&] { loaded.remove(&removed, 1); }, "remove"); + expect_refused([&] { loaded.save((path + ".copy").c_str()); }, "save"); + EXPECT_EQ(loaded.num_points(), 32U); + + // Rebuilding replaces the data the unknown features described, so it is writable. + const auto points = random_rows(33, 951); + const auto centroids = random_rows(kDynNumClusters, 952); + const auto assigned = nearest_centroids(centroids, points.data(), 33, METRIC_L2); + loaded.construct(points.data(), centroids.data(), assigned.data(), false, 1); + EXPECT_NO_THROW(loaded.save(path.c_str())); + + // An unknown incompatible feature is rejected outright. + rabitq_test::patch_format2_header(path, 16, 2, 8); + try { + loaded.load(path.c_str()); + ADD_FAILURE() << "an unknown incompatible feature was accepted"; + } catch (const std::runtime_error& error) { + EXPECT_EQ( + std::string(error.what()), + std::string( + "IVF index file uses incompatible features 0x2 that this release " + "does not know; written by rabitqlib " + ) + RABITQ_TEST_LIBRARY_VERSION + ); + } +} + +TEST(IvfFormat2Test, RecordsTheWriterAndSkipsUnknownOptionalSections) { + const rabitq_test::TempFile path_file("rabitq_ivf_format2_optional.index"); + const std::string& path = path_file.path(); + const auto index = saved_format2_index(path, 4, METRIC_IP, InitializerType::FlatRaBitQ); + auto contents = + rabitq_test::read_format2_file(path, IndexKind::Ivf, rabitq_test::kIvfSections); + EXPECT_EQ(contents.writer_version, RABITQ_TEST_LIBRARY_VERSION); + // A later release may add an optional section; this one skips it. + f2::Section future; + future.kind = 900; + contents.sections.emplace_back(future, std::string(100, 'x')); + rabitq_test::write_format2_file(path, contents); + IVF loaded; + ASSERT_NO_THROW(loaded.load(path.c_str())); + const auto query = random_rows(1, 956); + std::vector expected_ids(5), ids(5); + std::vector distances(5); + index->search(query.data(), 5, kDynNumClusters, expected_ids.data(), distances.data()); + loaded.search(query.data(), 5, kDynNumClusters, ids.data(), distances.data()); + EXPECT_EQ(ids, expected_ids); + // save drops the section it does not know. + loaded.save(path.c_str()); + std::vector kinds = rabitq_test::kIvfSections; + kinds.push_back(900); + const auto resaved = rabitq_test::read_format2_file(path, IndexKind::Ivf, kinds); + EXPECT_EQ(resaved.sections.size(), contents.sections.size() - 1); + EXPECT_THROW( + static_cast(rabitq_test::Format2Contents(resaved).bytes(900)), + std::runtime_error + ); +} + +TEST(IvfFormat2Test, FailedWriteReportsAnErrorAndKeepsTheIndex) { + if (!std::filesystem::exists("/dev/full")) { + GTEST_SKIP() << "Requires a device that rejects writes"; + } + const rabitq_test::TempFile path_file("rabitq_ivf_format2_write_failure.index"); + const std::string& path = path_file.path(); + for (const auto initializer : + {InitializerType::Flat, InitializerType::FlatRaBitQ, InitializerType::HNSW}) { + const auto index = saved_format2_index(path, 5, METRIC_L2, initializer); + // /dev/full is not a regular file, so save writes it in place and the write fails. + EXPECT_ANY_THROW(index->save("/dev/full")); + index->save(path.c_str()); + IVF loaded; + ASSERT_NO_THROW(loaded.load(path.c_str())); + EXPECT_EQ(loaded.num_points(), 32U); + } +} + } // namespace } // namespace rabitqlib::ivf -TEST(IvfSidecarValidationTest, RejectsMismatchedVectorStorage) { +TEST(IvfRoutingGraphValidationTest, RejectsMismatchedVectorStorage) { using namespace rabitqlib; for (const auto metric : {METRIC_L2, METRIC_IP}) { for (const size_t sidecar_dim : {64U, 128U}) { @@ -2034,12 +2428,19 @@ TEST(IvfSidecarValidationTest, RejectsMismatchedVectorStorage) { original.save(path.c_str()); ivf::HNSWInitializer other(sidecar_dim, 1, metric); other.add_vectors(center.data(), 1); - std::ofstream ignored; - other.save(ignored, path.c_str()); + std::ostringstream graph; + other.write(graph); + rabitq_test::replace_format2_section( + path, + rabitq_test::format2::IndexKind::Ivf, + rabitq_test::kIvfSections, + 260, + graph.str() + ); ivf::IVF loaded; try { loaded.load(path.c_str()); - FAIL() << "mismatched sidecar must be rejected"; + FAIL() << "mismatched routing graph must be rejected"; } catch (const std::runtime_error& error) { EXPECT_STREQ( error.what(), "HNSW centroid layout does not match IVF dimension" diff --git a/tests/unit/rabitqlib/utils/blob_test.cpp b/tests/unit/rabitqlib/utils/blob_test.cpp new file mode 100644 index 00000000..b7d61100 --- /dev/null +++ b/tests/unit/rabitqlib/utils/blob_test.cpp @@ -0,0 +1,206 @@ +#include "blob.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "crc32.hpp" +#include "rabitqlib/core/rotator.hpp" +#include "rabitqlib/core/types.hpp" + +namespace { +using rabitqlib::RotatorType; +namespace detail = rabitqlib::detail; + +std::vector read_fixture(const std::string& name) { + std::ifstream input( + std::string(RABITQ_QUANT_FIXTURE_ROOT) + "/" + name, std::ios::binary + ); + EXPECT_TRUE(input.is_open()) << name; + return {std::istreambuf_iterator(input), std::istreambuf_iterator()}; +} + +std::vector encode( + const rabitqlib::Rotator& rotator, + RotatorType type, + size_t dim, + std::optional seed +) { + std::vector blob(detail::rotator_blob_bytes(type, dim, rotator.size())); + detail::encode_rotator_blob(rotator, type, dim, seed, blob.data()); + return blob; +} + +std::vector rotate(const rabitqlib::Rotator& rotator, size_t dim) { + std::vector input(dim); + for (size_t i = 0; i < dim; ++i) { + input[i] = static_cast(i % 7) - 3.0F; + } + std::vector output(rotator.size()); + rotator.rotate(input.data(), output.data()); + return output; +} + +void put_u32(std::vector& blob, size_t offset, uint32_t value) { + for (size_t i = 0; i < 4; ++i) { + blob[offset + i] = static_cast(value >> (8 * i)); + } +} + +// Recompute the checksum after an edit, so that the check under test fires. +void reseal(std::vector& blob) { + put_u32(blob, 32, 0); + put_u32(blob, 32, detail::crc32(blob.data(), blob.size())); +} + +void expect_rejected(const std::vector& blob, const std::string& message) { + try { + detail::decode_rotator_blob(blob.data(), blob.size()); + ADD_FAILURE() << "expected std::invalid_argument: " << message; + } catch (const std::invalid_argument& error) { + EXPECT_EQ(std::string(error.what()), message); + } +} + +TEST(CoreBlobTest, Crc32MatchesZlib) { + EXPECT_EQ(detail::crc32("", 0), 0U); + EXPECT_EQ(detail::crc32("123456789", 9), 0xCBF43926U); + std::vector data(1024); + for (size_t i = 0; i < data.size(); ++i) { + data[i] = static_cast(i); + } + EXPECT_EQ(detail::crc32(data.data(), data.size()), 0xB70B4C26U); // zlib.crc32 + const uint32_t first = detail::crc32(data.data(), 100); + EXPECT_EQ(detail::crc32(data.data() + 100, data.size() - 100, first), 0xB70B4C26U); +} + +// The golden files rabitqlib.quant writes: the C++ codec reads and rewrites them +// byte for byte, and they hold the same rotation as a freshly seeded rotator. +TEST(CoreBlobTest, ReadsAndRewritesTheQuantGoldenRotators) { + const struct { + const char* name; + size_t dim; + size_t padded_dim; + uint32_t seed; + } cases[] = { + {"rotator_fht_kac_dim64_seed0.bin", 64, 64, 0}, + {"rotator_fht_kac_dim100_padded128_seed42.bin", 100, 128, 42}, + }; + for (const auto& c : cases) { + const std::vector golden = read_fixture(c.name); + const detail::DecodedRotator decoded = + detail::decode_rotator_blob(golden.data(), golden.size()); + EXPECT_EQ(decoded.type, RotatorType::FhtKacRotator); + EXPECT_EQ(decoded.dim, c.dim); + EXPECT_EQ(decoded.padded_dim, c.padded_dim); + ASSERT_TRUE(decoded.seed.has_value()); + EXPECT_EQ(*decoded.seed, c.seed); + EXPECT_EQ( + encode(*decoded.rotator, decoded.type, decoded.dim, decoded.seed), golden + ); + + const std::unique_ptr> fresh( + rabitqlib::choose_rotator( + c.dim, RotatorType::FhtKacRotator, c.padded_dim, c.seed + ) + ); + EXPECT_EQ(rotate(*decoded.rotator, c.dim), rotate(*fresh, c.dim)); + } +} + +TEST(CoreBlobTest, RoundTripsAMatrixRotatorWithoutASeed) { + const std::unique_ptr> rotator( + rabitqlib::choose_rotator(40, RotatorType::MatrixRotator, 64, 7) + ); + const std::vector blob = encode(*rotator, RotatorType::MatrixRotator, 40, {}); + EXPECT_EQ(blob.size(), 64 + (4 * 40 * 64)); + const detail::DecodedRotator decoded = + detail::decode_rotator_blob(blob.data(), blob.size()); + EXPECT_EQ(decoded.type, RotatorType::MatrixRotator); + EXPECT_EQ(decoded.dim, 40U); + EXPECT_EQ(decoded.padded_dim, 64U); + EXPECT_FALSE(decoded.seed.has_value()); + EXPECT_EQ(rotate(*decoded.rotator, 40), rotate(*rotator, 40)); +} + +TEST(CoreBlobTest, EncoderRejectsDimensionsTheFormatDoesNotAllow) { + const std::unique_ptr> rotator( + rabitqlib::choose_rotator(40, RotatorType::MatrixRotator, 64, 7) + ); + std::vector blob( + detail::rotator_blob_bytes(RotatorType::MatrixRotator, 40, 64) + ); + EXPECT_THROW( + detail::encode_rotator_blob( + *rotator, RotatorType::FhtKacRotator, 40, {}, blob.data() + ), + std::invalid_argument + ); + EXPECT_THROW( + detail::encode_rotator_blob( + *rotator, RotatorType::MatrixRotator, 41, {}, blob.data() + ), + std::invalid_argument + ); +} + +TEST(CoreBlobTest, RejectsInvalidBlobsWithTheQuantMessages) { + const std::vector golden = read_fixture("rotator_fht_kac_dim64_seed0.bin"); + const auto edited = [&golden](size_t offset, uint32_t value) { + std::vector blob = golden; + put_u32(blob, offset, value); + reseal(blob); + return blob; + }; + + expect_rejected( + std::vector(golden.begin(), golden.begin() + 39), + "data is too short for a rabitqlib blob" + ); + std::vector blob = golden; + blob[0] = 'X'; + expect_rejected(blob, "data is not a rabitqlib blob (bad magic)"); + expect_rejected(edited(8, 3), "data is not a rotator blob; got kind 3"); + expect_rejected( + edited(12, 2), + "data has unsupported rotator format version 2; this release reads version 1" + ); + expect_rejected( + edited(16, 72), "data has an invalid header size for rotator version 1" + ); + expect_rejected( + std::vector(golden.begin(), golden.end() - 1), + "data length does not match its header (truncated or padded)" + ); + blob = golden; + blob[70] ^= 1; + expect_rejected(blob, "data checksum mismatch (corrupt blob)"); + expect_rejected(edited(20, 2), "data has unknown flags"); + expect_rejected(edited(60, 1), "data has non-zero reserved fields"); + expect_rejected(edited(40, 3), "data has an unknown rotator method 3"); + expect_rejected( + edited(44, 2), "data stores a float64 rotator, which version 1 cannot load" + ); + expect_rejected(edited(44, 9), "data has an unknown scalar type 9"); + expect_rejected( + edited(48, 32), + "data has invalid rotator dimensions: dim must be at least 64 for method " + "'fht_kac'; got 32" + ); + std::vector seeded = + read_fixture("rotator_fht_kac_dim100_padded128_seed42.bin"); + put_u32(seeded, 20, 0); // seed 42 without seed_known + reseal(seeded); + expect_rejected(seeded, "data has a seed without the seed_known flag"); + expect_rejected(edited(52, 96), "data body size does not match the rotator parameters"); +} + +} // namespace From ad4317df01cf98769a1c384018a98a5583c3165f Mon Sep 17 00:00:00 2001 From: gouyt13clear Date: Sat, 7 Nov 2026 16:36:23 +0800 Subject: [PATCH 04/11] fix(ivf): migrate the load benchmark and a Windows test path The HNSW load benchmark still called the removed Initializer save and load; write and read the graph file through the stream interface, with the file open inside the timing as before. The legacy-sidecar test named its directory with a narrow literal, which MSVC encodes in the code page; use a UTF-8 literal. --- benchmarks/hotpaths/load.cpp | 21 +++++++++++++++---- .../index/historical_persistence_test.cpp | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/benchmarks/hotpaths/load.cpp b/benchmarks/hotpaths/load.cpp index 8e4d0de2..dc1c6c84 100644 --- a/benchmarks/hotpaths/load.cpp +++ b/benchmarks/hotpaths/load.cpp @@ -37,8 +37,11 @@ void run(int argc, char** argv) { if (!std::filesystem::exists(rabitqlib::io_impl::filesystem_path(path + ".hnsw"))) { rabitqlib::ivf::HNSWInitializer index(dim, count); index.add_vectors(data.data(), 1); - std::ofstream unused; - index.save(unused, path.c_str()); + std::ofstream output( + rabitqlib::io_impl::filesystem_path(path + ".hnsw"), std::ios::binary + ); + output.exceptions(std::ios::failbit | std::ios::badbit); + index.write(output); return; } } @@ -46,9 +49,19 @@ void run(int argc, char** argv) { uint64_t expected = 0; for (size_t rep = 0; rep <= reps; ++rep) { rabitqlib::ivf::HNSWInitializer index(dim, count); - std::ifstream unused; const auto start = bench::Clock::now(); - index.load(unused, path.c_str()); + // Open the graph file inside the timing, as loading an index does. + std::ifstream input( + rabitqlib::io_impl::filesystem_path(path + ".hnsw"), + std::ios::binary | std::ios::ate + ); + if (!input) { + throw std::runtime_error("Cannot open the HNSW centroid graph file"); + } + const auto bytes = + static_cast(static_cast(input.tellg())); + input.seekg(0); + index.read(input, bytes); const double seconds = std::chrono::duration(bench::Clock::now() - start).count(); uint64_t h = 14695981039346656037ULL; diff --git a/tests/unit/rabitqlib/index/historical_persistence_test.cpp b/tests/unit/rabitqlib/index/historical_persistence_test.cpp index 7be5605d..d1a45ede 100644 --- a/tests/unit/rabitqlib/index/historical_persistence_test.cpp +++ b/tests/unit/rabitqlib/index/historical_persistence_test.cpp @@ -201,7 +201,7 @@ TEST(HistoricalPersistenceTest, LoadsTheHnswSidecarOfEarlierLayouts) { fs::path(RABITQ_TEST_FIXTURE_ROOT) / "v0.8.3" / "ivf4_hnsw_l2.index"; const fs::path directory = fs::temp_directory_path() / - fs::u8path(rabitq_test::process_unique("historical_sidecar_\u6d4b\u8bd5")); + fs::u8path(rabitq_test::process_unique(u8"historical_sidecar_\u6d4b\u8bd5")); fs::create_directories(directory); const fs::path index_path = directory / fs::u8path(u8"\u7d22\u5f15.index"); const fs::path sidecar = fs::path(index_path.native() + fs::path(".hnsw").native()); From 8c6951c3924b2578a3746860f1606854758b1f39 Mon Sep 17 00:00:00 2001 From: gouyt13clear Date: Sat, 7 Nov 2026 16:46:07 +0800 Subject: [PATCH 05/11] test(ivf): build both HNSW routing graphs with one thread The first-add test compares saved files, which now include the HNSW routing graph. Training the explicit index with several threads built that graph in a nondeterministic order, so the files differed on CI; train it with one thread, like the implicit first add. Document that the routing graph is reproducible only when built with one thread. --- docs/docs/index/usage.md | 3 ++- docs/docs/releases/unreleased.md | 1 + tests/python/test_train_api.py | 6 ++++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/docs/index/usage.md b/docs/docs/index/usage.md index 77e54321..22023ef2 100644 --- a/docs/docs/index/usage.md +++ b/docs/docs/index/usage.md @@ -148,7 +148,8 @@ native call, or pass `num_threads=1`, in such processes. clustering classes default to `0`. `search` defaults to one thread (`None` or `1`), as do the deprecated `build`, which requires an integer, and `search_batch`. Multithreaded HNSW-RaBitQ `add` links points in a nondeterministic order, so pass -`num_threads=1` when a build must be reproducible. +`num_threads=1` when a build must be reproducible. The same holds for the centroid graph +of IVF-RaBitQ HNSW routing, which `train`, `set_centroids` or the first `add` builds. On Linux, CPU detection accounts for affinity and OpenMP binding. Apply binding before starting the program. diff --git a/docs/docs/releases/unreleased.md b/docs/docs/releases/unreleased.md index 1768230d..8fe6d237 100644 --- a/docs/docs/releases/unreleased.md +++ b/docs/docs/releases/unreleased.md @@ -4,6 +4,7 @@ - IVF-RaBitQ `save` writes [index format 2](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/docs/formats.md#index-format-2), a single file with a header, a section table and checksummed metadata. HNSW centroid routing is stored inside it, so `save` no longer writes a `.hnsw` sidecar. A sidecar left by an earlier save is not removed. - Releases up to 0.8 cannot read these files. Keep a copy saved by 0.8 if you may downgrade. +- The saved bytes now include the HNSW routing graph, which is reproducible only when `train`, `set_centroids` or the first `add` builds it with `num_threads=1`; saving the same index twice still gives identical files. - Files written by every earlier release still load, including the `.hnsw` sidecars of HNSW-routed files; saving one writes format 2 with the same rotation, codes and results. - Loading a damaged or mismatched format-2 file raises `RuntimeError` with a message naming the problem, for example `IVF index file is 6916 bytes, but its sections end at 6852` for trailing bytes. - `save` rejects IVF-RaBitQ indexes of more than 65,536 dimensions. diff --git a/tests/python/test_train_api.py b/tests/python/test_train_api.py index f80e062b..f1e4794a 100644 --- a/tests/python/test_train_api.py +++ b/tests/python/test_train_api.py @@ -496,9 +496,11 @@ def make(): return index implicit = make() - implicit.add(base_data, num_threads=1) + implicit.add(base_data, num_threads=1) # trains with the add's single thread explicit = make() - explicit.train(base_data) + # The saved file includes HNSW routing, whose graph is reproducible only when + # it is built with one thread. + explicit.train(base_data, num_threads=1) explicit.add(base_data, num_threads=1) assert saved_bytes(implicit, tmp_path / "implicit.index") == saved_bytes( explicit, tmp_path / "explicit.index" From 5b7dd4f8fcdb66260a96ab5d5f360be7c72262dc Mon Sep 17 00:00:00 2001 From: gouyt13clear Date: Sat, 7 Nov 2026 17:40:07 +0800 Subject: [PATCH 06/11] test(persistence): freeze v0.8.3 HNSW-RaBitQ and SymphonyQG fixtures Before HNSW-RaBitQ and SymphonyQG move to index format 2, freeze files their v0.8.3 writers produce: HNSW at 1, 5 and 9 bits with removals and upper layers, and SymphonyQG raw, 4- and 8-bit with FHT/Kac and matrix rotation. A separate generator writes them, so the committed IVF fixtures keep their bytes. --- .../persistence/v0.8.3/CMakeLists.txt | 2 + tests/fixtures/persistence/v0.8.3/README.md | 34 +++-- .../persistence/v0.8.3/generate_graphs.cpp | 121 ++++++++++++++++++ .../persistence/v0.8.3/hnsw1_l2.expected | 17 +++ .../persistence/v0.8.3/hnsw1_l2.index | Bin 0 -> 15909 bytes .../persistence/v0.8.3/hnsw5_ip.expected | 17 +++ .../persistence/v0.8.3/hnsw5_ip.index | Bin 0 -> 23189 bytes .../persistence/v0.8.3/hnsw9_l2.expected | 17 +++ .../persistence/v0.8.3/hnsw9_l2.index | Bin 0 -> 29429 bytes .../fixtures/persistence/v0.8.3/manifest.json | 18 ++- .../persistence/v0.8.3/qg0_ip.expected | 17 +++ .../fixtures/persistence/v0.8.3/qg0_ip.index | Bin 0 -> 133726 bytes .../persistence/v0.8.3/qg4_l2.expected | 17 +++ .../fixtures/persistence/v0.8.3/qg4_l2.index | Bin 0 -> 107610 bytes .../persistence/v0.8.3/qg4_matrix_l2.expected | 17 +++ .../persistence/v0.8.3/qg4_matrix_l2.index | Bin 0 -> 132522 bytes .../persistence/v0.8.3/qg8_ip.expected | 17 +++ .../fixtures/persistence/v0.8.3/qg8_ip.index | Bin 0 -> 113850 bytes .../index/historical_persistence_test.cpp | 9 +- 19 files changed, 291 insertions(+), 12 deletions(-) create mode 100644 tests/fixtures/persistence/v0.8.3/generate_graphs.cpp create mode 100644 tests/fixtures/persistence/v0.8.3/hnsw1_l2.expected create mode 100644 tests/fixtures/persistence/v0.8.3/hnsw1_l2.index create mode 100644 tests/fixtures/persistence/v0.8.3/hnsw5_ip.expected create mode 100644 tests/fixtures/persistence/v0.8.3/hnsw5_ip.index create mode 100644 tests/fixtures/persistence/v0.8.3/hnsw9_l2.expected create mode 100644 tests/fixtures/persistence/v0.8.3/hnsw9_l2.index create mode 100644 tests/fixtures/persistence/v0.8.3/qg0_ip.expected create mode 100644 tests/fixtures/persistence/v0.8.3/qg0_ip.index create mode 100644 tests/fixtures/persistence/v0.8.3/qg4_l2.expected create mode 100644 tests/fixtures/persistence/v0.8.3/qg4_l2.index create mode 100644 tests/fixtures/persistence/v0.8.3/qg4_matrix_l2.expected create mode 100644 tests/fixtures/persistence/v0.8.3/qg4_matrix_l2.index create mode 100644 tests/fixtures/persistence/v0.8.3/qg8_ip.expected create mode 100644 tests/fixtures/persistence/v0.8.3/qg8_ip.index diff --git a/tests/fixtures/persistence/v0.8.3/CMakeLists.txt b/tests/fixtures/persistence/v0.8.3/CMakeLists.txt index 4ddc10df..4fd4b3e5 100644 --- a/tests/fixtures/persistence/v0.8.3/CMakeLists.txt +++ b/tests/fixtures/persistence/v0.8.3/CMakeLists.txt @@ -11,3 +11,5 @@ set(RABITQ_ENABLE_NATIVE_OPTIMIZATION OFF CACHE BOOL "" FORCE) add_subdirectory("${RABITQ_HISTORICAL_SOURCE}" historical) add_executable(generate_fixtures generate.cpp) target_link_libraries(generate_fixtures PRIVATE rabitqlib::index) +add_executable(generate_graph_fixtures generate_graphs.cpp) +target_link_libraries(generate_graph_fixtures PRIVATE rabitqlib::index) diff --git a/tests/fixtures/persistence/v0.8.3/README.md b/tests/fixtures/persistence/v0.8.3/README.md index 72bd9bcd..74091fa0 100644 --- a/tests/fixtures/persistence/v0.8.3/README.md +++ b/tests/fixtures/persistence/v0.8.3/README.md @@ -1,7 +1,8 @@ -# IVF-RaBitQ fixtures from v0.8.3 +# Index fixtures from v0.8.3 These files were written by release **v0.8.3**, commit -`dabfdb8c200a6f155df4edd0b9c825e6728b14a1`, using [generate.cpp](generate.cpp). +`dabfdb8c200a6f155df4edd0b9c825e6728b14a1`, using [generate.cpp](generate.cpp) for +IVF-RaBitQ and [generate_graphs.cpp](generate_graphs.cpp) for HNSW-RaBitQ and SymphonyQG. [manifest.json](manifest.json) records the producer and SHA-256 hashes. Keep these files unchanged; tests never regenerate them with the current writer. @@ -17,10 +18,24 @@ HNSW routing writes beside the index. The five cases are: | `ivf32_hnsw_ip` | raw (`nbits=32`) | inner product | FHT/Kac | HNSW, with sidecar | | `ivf1_flat_matrix_l2` | 1 | L2 | matrix | Flat | -Each index holds 130 points in 65 original dimensions (96 padded) across four clusters -of 0, 1, 32 and 97 points, with point 0 removed, as in +The graph generator, run separately, adds the last HNSW-RaBitQ and SymphonyQG layouts +before index format 2: + +| File | Bits | Metric | Rotator | Notes | +| --- | --- | --- | --- | --- | +| `hnsw1_l2` | 1 | L2 | FHT/Kac | M = 8, `ef_construction` 64, seed 42, point 0 removed | +| `hnsw5_ip` | 5 | inner product | FHT/Kac | As above | +| `hnsw9_l2` | 9 | L2 | FHT/Kac | As above | +| `qg0_ip` | raw | inner product | FHT/Kac | Degree 32, PiPNN, `ef_construction` 64, seed 42 | +| `qg4_l2` | 4 | L2 | FHT/Kac | As above | +| `qg8_ip` | 8 | inner product | FHT/Kac | As above | +| `qg4_matrix_l2` | 4 | L2 | matrix | As above | + +Each index holds 130 points in 65 original dimensions (96 padded); the IVF-RaBitQ and +HNSW-RaBitQ files use four clusters of 0, 1, 32 and 97 points, with point 0 removed, as in [pre-ivf-per-cluster-storage](../pre-ivf-per-cluster-storage/README.md). The -`.expected` files hold reference results of a search that probes every cluster. +`.expected` files hold reference results: IVF-RaBitQ probes every cluster, and the graph +indexes search with `ef` 64. ## Deliberate regeneration @@ -32,10 +47,11 @@ git archive v0.8.3 | tar -x -C build/fixtures/v0.8.3 cmake -S tests/fixtures/persistence/v0.8.3 -B build/fixtures/v0.8.3-generator \ -DRABITQ_HISTORICAL_SOURCE="$PWD/build/fixtures/v0.8.3" \ -DCMAKE_BUILD_TYPE=Release -cmake --build build/fixtures/v0.8.3-generator --target generate_fixtures +cmake --build build/fixtures/v0.8.3-generator --target generate_fixtures generate_graph_fixtures ``` -Run `generate_fixtures` with a new output directory and `OMP_NUM_THREADS=1`. -Rotation is seeded from `random_device`, so the bytes differ on every run; each index, -its sidecar and its `.expected` file form a fixed set. Record new hashes in the manifest +Run either generator with a new output directory and `OMP_NUM_THREADS=1`. +IVF-RaBitQ rotation is seeded from `random_device`, so those bytes differ on every run; +the graph indexes use seed 42. Each index, its sidecar and its `.expected` file form a +fixed set. Record new hashes in the manifest before adopting them. diff --git a/tests/fixtures/persistence/v0.8.3/generate_graphs.cpp b/tests/fixtures/persistence/v0.8.3/generate_graphs.cpp new file mode 100644 index 00000000..8b023493 --- /dev/null +++ b/tests/fixtures/persistence/v0.8.3/generate_graphs.cpp @@ -0,0 +1,121 @@ +// Build only against the pinned v0.8.3 source, never the current checkout. Writes the +// HNSW-RaBitQ and SymphonyQG fixtures; generate.cpp writes the IVF-RaBitQ ones. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rabitqlib/core/metric.hpp" +#include "rabitqlib/core/types.hpp" +#include "rabitqlib/index/hnsw/hnsw.hpp" +#include "rabitqlib/index/symqg/qg.hpp" +#include "rabitqlib/index/symqg/qg_builder.hpp" +#include "rabitqlib/index/types.hpp" + +int main(int argc, char** argv) { + if (argc != 2) { + throw std::invalid_argument("Pass the fixture output directory"); + } + using namespace rabitqlib; + // The IVF fixtures' data: cluster sizes 0, 1, 32 and 97. + constexpr size_t kCount = 130, kDim = 65, kClusters = 4, kQueries = 3, kTopK = 5; + const std::filesystem::path directory(argv[1]); + std::filesystem::create_directories(directory); + uint32_t state = 713; + const auto next = [&state] { + state = state * 1664525U + 1013904223U; + return static_cast(static_cast((state >> 8) % 2001) - 1000) / 256.0F; + }; + std::vector data(kCount * kDim), queries(kQueries * kDim); + std::vector centroids(kClusters * kDim); + for (auto& value : data) { + value = next(); + } + for (auto& value : queries) { + value = next(); + } + for (auto& value : centroids) { + value = next(); + } + std::vector clusters(kCount, 3); + clusters[0] = 1; + for (size_t i = 1; i < 33; ++i) { + clusters[i] = 2; + } + + const auto write_expected = [&](const std::string& name, + const std::vector& ids, + const std::vector& distances) { + std::ofstream expected(directory / (name + ".expected"), std::ios::binary); + expected.exceptions(std::ios::failbit | std::ios::badbit); + expected << std::setprecision(std::numeric_limits::max_digits10); + expected << kCount << ' ' << kDim << ' ' << kQueries << ' ' << kTopK << '\n'; + for (size_t i = 0; i < queries.size(); ++i) { + expected << queries[i] << (i + 1 == queries.size() ? '\n' : ' '); + } + for (size_t i = 0; i < ids.size(); ++i) { + expected << ids[i] << ' ' << distances[i] << '\n'; + } + expected.close(); + }; + + const struct { + const char* name; + size_t total_bits; + MetricType metric; + } hnsw_cases[] = { + {"hnsw1_l2", 1, METRIC_L2}, + {"hnsw5_ip", 5, METRIC_IP}, + {"hnsw9_l2", 9, METRIC_L2}, + }; + for (const auto& c : hnsw_cases) { + const std::string name = c.name; + hnsw::HierarchicalNSW index(kCount, kDim, c.total_bits, 8, 64, 42, c.metric); + std::vector assigned = clusters; + index.construct(kClusters, centroids.data(), kCount, data.data(), assigned.data(), 1); + const PID removed = 0; + index.remove(&removed, 1); + index.save((directory / (name + ".index")).string().c_str()); + std::vector ids(kQueries * kTopK); + std::vector distances(ids.size()); + const auto result = index.search(queries.data(), kQueries, kTopK, 64, 1); + for (size_t i = 0; i < kQueries; ++i) { + for (size_t j = 0; j < kTopK; ++j) { + ids[i * kTopK + j] = result[i][j].second; + distances[i * kTopK + j] = result[i][j].first; + } + } + write_expected(name, ids, distances); + } + + const struct { + const char* name; + size_t bits; // 0 stores raw vectors + MetricType metric; + RotatorType rotator; + } qg_cases[] = { + {"qg0_ip", 0, METRIC_IP, RotatorType::FhtKacRotator}, + {"qg4_l2", 4, METRIC_L2, RotatorType::FhtKacRotator}, + {"qg8_ip", 8, METRIC_IP, RotatorType::FhtKacRotator}, + {"qg4_matrix_l2", 4, METRIC_L2, RotatorType::MatrixRotator}, + }; + for (const auto& c : qg_cases) { + const std::string name = c.name; + symqg::QuantizedGraph index(kCount, kDim, 32, c.metric, c.rotator, c.bits, 42); + symqg::QGBuilder builder(index, 64, data.data(), 1, symqg::QGInitialization::PiPNN, 42); + builder.build(); + index.save((directory / (name + ".index")).string().c_str()); + std::vector ids(kQueries * kTopK); + std::vector distances(ids.size()); + index.set_ef(64); + index.search_batch(queries.data(), kQueries, kTopK, ids.data(), distances.data(), 1); + write_expected(name, ids, distances); + } + return 0; +} diff --git a/tests/fixtures/persistence/v0.8.3/hnsw1_l2.expected b/tests/fixtures/persistence/v0.8.3/hnsw1_l2.expected new file mode 100644 index 00000000..96a62dad --- /dev/null +++ b/tests/fixtures/persistence/v0.8.3/hnsw1_l2.expected @@ -0,0 +1,17 @@ +130 65 3 5 +0.7734375 -1.265625 2.28125 -3.40234375 -1.609375 0.91796875 2.7109375 -0.20703125 0.31640625 0.62890625 1.296875 2.8203125 -1.46484375 -0.171875 -1.53515625 2.46875 3.79296875 -3.61328125 0.52734375 -3.21484375 2.94921875 2.4453125 0.7265625 -1.44921875 2.546875 -2.00390625 -2.8125 -1.609375 -2.1953125 -3.625 2.00390625 -1.5 -0.57421875 3.296875 -2.828125 -3.30078125 -3.03515625 3.25 -2.89453125 0.54296875 1.21875 -2.16015625 -3.34375 2.84765625 -2.24609375 -1.34765625 -0.1484375 -3.7265625 -3.87109375 -0.2890625 1.7421875 -3.08203125 0.66015625 -1.3359375 1.62109375 2.5 -2.8828125 1.203125 -2.82421875 2.0703125 1.05078125 -2.21875 -0.7578125 -1.1796875 1.86328125 -1.00390625 -2.02734375 -2.69140625 3.0703125 -0.796875 -0.78515625 -2.34375 3.60546875 -1.81640625 -1.3671875 -0.80078125 3.63671875 2.00390625 -3.828125 1.8125 1.34765625 0.5234375 0.8359375 3.71484375 1.19140625 0.54296875 -0.06640625 3.25390625 1.68359375 -1.59375 3.03515625 1.22265625 -3.2734375 0.05859375 -0.84765625 -0.96484375 1.046875 -2.58984375 -0.3828125 -1.765625 0.00390625 3.5 0.1484375 0.234375 -1.9609375 -0.375 -3.37109375 -0.40234375 1.34765625 -3.12109375 1 -1.16015625 1.83984375 2.46484375 1.25 -1.3125 -3.7734375 -0.9765625 -1.390625 -0.16015625 -1.6484375 -2.6015625 2.40234375 3.03515625 2.64453125 -1.49609375 -1.9765625 -2.3515625 -1.57421875 -0.18359375 1.37109375 1.4765625 2.46875 -0.75390625 -0.6171875 1.484375 2.1328125 -0.73046875 -1.640625 3.1875 0.6484375 2.80859375 2.79296875 -1.578125 -3.72265625 -3.0546875 -3.57421875 -3.1953125 -3.51171875 -0.9921875 1.171875 0.7734375 -3.08984375 -0.07421875 1.25390625 1.55078125 -2.6171875 -0.5703125 0.21484375 -0.90234375 -0.65234375 0.44140625 1.62109375 0.76171875 2.58984375 -2.8046875 0.90625 -3.09375 -1.3515625 0.5625 1.84765625 1.8515625 2.03125 1.51171875 -3.25 -1.32421875 0.94140625 -1.0390625 2.3984375 -3.0546875 -3.3828125 3.109375 0.01171875 -1.3203125 -0.328125 -0.94140625 0.4296875 2.43359375 -1.5390625 -2.58203125 2.53515625 -2.578125 0.0703125 2.79296875 -1.49609375 +59 457.198364 +94 473.985107 +92 476.936432 +13 481.009399 +1 482.431854 +71 283.250702 +120 364.552856 +102 403.540588 +50 411.44928 +28 413.655273 +119 332.883972 +48 336.432617 +12 339.079712 +100 404.918793 +105 407.847778 diff --git a/tests/fixtures/persistence/v0.8.3/hnsw1_l2.index b/tests/fixtures/persistence/v0.8.3/hnsw1_l2.index new file mode 100644 index 0000000000000000000000000000000000000000..fae1c70583be512c3148ab2154aa8114030d342b GIT binary patch literal 15909 zcmb`N2~<_p+y6HRh=K|@p{AlJjws-W;yvdcM43ki5DA<`K*13a@K~mnWlo7hDyBKq zTREjA$~gxsHFHW!4NWvH%SH6fEvRLFLi`IrFl7G}hL4_o0+RNgv z7P5F}zb4itO5&%1lB#x)Mb}p~@lgX^9a~iwT_ zER@9N7j}!?#vb*xL`{DEj3hTZq>28om8wdCCVI}Z$mKf<9-Oiz;2UiOvMw7!zs zp|>QCZL_4kX4TbK+i3c%l~Tnwv9i~(VS2G=K5WUh=x@x`^yAw#H9S_+U-?~9<1T1o zL48R*wm?(28JfCtog~&|X!<@USq^QdtJ_XW>d?!YeyOLdwwx`g6W@~5Q?n%zW|4$t zcBy{#pd>zO)vLH+7rEHiPFB~1X!`Dzn(TH$lcVQo@|7Y@e6Y`=E(wz=qC4rLqRDPG zx~1gx%6L6(fty^>GFF%CotD(;(UKUSU#iD0l8TG4Z}DfnRHcR_uPc`H=sjBT3=du1 zJW7+ZPHGj-?y^4jj3yV&l0;A@%L?u`1QCXJ}cFPjywjGXriK% zr2jrc7oo1DV(AAK@ppwvEB?w}Pm8w7@~hC>W`iac z>7~Nq%U*hm^^#hpktE)4XVDk`DajLWOKHPAbp6=NlHTbHRH6X7NvT2mE_vhbN(DkAJOb&sa0MK7aveuexQuS_uE|lhy9<*2%&*>HsH%n5tRMExmy%urfk5bW4k;K$DB+;U*R5-Pg z#Dec7G3*^pucR)^(uI0V5+j#LA~Cd7eP5PEhi#HbyJAt}({<7Eo}}*gkww-TO{j~5 zgzl}0LqALEAO5oF?W~Ei8=AVlimduolhtd@b#-uCSuO9Pi$$w6am%GtxNa|1f3(UX z=Q@1PK->_Ih_#6EsXgL9_L7>`OjlQYEr}b4O68t0vigRLF65hg)b1;g<4c-aPVrh)!@_2WfrwDdhv~hvgo{6Q|rU;rMab|!5&SVnz={F?JR1C zyP7yZND@h#C2_L3Brg4-iOZM;c3ow)u%0Xi`b%O0>Sb$B)Eef=B-Bu-izLGBByseb zMeL|0iNKSR=x)`;>)SQqwQ7%e5&hsiToOG$k;E6UGtOU9GhJlif_~l~TPm(6%5q*H za@)`%My^5}5xY5vZ|9U!)#|8=QL{Di^;wGu@2aVjF*{lmScJcwEJ~Vc!ZK7+zi%b$ z5$$D>-J?|e^^!#_@|DD=U+ug=% zcjFBA3FrRN!7~~I`qUD10Q7-p@V;ySxXv}|X$`oiGoTKV($M{Qr_$OX?pbj!sQZ7= zFRn{8J}$Jk*2Eg`QQFoXP?ra23VZ?W->IpX9?`gACuRM`S+FXIg+ZfcDh@-9R6}ypIP{0rx!zxYw2!K0_T& z;HyP#{H1zdMkTc!GBmbp*RoOS7^6m>qm}3JIoiS6U~ZUO{!U>|Xm2E-zFd$8xW@}{ zojMqguN=bsEsGjv)$I^BwBp9MW!H218?%S~Wafjm@J!mydSo1!^Fe?yD**KXYm2{g zozHMDpP>$Ca5BE^!>&tbhotysyQnt`6}4xw@z?Kv{}Zb**fRkz$LWA)jRr}8d7Tat z9(?ET+{0(MpE|068TIyE>b0b(|Gf|T`-n4_G*PpgabeS^GIPl|*8$BzU!VZmX9bJ} z^|9_)S2;iiT;o3Ow+GDY-5UB9X~ptOQ)*1;sJ=F&?E2f0#@+dU$&3+&zOq+XqwEjr zwe=eHvHwGW3TA+Zg*sfok!CZtcVFlcH6km%V{XD4<=ktz#=CPot+uln+c?k;FfOTp zF=s9ClfT>IABewu0`BKN>R>z!f4}bnmU#5xgy7x}ZMx zEB#>X7@LNGGlPBC1@L@ZpHIc#w1+wvkAZ7b&im9~81j7UoS|ZG10!sGyis!W{!EO* zdV3HFnsP5-9)6l?U} z_N|$7y9fJhe$a;|pf#ZV>`R_M60koW73$!eiy0rebC%X8s=znaUe#W;x~`8iUhigS zWz8`@j1~KraiPsU0AtR&WlWwX)WMj)-Qhre%ZD#S^?%>Jx!1gnO8n=kM%c@Ln?1;$ z?G2be&el-CdDjdu7ObOQKnDDsd%1^uZLwlK9_!TRQnw`u*07d?#)_yzMqf*~F*Tw> zw$&?ZuLht`wSld#sh|EaZj2XupS_m@Y(hU+CsFUW`(xGe2AKof#}DME=;CyWW*e9$5_KFPX!=pkSwCmGY<9xT}Uyc~qAz{7R=5sk#ooI-Y zPNoj#i$1VdsgwEk0<8dZE&;|d1LT88g*sR#XVn7l_lpKu`v=DN(d%|8TW}%9s1bC- z%m<&L9rT6!>jUA zp3V3$_KZmy7zIXyrwMg1e=A$q4PT#4CxY$FcLwId0Z@kATE)uqX&Yn3 z{IaKMFJ}eg#hRks%(>0(Z2ZkS;5zqF2W`VWSlo2^ipb<%6P(m(_GPF1#~CkJU9Iek zmY@Y-9Wh?)bDldH^aafT?e6n{P<(QoKJjjhP}$7i(-v; zcK>ePAMB5@fOW;$!kDqnSi=_N7AJ&wAltMbWB7AtSO9 z-ny(7mAOPm8k_3Rel z;&df+YgeOb^ZhdGi9&x`f$kt2j0BYx>Szu&)w!MTH+bCg=yPMdH?PQ1JZJSbmiIU* zvj0KiPn~er9*|i} zZh-!k;bUoY+;A5z{>)Yf#PKmHCH{)CrE85n&kD zzcc&8cD}7o+F*ZMKpkB`SMW5U4$h<0ZUu3%ho=wx`Sj)MGv_J6N1ux^0{h*NS*vz{ zdKfp>2y2#el)5;xo+Z@b1CDi$oZ{c8MeTmKe_ZS385mr4ILu+h#`Vv|nF;P0>9q$Z|KzT^^p?jt;R>`9j` z`B`Qj=m+)G1l&)1|BdjO_Ml|Rl_7G^peoU)KbL#|J|;NvoityI%T4j3a9a4*-WgY!X~-Fo8uwo-D_!+Y0yy>Zz3_R98#+t^RdyY@M( z*8rS}%#S_r0-k{Lh4BgktRtS81|Amb=m2&(rvJ9I+B7@QJ41?lzw@3l_M`5`bmfH1 zJn&rRfjVe6bIm$oPT~OX)(kKfFc*&sb+86^)XB~3mvr@{=W%7h{Fm=2ao778$vwX@ z`@BEa*?&U->ya~welafW57ty)z~8x-aZLl<&pp)P2e!U6F-@fSCPc?f8<{JGDDlCq zjqX?8GyPz_vMy+gH(+cTKlY(*9x^waz2m^6LLKzEq<+&}&j{Z^t_c~<<^+7H1O~<% zjSDNx+0M8#PIiF07$-j02}mFuL;%j0#|ihc&NYY3F>apu2@a>nc&UdYl*Z`6mgg^; zIvlW0n`vV$&>f5bj1A9aJlG?gBM<+^J)?om9<6JBu6t^hbM(s5wR0;RlrX8Yv7zo> zb6zo4j1B9F{_q+4))jOEGN2zkKM`<7F>W^D{?6d`R`EzkJ#sX`sBZ*51^#03lY)ONk|lHI8gyWmM~7GuWkFHJwF zgSntBtYy}cGhk0J=j{7P5Dn-<7MK8RLLKz^pws9H<5R{BjFiXu$;0a@1+I!Qrt~ZG zu64mW&!!G1z}Qho1HhhVeqsRU6>E1Cm;`bGg*q71J9Toi{Tj5r-R8iLRnmq`RGha2 z8Q*??*1V%=cXQC`!CuxCV`TxA5$d2F1$BS+cONjWQB3Mj!Nq%TD!&zl7~YNV*Dm8k zKd6IwVXd?8Sp$q$Kfrf{@!)Yn9jr@_qPz)f#L0YQ9ABm9k)Jl(XYyZ-QmWJfQzP% z+E`~z1%pJu{@{FK4yFS7#+(q_4CWg5a-BM4aIf=Uy=vB#GuC-- zJ?;a{2WyD=XRa7G*3xj01?bOYkOwH#(Hk6{Ir_x*M| zvz>l40el}}@6jjrJ@xbjwp=sMwqD`fwFz}__N;#C&_p+-V>Q>)8UA(7yrJwWk&L;+ zkC=0}J=QtX*?X*W=AZgfKxKqF*iR)6moxm5Gwb?B3~A;$Xr5wUKioLf`mC9A))VIw z&t;8y0p^^xrGX(J9pr+?33brll^ae>oS!7sZeQN_MZeDR%Ax^rhL`1rIj?v&b6^jc z7uJ~_uQ>pYu!_)gLSu!h->13)53 z1sQ-n_^41v6j;4@boS<2$;nQO=grvu%A3lm$gW1G`~#*Rj6dg84?tfeKp)s|kLDLY z{eMCo(I9YETJEhB-#FLc82b~$9fKXo54EHzcE!}DMIb+fgFkXxm^UWS)j4C745ev4|xqNg$ z#I57)8n1O5?pq~z)#SFuya9b-?4RWWwwN<94xejUO_X!P(vt=! zU-0UD?uc@}Pj_R=Y{SesbHN^D%;+IZ7O|vdieFh?Z0W9 zJ;FPpPn!8&IM_XdLJ0Q1Ay#l9Q_(gELnrhq)~aPRXu-jyrs<`w#l zySvD>eAqVmCwJ@DNQ$QL*Q>SJ#(KEuJYggV%(AI%<`wI%G2go~eC zSsNPiiSqTnK%+xch54Sy*znzob~FBbrXyhAGhWok807-yybwGr+&>WXsNZ^sno^MJ z8e4N)nkZ4~Z3s2G=AJiaB6A)Km>2pZ1Nub&=_Bt$&XFexbugZHwqKjtz_H+t!`>?! zcb{9XoH*RdxKX^z%m?!t40-_iL7l8I&K35*?f#%21>jMkjwGlX!&Jhnl`x}B#z*?dHvEXS!9fQDYOEL@RyJR)$FsOE?wC012 zR{NS6e~;X1>S%>^_66&gey}$Jz{B?A>l1}KlEI!jqZQAlL4!i(jJk%b6uA6i=EDx_yqEa?;}4iWZ@~8%=7u#q5=;e8>JQe%GN+`ei79_4S4++CD(+BX z4UCtJ*YBM$X9?#M>zlnteXL{JM&DSIsepa*q}t-%=fUctvFQs^uO_&D5xpk1X^wL8 zW~i}#^BFTAoV%PM+{e7ICmMh#PzTWdrwMh?j^%Z8r)+85_nWbk#x55B^OmwK)!(Q( z@+RYs^I4eWG`ojm_a`$dsXS6}g1T{U>pYkiFA)vua;-URE+ z8Eb<*k_4!WdcpwnF#==(_Q%8X5MTePi*D;P`*=?GT@<o?V$((T-hxf0^#y?_T|=bLpG?(l04@|CEh6vG?;q`xz_x5)SGE z`a&OSg39#ZFzj2}a%#3;l6L&HvatcNqb2?%FG3t=Ew#YyK-8jh!C5We2SBeEQ1y!C3uUp$^8hyU*as?kT?Y zB9CP4$Svsi>m`>lXIF%dA=PZH{2k1nelHp1iXx<;<4 zZP%zpy{)^)^)WUtyld7jYt|9)9mpAU1I&*FR7R*{6nNJw?Sx;dZ@tDTKJT2cU!??k zb~Ii(c+k8Dse|!ij;V*auLgL3R7MYVaF)%TnLEjS#MQ8vBL#MOpVbc@9~5cCx4i$~ zMH^#X0z5Yju$DO2y#RZJGmvwt5Ijz(BONSo%rEeand)Bin~^h)?(d`|w@z?$TSB*1?^~*cl zRtdbvHvOOvj1%ju zHDJFyOQ?hIUL{T$Q@2NtORiP^-F10TbWiIn*(G?tbvZO3_ zViX2^VcxZj730tPWt~?C?SKNPgEKK7JSi{K!5-@3G^XFOsZ$p{Up~}c|K=mvA+D1# zWZc&>>n9K}FA0Em#=jNn7!T(DIXZWN&&`AuXNGOfo76FQuuqT?yh@W-ov1YZisyV)$b;;#Y5^!R(`Oyn$KzS(!|3D%ew zsLZUwHuJ9KY+mi~Nl&|!tiE-=9s5CIQ|I7qOMH!z-%4c8Th3XY-2`|6&X#{8^kWh@ zGOHk9{`7Wst_OY?`c6N;;Gq}ejBow#*RBuNV*vBc{%Z&rqsj_(Oa|Z0%>R9U@1NXV zllHD@lH(FQX;`@N%X=5i{$SpR1Nt-&utqpfDl62%+WWYEdcll>f`sVujI~GC)+rl# zey~yRS7$48%UE%S*8+T3NCwmu2C9N5)hw1wp$`7LpjoNMIeaQ}oi!>0 z?)z_qXV8vA>qf`>N8K%oEWbQ#|Heq=-UmI5#E7rVSaBY*cG*XD0CUbdW=<;8Bh+C<~dkEmHs?6EgW(su_fVmqg3S*;u zYex?B>E_k_u=4H)@y4skx6M9phIQtI{m!#{0`>>}sEklYA-FTMd)h+p3=h}SPQR}I z!6*2m9%06zmf!wUSDaP+cStqB{8JBoqW)(Ibxa2fJc5S14V>8Yi-W`bZ(O{jEcmvs z(csSg9<;+c<1+@h0N&vlpt3?8DyU!gX0{qQ_8X^LRlT?8o>ZnBYG$-Mu-5dW7uMNB zypw|f^|%8oVE&&a)G-6xYc*)NTkO=@k@26;*!X5s<=Tjz#^1xgmY?v`h#Dae-w9BT0v+8j=9-hD4&lzCz zgLct&>ZUEsVP)*#Ih?;dhx_R}pJn~q;_>J?Hd`N#1#M=0Y|qe!|3(;V2f*_lwukZH zZ#EwpYug_D`M(mIKRk~<(Fa>i&?oACIQIDZUy02(o=qJ*m+vHxip@9f`EP|jIRWaT z9$Rkc2Xp(Va6jXo1Z=hRINKh5?$PhIb68J|Cv`tg-=oj*yX{%p`f!XMx7W6xv0|P4 zU*zXeJ+z0jhqX+57@NvS>cQVTldjepV;#e1y*bj`CH!_u&;JKm+`*Or literal 0 HcmV?d00001 diff --git a/tests/fixtures/persistence/v0.8.3/hnsw5_ip.expected b/tests/fixtures/persistence/v0.8.3/hnsw5_ip.expected new file mode 100644 index 00000000..193d8d0d --- /dev/null +++ b/tests/fixtures/persistence/v0.8.3/hnsw5_ip.expected @@ -0,0 +1,17 @@ +130 65 3 5 +0.7734375 -1.265625 2.28125 -3.40234375 -1.609375 0.91796875 2.7109375 -0.20703125 0.31640625 0.62890625 1.296875 2.8203125 -1.46484375 -0.171875 -1.53515625 2.46875 3.79296875 -3.61328125 0.52734375 -3.21484375 2.94921875 2.4453125 0.7265625 -1.44921875 2.546875 -2.00390625 -2.8125 -1.609375 -2.1953125 -3.625 2.00390625 -1.5 -0.57421875 3.296875 -2.828125 -3.30078125 -3.03515625 3.25 -2.89453125 0.54296875 1.21875 -2.16015625 -3.34375 2.84765625 -2.24609375 -1.34765625 -0.1484375 -3.7265625 -3.87109375 -0.2890625 1.7421875 -3.08203125 0.66015625 -1.3359375 1.62109375 2.5 -2.8828125 1.203125 -2.82421875 2.0703125 1.05078125 -2.21875 -0.7578125 -1.1796875 1.86328125 -1.00390625 -2.02734375 -2.69140625 3.0703125 -0.796875 -0.78515625 -2.34375 3.60546875 -1.81640625 -1.3671875 -0.80078125 3.63671875 2.00390625 -3.828125 1.8125 1.34765625 0.5234375 0.8359375 3.71484375 1.19140625 0.54296875 -0.06640625 3.25390625 1.68359375 -1.59375 3.03515625 1.22265625 -3.2734375 0.05859375 -0.84765625 -0.96484375 1.046875 -2.58984375 -0.3828125 -1.765625 0.00390625 3.5 0.1484375 0.234375 -1.9609375 -0.375 -3.37109375 -0.40234375 1.34765625 -3.12109375 1 -1.16015625 1.83984375 2.46484375 1.25 -1.3125 -3.7734375 -0.9765625 -1.390625 -0.16015625 -1.6484375 -2.6015625 2.40234375 3.03515625 2.64453125 -1.49609375 -1.9765625 -2.3515625 -1.57421875 -0.18359375 1.37109375 1.4765625 2.46875 -0.75390625 -0.6171875 1.484375 2.1328125 -0.73046875 -1.640625 3.1875 0.6484375 2.80859375 2.79296875 -1.578125 -3.72265625 -3.0546875 -3.57421875 -3.1953125 -3.51171875 -0.9921875 1.171875 0.7734375 -3.08984375 -0.07421875 1.25390625 1.55078125 -2.6171875 -0.5703125 0.21484375 -0.90234375 -0.65234375 0.44140625 1.62109375 0.76171875 2.58984375 -2.8046875 0.90625 -3.09375 -1.3515625 0.5625 1.84765625 1.8515625 2.03125 1.51171875 -3.25 -1.32421875 0.94140625 -1.0390625 2.3984375 -3.0546875 -3.3828125 3.109375 0.01171875 -1.3203125 -0.328125 -0.94140625 0.4296875 2.43359375 -1.5390625 -2.58203125 2.53515625 -2.578125 0.0703125 2.79296875 -1.49609375 +128 -94.5418472 +46 -83.159584 +92 -80.4502106 +50 -75.6382751 +13 -72.0860596 +128 -75.1309814 +11 -73.6424179 +112 -73.1209106 +31 -65.0838242 +14 -64.2643051 +32 -69.4516983 +106 -66.4129791 +3 -62.6622734 +87 -57.6153526 +127 -55.5868149 diff --git a/tests/fixtures/persistence/v0.8.3/hnsw5_ip.index b/tests/fixtures/persistence/v0.8.3/hnsw5_ip.index new file mode 100644 index 0000000000000000000000000000000000000000..2e69c115654b336674711c70b4d645f0b3a41c32 GIT binary patch literal 23189 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 MD8jgF!A0x808NS*A^-pY literal 0 HcmV?d00001 diff --git a/tests/fixtures/persistence/v0.8.3/manifest.json b/tests/fixtures/persistence/v0.8.3/manifest.json index 103e47ff..15790eb6 100644 --- a/tests/fixtures/persistence/v0.8.3/manifest.json +++ b/tests/fixtures/persistence/v0.8.3/manifest.json @@ -1,11 +1,17 @@ { "source_tag": "v0.8.3", "source_commit": "dabfdb8c200a6f155df4edd0b9c825e6728b14a1", - "writer": "tests/fixtures/persistence/v0.8.3/generate.cpp", + "writer": "tests/fixtures/persistence/v0.8.3/generate.cpp (IVF-RaBitQ) and generate_graphs.cpp (HNSW-RaBitQ, SymphonyQG)", "producer": "Linux x86-64, g++ 14.3.0, Release, one thread, native optimization OFF", "byte_order": "little", "size_t_bytes": 8, "sha256": { + "v0.8.3/hnsw1_l2.expected": "00af920380168a49f0739b9f180a764c3fac3bbc3a7b31b6acbbf0ecd621f62a", + "v0.8.3/hnsw1_l2.index": "cb89a17e02c04233b599d9077d81b030586eba9bc32fe1ead9ea30271be0b7e1", + "v0.8.3/hnsw5_ip.expected": "199106ce08b2667df5b4cbc47bc06309564637e6790cf74448324c27e377b2f2", + "v0.8.3/hnsw5_ip.index": "5780fb6ad6843df24cfa0b9a545e5251505e50040e3ac73593e1f897d29b5967", + "v0.8.3/hnsw9_l2.expected": "ef0fa91553a6effcdbba13bf0fc5263fa75a3af5ac37acc948809ef051d93ab3", + "v0.8.3/hnsw9_l2.index": "116d2496789bfb392f0cb03aca6d0163985298c71aa0a8259f4e3092efc8a47f", "v0.8.3/ivf1_flat_matrix_l2.expected": "f1616ce0a6f59a57984e62eda1a16b92a54f603434e163ce5a2f33c336a0a252", "v0.8.3/ivf1_flat_matrix_l2.index": "c864c367f4eaac449984694402d42ba81706391c61405f3fcdcf233a7f4d92f4", "v0.8.3/ivf32_hnsw_ip.expected": "8280cf8c79e396c94ddb0a3bf77d69f45f7261b1ea71e90e5522395286376f89", @@ -17,6 +23,14 @@ "v0.8.3/ivf4_hnsw_l2.index": "88b5b7d1b7e5be99506421cd36ad6bb1785490446f55788d5b39a1069d5b304c", "v0.8.3/ivf4_hnsw_l2.index.hnsw": "1797edb3eb1fb16a20684d8873cbc2c0713bdf1623ff31f366e8f016f5e616cf", "v0.8.3/ivf7_flatrabitq_ip.expected": "c602bd98139b597532067094b001ff992d4498ea07ad7ddaf6762b8261f67d83", - "v0.8.3/ivf7_flatrabitq_ip.index": "54c11bcb813395eed099c8e8f65cdd96d97ca1ed684703e66abe27fd06887929" + "v0.8.3/ivf7_flatrabitq_ip.index": "54c11bcb813395eed099c8e8f65cdd96d97ca1ed684703e66abe27fd06887929", + "v0.8.3/qg0_ip.expected": "3ecb6604dce2108556e5e48130d4e9902d7ef7f0ef81aba39a3caa40b62230fe", + "v0.8.3/qg0_ip.index": "5b48546797a13825d27f0296b3c21c9c79fcf80ce44121fa5e9540358bb5762f", + "v0.8.3/qg4_l2.expected": "e973d22674ccde5c5afe4cf916e7c9271a79e8b6230c2da11bbc304218f488cf", + "v0.8.3/qg4_l2.index": "9f61d0570b75e67f101bc8e50e9af9db35194fa51187317d92a831db1a00d1e1", + "v0.8.3/qg4_matrix_l2.expected": "8f7d74551eb36226a89b195d23b69eac2d7194fbfe8f47b73caee8364757730e", + "v0.8.3/qg4_matrix_l2.index": "f60b01bb2e6b670eb63a0ed108346cc5c3cde0614b5178462b9a117b3c563484", + "v0.8.3/qg8_ip.expected": "46b4b3bd3e0b6ad1ec7e00df843e679977a7092fd3dcca78eb85a5239293cfe5", + "v0.8.3/qg8_ip.index": "5519fecdbde73515940de91d213ca4ab7ad3c486cd97a773fe02927c121ce879" } } diff --git a/tests/fixtures/persistence/v0.8.3/qg0_ip.expected b/tests/fixtures/persistence/v0.8.3/qg0_ip.expected new file mode 100644 index 00000000..750cfd52 --- /dev/null +++ b/tests/fixtures/persistence/v0.8.3/qg0_ip.expected @@ -0,0 +1,17 @@ +130 65 3 5 +0.7734375 -1.265625 2.28125 -3.40234375 -1.609375 0.91796875 2.7109375 -0.20703125 0.31640625 0.62890625 1.296875 2.8203125 -1.46484375 -0.171875 -1.53515625 2.46875 3.79296875 -3.61328125 0.52734375 -3.21484375 2.94921875 2.4453125 0.7265625 -1.44921875 2.546875 -2.00390625 -2.8125 -1.609375 -2.1953125 -3.625 2.00390625 -1.5 -0.57421875 3.296875 -2.828125 -3.30078125 -3.03515625 3.25 -2.89453125 0.54296875 1.21875 -2.16015625 -3.34375 2.84765625 -2.24609375 -1.34765625 -0.1484375 -3.7265625 -3.87109375 -0.2890625 1.7421875 -3.08203125 0.66015625 -1.3359375 1.62109375 2.5 -2.8828125 1.203125 -2.82421875 2.0703125 1.05078125 -2.21875 -0.7578125 -1.1796875 1.86328125 -1.00390625 -2.02734375 -2.69140625 3.0703125 -0.796875 -0.78515625 -2.34375 3.60546875 -1.81640625 -1.3671875 -0.80078125 3.63671875 2.00390625 -3.828125 1.8125 1.34765625 0.5234375 0.8359375 3.71484375 1.19140625 0.54296875 -0.06640625 3.25390625 1.68359375 -1.59375 3.03515625 1.22265625 -3.2734375 0.05859375 -0.84765625 -0.96484375 1.046875 -2.58984375 -0.3828125 -1.765625 0.00390625 3.5 0.1484375 0.234375 -1.9609375 -0.375 -3.37109375 -0.40234375 1.34765625 -3.12109375 1 -1.16015625 1.83984375 2.46484375 1.25 -1.3125 -3.7734375 -0.9765625 -1.390625 -0.16015625 -1.6484375 -2.6015625 2.40234375 3.03515625 2.64453125 -1.49609375 -1.9765625 -2.3515625 -1.57421875 -0.18359375 1.37109375 1.4765625 2.46875 -0.75390625 -0.6171875 1.484375 2.1328125 -0.73046875 -1.640625 3.1875 0.6484375 2.80859375 2.79296875 -1.578125 -3.72265625 -3.0546875 -3.57421875 -3.1953125 -3.51171875 -0.9921875 1.171875 0.7734375 -3.08984375 -0.07421875 1.25390625 1.55078125 -2.6171875 -0.5703125 0.21484375 -0.90234375 -0.65234375 0.44140625 1.62109375 0.76171875 2.58984375 -2.8046875 0.90625 -3.09375 -1.3515625 0.5625 1.84765625 1.8515625 2.03125 1.51171875 -3.25 -1.32421875 0.94140625 -1.0390625 2.3984375 -3.0546875 -3.3828125 3.109375 0.01171875 -1.3203125 -0.328125 -0.94140625 0.4296875 2.43359375 -1.5390625 -2.58203125 2.53515625 -2.578125 0.0703125 2.79296875 -1.49609375 +128 -94.8744507 +92 -82.2540436 +46 -80.8800812 +13 -71.9051971 +50 -71.6611023 +112 -73.9705811 +128 -73.9433899 +11 -71.4328156 +14 -64.1144104 +31 -59.65448 +32 -72.5804443 +106 -66.686676 +3 -59.7108917 +87 -57.427536 +127 -53.9758759 diff --git a/tests/fixtures/persistence/v0.8.3/qg0_ip.index b/tests/fixtures/persistence/v0.8.3/qg0_ip.index new file mode 100644 index 0000000000000000000000000000000000000000..c25390c8fec38efe018e078d81138803b19a3e7d GIT binary patch literal 133726 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
8Jv62H_L&zB$q^-C_j6X~YG&I8 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 literal 0 HcmV?d00001 diff --git a/tests/unit/rabitqlib/index/historical_persistence_test.cpp b/tests/unit/rabitqlib/index/historical_persistence_test.cpp index d1a45ede..434e44f4 100644 --- a/tests/unit/rabitqlib/index/historical_persistence_test.cpp +++ b/tests/unit/rabitqlib/index/historical_persistence_test.cpp @@ -189,7 +189,14 @@ INSTANTIATE_TEST_SUITE_P( "v0.8.3", "ivf4_hnsw_l2", 96, 130, 4, ivf::InitializerType::HNSW}, HistoricalFixtureCase{ "v0.8.3", "ivf32_hnsw_ip", 96, 130, 4, ivf::InitializerType::HNSW}, - HistoricalFixtureCase{"v0.8.3", "ivf1_flat_matrix_l2", 96, 130, 4} + HistoricalFixtureCase{"v0.8.3", "ivf1_flat_matrix_l2", 96, 130, 4}, + HistoricalFixtureCase{"v0.8.3", "hnsw1_l2", 96, 130}, + HistoricalFixtureCase{"v0.8.3", "hnsw5_ip", 96, 130}, + HistoricalFixtureCase{"v0.8.3", "hnsw9_l2", 96, 130}, + HistoricalFixtureCase{"v0.8.3", "qg0_ip", 96, 130}, + HistoricalFixtureCase{"v0.8.3", "qg4_l2", 96, 130}, + HistoricalFixtureCase{"v0.8.3", "qg8_ip", 96, 130}, + HistoricalFixtureCase{"v0.8.3", "qg4_matrix_l2", 96, 130} ), [](const auto& info) { return info.param.name; } ); From c0414e45282bad10ca411927c1c79f4521674ada Mon Sep 17 00:00:00 2001 From: gouyt13clear Date: Sat, 7 Nov 2026 18:22:25 +0800 Subject: [PATCH 07/11] feat(index): save and load HNSW-RaBitQ and SymphonyQG in index format 2 HNSW-RaBitQ (sections 512-516) and SymphonyQG (sections 768-770) now save index format 2, like IVF-RaBitQ. Loading dispatches on the magic and keeps reading their earlier layouts; HNSW-RaBitQ shares its graph checks between both readers. - Every index records its construction seed, and SymphonyQG also its ef_construction and initialization, so Python keeps them after load. New C++ accessors report them: IVF::seed(), HierarchicalNSW::seed(), QuantizedGraph::seed(), ef_construction() and initialization(). QGInitialization moves to qg.hpp. - Unknown read-only-compatible features make every index read-only: HNSW-RaBitQ refuses add, resize, remove and save; SymphonyQG refuses save until a builder rebuilds it. - A failed HNSW-RaBitQ write reports "HNSW index file cannot be written". - Tests that patched freshly saved files at old offsets now damage format-2 fields, and the earlier layouts' checks run on copies of the v0.8.3 fixtures. - Document the HNSW-RaBitQ and SymphonyQG reader rules and the seed of each index in the specification, and format the fixture generator. --- docs/development.md | 11 +- docs/docs/index/hnsw.md | 4 + docs/docs/index/qg.md | 4 + docs/docs/index/usage.md | 4 +- docs/docs/releases/unreleased.md | 30 +- docs/formats.md | 24 +- include/rabitqlib/index/hnsw/hnsw.hpp | 22 +- include/rabitqlib/index/ivf/ivf.hpp | 6 +- include/rabitqlib/index/symqg/qg.hpp | 25 +- include/rabitqlib/index/symqg/qg_builder.hpp | 2 - python_bindings/__init__.pyi | 16 +- python_bindings/hnsw_bindings.cpp | 16 +- python_bindings/ivf_bindings.cpp | 10 +- python_bindings/symqg_bindings.cpp | 20 +- src/index/format2.cpp | 28 - src/index/format2.hpp | 29 + src/index/hnsw.cpp | 676 ++++++++++++------ src/index/ivf.cpp | 46 +- src/index/qg.cpp | 250 ++++++- src/index/qg_builder.cpp | 5 + tests/common/format2_file.hpp | 4 +- tests/fixtures/persistence/README.md | 5 +- .../persistence/v0.8.3/generate_graphs.cpp | 16 +- tests/python/format2_file.py | 4 + tests/python/test_historical_persistence.py | 32 + tests/python/test_hnsw.py | 17 +- tests/python/test_portable_indexes.py | 31 +- tests/python/test_symqg.py | 19 +- tests/python/test_symqg_api.py | 4 +- tests/python/test_train_api.py | 36 +- .../index/historical_persistence_test.cpp | 89 ++- tests/unit/rabitqlib/index/hnsw_test.cpp | 188 ++++- tests/unit/rabitqlib/index/ivf_test.cpp | 6 +- tests/unit/rabitqlib/index/qg_test.cpp | 250 ++++++- 34 files changed, 1474 insertions(+), 455 deletions(-) diff --git a/docs/development.md b/docs/development.md index d8c43aef..b7276203 100644 --- a/docs/development.md +++ b/docs/development.md @@ -553,14 +553,13 @@ that corrupt or mismatched files are reported before allocation, without treatin crafted file as a supported input. Do not silently reinterpret an old file. Add a magic/version discriminator, use fixed-width -serialized fields for new formats (the HNSW and QG bodies still store several `size_t` fields; -change those only with a format version bump), validate sizes before allocation, check every +serialized fields for new formats (earlier layouts store several `size_t` fields; keep their readers unchanged), validate sizes before allocation, check every read, and retain a compatibility test fixture or an explicit rejection path. Review each affected format; changes to shared serialization helpers require auditing every format using them. -IVF-RaBitQ writes index format 2 ([`docs/formats.md`](formats.md#index-format-2), container in -`src/index/format2.*`) and still reads its earlier layouts. HNSW-RaBitQ and SymphonyQG have -separate formats. SymphonyQG includes a versioned quantized format plus a legacy raw-format -fallback; preserve both unless a breaking change is explicitly requested. When a writer moves to +Every index writes index format 2 ([`docs/formats.md`](formats.md#index-format-2), container in +`src/index/format2.*`) and still reads its earlier layouts: IVF-RaBitQ's `RABQIDX1` versions and +unversioned files, HNSW-RaBitQ's unversioned layout, and SymphonyQG's versioned quantized layout +and raw fallback. Preserve those readers unless a breaking change is explicitly requested. When a writer moves to a new format, freeze the last files of the old writer as release-produced fixtures first. ### Change Python bindings diff --git a/docs/docs/index/hnsw.md b/docs/docs/index/hnsw.md index a846c58d..583e2e52 100644 --- a/docs/docs/index/hnsw.md +++ b/docs/docs/index/hnsw.md @@ -316,6 +316,10 @@ through 0.5.1 check every cluster ID on `load` and reject the file as invalid instead of returning removed points. Releases before 0.3.7 do not check cluster IDs, so do not open such a file with them. +`save` writes [index format 2](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/docs/formats.md#index-format-2), which also records the construction seed, so +`seed` survives a reload. Releases up to 0.8 cannot read these files; every earlier +file still loads, without a recorded seed, and saving it writes format 2. + New HNSW-RaBitQ indexes pad dimensions to multiples of 32 for all supported bit widths. Loading preserves the saved padded dimension and rotation, so older indexes using multiples of 64 remain readable. Older library versions cannot load new diff --git a/docs/docs/index/qg.md b/docs/docs/index/qg.md index cbab008b..790b5643 100644 --- a/docs/docs/index/qg.md +++ b/docs/docs/index/qg.md @@ -209,6 +209,10 @@ Python counterparts for complete examples. ## Persistence +`save` writes [index format 2](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/docs/formats.md#index-format-2), which also records the seed, `ef_construction` +and `init`, so they survive a reload. Releases up to 0.8 cannot read these files; every +earlier file still loads, without these settings, and saving it writes format 2. + All three modes pad the rotated domain to a multiple of 32. Loading an older index preserves its saved padded dimension, including historical multiples of 64. Older library versions that enforce 64-dimension padding cannot load new files diff --git a/docs/docs/index/usage.md b/docs/docs/index/usage.md index 22023ef2..a06f3328 100644 --- a/docs/docs/index/usage.md +++ b/docs/docs/index/usage.md @@ -170,9 +170,9 @@ routing graph. The compatibility suite loads frozen files written by v0.5.2 and v0.8.3, compares search IDs and distances, and verifies save/load migration and rejection of truncated -or unsupported versions. IVF-RaBitQ saves in +or unsupported versions. Every index saves in [index format 2](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/docs/formats.md#index-format-2), -which releases up to 0.8 cannot read. See the +which releases up to 0.8 cannot read, and which records the construction seed. See the [fixture provenance and coverage](https://github.com/VectorDB-NTU/RaBitQ-Library/tree/main/tests/fixtures/persistence). Compatibility is format-specific; preserving old readers' ability to open new files is not guaranteed. Consult each index's format documentation before diff --git a/docs/docs/releases/unreleased.md b/docs/docs/releases/unreleased.md index 8fe6d237..55721e0a 100644 --- a/docs/docs/releases/unreleased.md +++ b/docs/docs/releases/unreleased.md @@ -2,16 +2,32 @@ ## Index files -- IVF-RaBitQ `save` writes [index format 2](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/docs/formats.md#index-format-2), a single file with a header, a section table and checksummed metadata. HNSW centroid routing is stored inside it, so `save` no longer writes a `.hnsw` sidecar. A sidecar left by an earlier save is not removed. +- IVF-RaBitQ, HNSW-RaBitQ and SymphonyQG `save` write [index format 2](https://github.com/VectorDB-NTU/RaBitQ-Library/blob/main/docs/formats.md#index-format-2), a single file with a header, a section table and checksummed metadata. - Releases up to 0.8 cannot read these files. Keep a copy saved by 0.8 if you may downgrade. -- The saved bytes now include the HNSW routing graph, which is reproducible only when `train`, `set_centroids` or the first `add` builds it with `num_threads=1`; saving the same index twice still gives identical files. -- Files written by every earlier release still load, including the `.hnsw` sidecars of HNSW-routed files; saving one writes format 2 with the same rotation, codes and results. -- Loading a damaged or mismatched format-2 file raises `RuntimeError` with a message naming the problem, for example `IVF index file is 6916 bytes, but its sections end at 6852` for trailing bytes. +- Files written by every earlier release still load, including the `.hnsw` sidecars of HNSW-routed IVF files. Saving one writes format 2 with the same rotation, codes and results. +- IVF-RaBitQ stores HNSW centroid routing inside the file, so `save` no longer writes a `.hnsw` sidecar. A sidecar left by an earlier save is not removed. +- The saved bytes now include that routing graph. It is reproducible only when `train`, `set_centroids` or the first `add` builds it with `num_threads=1`. Saving the same index twice still gives identical files. - `save` rejects IVF-RaBitQ indexes of more than 65,536 dimensions. -- Errors in an HNSW routing graph name the graph rather than the sidecar: `Truncated HNSW centroid graph` and `HNSW centroid graph changed while loading` replace their `sidecar` forms. A missing sidecar of an earlier file still raises `Cannot open HNSW centroid sidecar`. -- A format-2 file that uses read-only-compatible features unknown to this release loads for search only: `add`, `reserve`, `remove` and `save` raise until `construct` rebuilds the index. +- A format-2 file that uses read-only-compatible features unknown to this release loads for search only: operations that change or save the index raise. Rebuilding with IVF-RaBitQ `construct`, or a new SymphonyQG build, makes it writable again. + +## Python changes + +- Loading a format-2 file keeps the `seed` of every index, and `ef_construction` and `init` of SymphonyQG. They were `None` after `load`, and still are for files from releases up to 0.8. +- With a recorded seed, `cp.seed` after `load` follows it as for a new index: the seed for IVF-RaBitQ, its low 32 bits for HNSW-RaBitQ. It was the k-means default. +- Messages for damaged or mismatched files change: + - A format-2 file raises `RuntimeError` naming the problem, for example `IVF index file is 6916 bytes, but its sections end at 6852` for trailing bytes. + - Errors in an HNSW routing graph name the graph rather than the sidecar: `Truncated HNSW centroid graph` and `HNSW centroid graph changed while loading` replace their `sidecar` forms. + - A missing sidecar of an earlier file still raises `Cannot open HNSW centroid sidecar`. + - A failed HNSW-RaBitQ write raises `HNSW index file cannot be written` instead of `HNSW failed to write index file`. ## C++ changes -- `ivf::Initializer` and its subclasses replace `save(std::ofstream&, const char*)` and `load(std::ifstream&, const char*)` with `write(std::ostream&)` and `read(std::istream&, uint64_t bytes)`. The HNSW initializer no longer writes a sidecar itself. +- `ivf::Initializer` and its subclasses: + - `save(std::ofstream&, const char*)` becomes `write(std::ostream&)`, and `load(std::ifstream&, const char*)` becomes `read(std::istream&, uint64_t bytes)`. + - The HNSW initializer no longer writes a sidecar itself. +- New accessors report what format-2 files record, or no value when unknown: + - `IVF::seed()`; + - `HierarchicalNSW::seed()`; + - `QuantizedGraph::seed()`, `ef_construction()` and `initialization()`. +- `symqg::QGInitialization` moves from `qg_builder.hpp` to `qg.hpp`, which `qg_builder.hpp` includes. - The vendored hnswlib adds `saveIndex(std::ostream&)` and `loadIndex(std::istream&, size_t bytes, ...)`; the path overloads are unchanged. diff --git a/docs/formats.md b/docs/formats.md index b7f97059..cb5a57fb 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -97,7 +97,7 @@ The body defines the rotation; `seed` only records where it came from. A seeded ## Index files, format 2 (draft) -> **Status: draft for 0.9 (#211, #212).** The container (`src/index/format2.*`) is implemented, and IVF-RaBitQ reads and writes format 2; HNSW-RaBitQ and SymphonyQG do not yet, and no index stores the cosine metric or a seed yet. Field numbers, section kinds and the open questions at the end may still change until a release writes format 2. +> **Status: draft for 0.9 (#211, #212).** The container (`src/index/format2.*`) is implemented, and IVF-RaBitQ, HNSW-RaBitQ and SymphonyQG read and write format 2; no index stores the cosine metric yet, and IVF-SSD and companion files are not implemented. Field numbers, section kinds and the open questions at the end may still change until a release writes format 2. Format 2 is one container for IVF-RaBitQ, HNSW-RaBitQ and SymphonyQG, and later for the SSD-resident IVF index from RaBitQ-SSD. It replaces the per-index layouts: IVF-RaBitQ `RABQIDX1` versions 1 and 2, the unversioned HNSW-RaBitQ layout, SymphonyQG's quantized version 1 and its raw layout, and the unversioned hnswlib sidecar `.hnsw` of IVF-RaBitQ. Every released layout stays readable (see [Earlier layouts](#earlier-layouts)); `save` always writes format 2. @@ -144,7 +144,7 @@ Every section starts at or after `header_bytes`. Non-empty sections do not overl | 68 | 4 | u32 | `raw_type` | Element type of stored raw vectors: 0 = none, 1 = float32, 2 = uint8 | | 72 | 4 | u32 | `id_bits` | Width of the point IDs stored in the file: 32 or 64 | | 76 | 4 | u32 | `flags` | Bit 0, `seed_known`: `seed` is valid | -| 80 | 8 | u64 | `seed` | The index's construction seed (graph levels, graph construction, training sample). Zero when `seed_known` is clear. The rotator records its own seed | +| 80 | 8 | u64 | `seed` | The index's construction seed. Zero when `seed_known` is clear, which a file migrated from an earlier layout has. IVF-RaBitQ: the rotation and training seed, at most 2^32 − 1. HNSW-RaBitQ: the graph-level and training seed, whose low 32 bits seeded the rotation. SymphonyQG: the rotation seed, at most 2^32 − 1, which Python also uses for graph construction. The rotator blob records the rotation seed again | | 88 | 8 | u64 | `num_points` | Stored points, including removed ones | | 96 | 8 | u64 | `section_table_offset` | A multiple of 8, at least `header_bytes` | | 104 | 4 | u32 | `section_count` | At least 1 | @@ -164,7 +164,7 @@ The Python `nbits` maps to these fields as follows. IVF-RaBitQ `nbits` 1–9 is | 24 | 4 | u32 | `crc32` | Over the section's bytes when `checksummed` is set; zero otherwise | | 28 | 4 | — | reserved | | -Kind 0 is reserved, and a kind appears at most once. Section flags other than the two above are rejected; a later version cannot add a section flag without a new `version`. Writers checksum the header-like sections (parameters, cluster sizes, companions), whose values size later allocations, and leave bulk data unchecksummed, so that loading a large index does not read it twice. +Kind 0 is reserved, and a kind appears at most once. Section flags other than the two above are rejected; a later version cannot add a section flag without a new `version`. Writers checksum the header-like sections (parameters, cluster sizes, companions), whose values size later allocations, and leave bulk data unchecksummed, so that loading a large index does not read it twice. Per-point arrays such as `hnsw_levels` count as bulk data: the header fixes their size, and readers check every value against its limit. A section the reader does not know is handled by its `required` bit: a required one rejects the file, and an optional one is skipped without reading it, so its checksum is not verified. A writer marks a section optional only if dropping it leaves a correct index, for example a cache that can be recomputed. `save` drops optional sections it does not know. @@ -175,7 +175,7 @@ The three masks follow the ext4 superblock: they say what an older reader may do | Mask | A reader that does not know a set bit | | --- | --- | | `incompat_features` | Rejects the file; reading it would give wrong results | -| `ro_compat_features` | May load and search; operations that change or save the index (`add`, `reserve`, `remove`, `save`) raise, because they would leave the unknown data stale. Rebuilding the index with `construct` replaces that data, so the rebuilt index is writable | +| `ro_compat_features` | May load and search; operations that change or save the index (`add`, `reserve`, `remove`, `save`) raise, because they would leave the unknown data stale. Rebuilding replaces that data and makes the index writable again: IVF-RaBitQ `construct`, or a new SymphonyQG build; HNSW-RaBitQ cannot be rebuilt in place | | `compat_features` | Ignores the bit; `save` clears it | The error names the unknown bits and `writer_version`, such as "index file uses incompatible features 0x4 that this release does not know; written by rabitqlib 0.10.0". @@ -219,7 +219,7 @@ Kinds 1–255 are shared, 256–511 are IVF-RaBitQ, 512–767 HNSW-RaBitQ, 768 | 514 | `hnsw_records` | HNSW | `num_points` [base-layer records](#hnsw-rabitq-records) | | 515 | `hnsw_levels` | HNSW | `num_points` × u32 levels | | 516 | `hnsw_upper_links` | HNSW | The upper-layer link lists of points with level > 0 in point order; each is `level × upper_links_bytes` | -| 768 | `qg_params` | SymphonyQG | `max_degree` u64; `entry_point` u64; `ef_construction` u64 (0 = unknown); `init` u32 (0 = unknown); reserved u32 | +| 768 | `qg_params` | SymphonyQG | `max_degree` u64; `entry_point` u64; `ef_construction` u64 (0 = unknown); `init` u32 (0 = unknown, 1 = PiPNN, 2 = random); reserved u32 | | 769 | `qg_centroid` | SymphonyQG, `code_bits > 0` | `padded_dim` f32 | | 770 | `qg_records` | SymphonyQG | `num_points` [records](#symphonyqg-records) | @@ -298,7 +298,7 @@ Every vertex has `max_degree` neighbor slots, a multiple of 32. Let `V` be the s | `V` | `(max_degree / 32) * (padded_dim * 4 + 256)` | The neighbors' 1-bit codes, quantized against this vertex, as [FastScan blocks](#fastscan-blocks-version-1) without the `error` factor: sign codes, then 32 `add` and 32 `rescale` values | | `V + (max_degree / 32) * (padded_dim * 4 + 256)` | `4 * max_degree` | Neighbor IDs | -Unused lanes of a partial neighbor block have zero factors, and their IDs are unspecified. +Unused lanes of a partial neighbor block have zero factors. Their IDs are left over from construction, but like every ID slot they are below `num_points`, which readers check. ### IVF-RaBitQ files @@ -308,10 +308,19 @@ Besides the rules above, an IVF-RaBitQ reader requires: - `code_bits` from 1 to 9, with `raw_type = float32` only when `code_bits = 1`, and `raw_type` none or float32; - `id_bits = 32`; - in `ivf_params`, `num_clusters` from 1 to 2^31, `initializer` 1 to 3, and the reserved field zero; -- cluster sizes that add up to `num_points`. +- cluster sizes that add up to `num_points`; +- a seed, when known, of at most 2^32 − 1. This release also rejects the cosine metric, which no IVF-RaBitQ file stores yet. +### HNSW-RaBitQ files + +An HNSW-RaBitQ reader requires `num_points` from 1 to 2^31, `code_bits` from 1 to 9, `raw_type` none, `id_bits = 32`, `dim` at least 64, and an FHT/Kac rotator. In `hnsw_params`: `max_elements` from `num_points` to 2^31; `num_clusters` from 1 to 2^31; `M` from 2 to 10,000, with `upper_max_degree = M` and `base_max_degree = 2 * M`; `ef_construction` at least `M`; a finite positive `level_multiplier`; `max_level` at least 0; `entry_point` below `num_points`; and the reserved field zero. Every level in `hnsw_levels` is at most `max_level`, and the entry point's level equals it. Every record's cluster ID (without the removal mark) is below `num_clusters`, its labels are distinct, its base-layer neighbor count is at most `base_max_degree` and each upper-layer count at most `M`, and every neighbor ID is below `num_points` with at least the level of the list that holds it. This release rejects the cosine metric. + +### SymphonyQG files + +A SymphonyQG reader requires `id_bits = 32` and either raw vectors (`code_bits = 0`, `raw_type = float32`) or `code_bits` 4 or 8 with `raw_type` none; a seed, when known, of at most 2^32 − 1; `num_points` up to 2^31; in `qg_params`, a `max_degree` that is a positive multiple of 32 and below `num_points`, `entry_point` below `num_points`, `init` 0 to 2 and the reserved field zero; `qg_centroid` exactly when the codes are quantized; and every neighbor ID below `num_points`. This release rejects the cosine metric. + ### Companion files Only IVF-SSD uses them. `.ssd` holds the page-packed inverted lists, read with `O_DIRECT` and too large to keep in the main file. The `companions` section lists each one: @@ -366,4 +375,3 @@ Readers dispatch on the leading bytes: `"RABQINDX"`, then each index's legacy ma - Python error type for a rejected file: index loads raise `RuntimeError` today, `rabitqlib.quant` raises `FormatError` (a `ValueError`). Proposed: one top-level `rabitqlib.FormatError` that subclasses both `ValueError` and `RuntimeError`, raised by `load` and by `rabitqlib.quant`, so existing `except RuntimeError` handlers keep working. - Whether the header should be a [blob](#blobs) kind instead of its own magic. Proposed: its own magic, as above. A blob checksums its whole body, which is too costly for large index files, so index files get their own container and nest blobs in sections. -- Whether `seed` should keep the full u64 that HNSW-RaBitQ accepts, while the rotator blob records a u32 seed. Proposed: u64 in the header; the rotator blob's seed stays informational and has `seed_known` clear when the seed does not fit in u32. diff --git a/include/rabitqlib/index/hnsw/hnsw.hpp b/include/rabitqlib/index/hnsw/hnsw.hpp index 001ffc65..1a3cbdd2 100644 --- a/include/rabitqlib/index/hnsw/hnsw.hpp +++ b/include/rabitqlib/index/hnsw/hnsw.hpp @@ -7,9 +7,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -70,6 +72,9 @@ class HierarchicalNSW { [[nodiscard]] size_t M() const { return M_; } [[nodiscard]] size_t ef_construction() const { return ef_construction_; } [[nodiscard]] MetricType metric_type() const { return metric_type_; } + // The construction seed, or no value for an index loaded from a file that does not + // record it (files from releases up to 0.8). + [[nodiscard]] std::optional seed() const { return seed_; } // Logical capacity, nonremoved points, and stored records (including removals). [[nodiscard]] size_t max_elements() const { return max_elements_; } [[nodiscard]] size_t num_points() const { @@ -284,6 +289,10 @@ class HierarchicalNSW { std::unordered_map label_lookup_; std::default_random_engine level_generator_; + std::optional seed_; // the construction seed, when known + // Loaded from a file with read-only-compatible features this release does not know: + // search works, but add, resize, remove and save refuse the index. + bool read_only_ = false; std::unique_ptr visited_set_pool_{nullptr}; @@ -381,9 +390,20 @@ class HierarchicalNSW { int random_level(double multiplier); - // Write the index file at exactly this path. + // Write the index file in format 2 at exactly this path. void write_file(const char* filename) const; + // Read a file into this freshly created index: format 2, or the earlier layout. + void read_format2(std::istream& input); + void read_legacy(std::ifstream& input); + + // Check the loaded records and link lists, and rebuild what they imply: the label + // lookup and the removed-point count. + void validate_graph(); + + // Refuse an operation that would modify or save a read-only index. + void check_writable(const char* operation) const; + // A link list is a count header followed by neighbor IDs. [[nodiscard]] PID* upper_layer_links(PID internal_id, int level) { return reinterpret_cast( diff --git a/include/rabitqlib/index/ivf/ivf.hpp b/include/rabitqlib/index/ivf/ivf.hpp index ec75e19b..05829655 100644 --- a/include/rabitqlib/index/ivf/ivf.hpp +++ b/include/rabitqlib/index/ivf/ivf.hpp @@ -94,7 +94,8 @@ class IVF { // Loaded from a file with read-only-compatible features this release does not know: // search works, but add, reserve, remove and save refuse the index. bool read_only_ = false; - size_t ex_bits_ = 0; // total bits = ex_bits_ + 1; 0 with raw reranking + std::optional seed_; // the construction seed, when known + size_t ex_bits_ = 0; // total bits = ex_bits_ + 1; 0 with raw reranking RotatorType rotator_type_ = RotatorType::FhtKacRotator; std::unique_ptr> rotator_; std::vector clusters_; @@ -264,6 +265,9 @@ class IVF { [[nodiscard]] size_t dimension() const { return dim_; } [[nodiscard]] size_t nbits() const { return raw_reranking_ ? 32 : ex_bits_ + 1; } [[nodiscard]] MetricType metric_type() const { return metric_type_; } + // The rotation seed of the seeded constructor, or no value when it is unknown: the + // point-count constructor draws one, and files from releases up to 0.8 omit it. + [[nodiscard]] std::optional seed() const { return seed_; } [[nodiscard]] RotatorType rotator_type() const { return rotator_type_; } [[nodiscard]] InitializerType initializer_type() const { return initializer_type_; } // Centroids are set, by train, set_centroids, construct or load. diff --git a/include/rabitqlib/index/symqg/qg.hpp b/include/rabitqlib/index/symqg/qg.hpp index 1e82f6d0..b72b35e2 100644 --- a/include/rabitqlib/index/symqg/qg.hpp +++ b/include/rabitqlib/index/symqg/qg.hpp @@ -2,7 +2,9 @@ #include #include +#include #include +#include #include #include @@ -26,6 +28,9 @@ class SearchBuffer; namespace rabitqlib::symqg { +// How a builder initializes the graph before refinement. +enum class QGInitialization { PiPNN, Random }; + class QuantizedQuery { private: const float* rotated_query_; @@ -114,6 +119,13 @@ class QuantizedGraph { size_t default_ef_search_ = 0; bool ready_ = false; uint32_t seed_ = 42; + bool seed_known_ = false; // false for files that do not record the seed + // Construction settings a builder records; unknown (0 and no value) otherwise. + size_t ef_construction_ = 0; + std::optional initialization_; + // Loaded from a file with read-only-compatible features this release does not know: + // search works, but save refuses the graph until a builder rebuilds it. + bool read_only_ = false; [[nodiscard]] static size_t checked_add(size_t lhs, size_t rhs); @@ -127,9 +139,12 @@ class QuantizedGraph { void initialize(bool randomize_rotation = true); - // Write the graph file at exactly this path. + // Write the graph file in index format 2 at exactly this path. void write_file(const char* filename) const; + // Load a format-2 file into this freshly created graph. + void read_format2(std::istream& input); + void copy_vectors(const float* data, size_t num_workers); void set_quantization_centroid(const float* centroid); @@ -279,6 +294,14 @@ class QuantizedGraph { [[nodiscard]] size_t quantization_bits() const; + // The rotation seed, or no value for a graph loaded from a file that does not + // record it (files from releases up to 0.8). + [[nodiscard]] std::optional seed() const; + + // The settings the builder used, or no value when they are unknown. + [[nodiscard]] std::optional ef_construction() const; + [[nodiscard]] std::optional initialization() const; + [[nodiscard]] bool is_quantized() const; void set_ep(PID entry); diff --git a/include/rabitqlib/index/symqg/qg_builder.hpp b/include/rabitqlib/index/symqg/qg_builder.hpp index e2bf47e7..70e5f4b3 100644 --- a/include/rabitqlib/index/symqg/qg_builder.hpp +++ b/include/rabitqlib/index/symqg/qg_builder.hpp @@ -14,8 +14,6 @@ namespace rabitqlib::symqg { inline constexpr size_t kMaxBsIter = 5; // max iter for binary search of pruning bar using CandidateList = std::vector>; -enum class QGInitialization { PiPNN, Random }; - // Owns temporary state for SymphonyQG initialization and refinement. class QGBuilder { friend struct QGConstructionTestAccess; diff --git a/python_bindings/__init__.pyi b/python_bindings/__init__.pyi index 3cdffe8b..3f44a278 100644 --- a/python_bindings/__init__.pyi +++ b/python_bindings/__init__.pyi @@ -208,7 +208,7 @@ class IvfIndex: @property def seed(self) -> int | None: - """Rotation seed, or None for an index loaded from disk or created in the 0.7 form.""" + """Rotation seed, or None for an index loaded from a file from a release up to 0.8 or created in the 0.7 form.""" @property def cp(self) -> RaBitQKMeansParameters: @@ -641,7 +641,7 @@ class HnswIndex: @property def seed(self) -> int | None: - """Graph and rotation seed, or None for an index loaded from disk.""" + """Graph and rotation seed, or None for an index loaded from a file from a release up to 0.8.""" @property def cp(self) -> RaBitQKMeansParameters: @@ -872,7 +872,8 @@ class HnswIndex: Notes ----- A trained index without vectors raises RuntimeError; add vectors first. - The saved state includes the rotation used to build the index. + The saved state includes the rotation used to build the index and its seed; + releases up to 0.8 cannot read the file. """ @staticmethod @@ -1068,11 +1069,11 @@ class SymqgIndex: @property def ef_construction(self) -> int | None: - """Construction search breadth of the current graph or the next add, or None for an index loaded from disk.""" + """Construction search breadth of the current graph or the next add, or None for an index loaded from a file from a release up to 0.8.""" @property def init(self) -> _QGInit | None: - """Graph initialization method of the current graph or the next add, or None for an index loaded from disk.""" + """Graph initialization method of the current graph or the next add, or None for an index loaded from a file from a release up to 0.8.""" @property def nbits(self) -> int: @@ -1088,7 +1089,7 @@ class SymqgIndex: @property def seed(self) -> int | None: - """Rotation and graph-construction seed, or None for an index loaded from disk.""" + """Rotation and graph-construction seed, or None for an index loaded from a file from a release up to 0.8.""" def train( self, x: npt.ArrayLike, *, num_threads: SupportsIndex | None = None @@ -1230,7 +1231,8 @@ class SymqgIndex: Notes ----- - The saved state includes the rotation used to build the index. + The saved state includes the rotation used to build the index and its seed; + releases up to 0.8 cannot read the file. """ @staticmethod diff --git a/python_bindings/hnsw_bindings.cpp b/python_bindings/hnsw_bindings.cpp index 18297530..5da36d1c 100644 --- a/python_bindings/hnsw_bindings.cpp +++ b/python_bindings/hnsw_bindings.cpp @@ -337,8 +337,15 @@ class HnswIndex { wrapper.num_clusters_ = wrapper.index_->is_trained() ? wrapper.index_->num_clusters() : kDefaultNumClusters; + // Format-2 files record the seed; files from releases up to 0.8 do not. + const std::optional seed = wrapper.index_->seed(); + if (seed.has_value()) { + wrapper.seed_ = static_cast(*seed); + } wrapper.cp_ = default_training_parameters( - wrapper.index_->metric_type(), rabitqkmeans::RaBitQKMeansParameters{}.seed + wrapper.index_->metric_type(), + seed.has_value() ? static_cast(*seed) + : rabitqkmeans::RaBitQKMeansParameters{}.seed ); return wrapper; } @@ -447,7 +454,7 @@ class HnswIndex { std::unique_ptr index_; rabitqkmeans::RaBitQKMeansParameters cp_; size_t num_clusters_ = 0; // Centroid count train and set_centroids expect. - std::optional seed_; // Unknown for loaded indexes. + std::optional seed_; // Unknown for files from releases up to 0.8. }; } // namespace rabitqlib::python_bindings @@ -722,7 +729,8 @@ path : str, bytes, bytearray, or os.PathLike Notes ----- A trained index without vectors raises RuntimeError; add vectors first. -The saved state includes the rotation used to build the index.)doc" +The saved state includes the rotation used to build the index and its seed; +releases up to 0.8 cannot read the file.)doc" ) .def_static( "load", @@ -793,7 +801,7 @@ index : HnswIndex .def_property_readonly( "seed", &HnswIndex::seed, - R"doc(Graph and rotation seed, or None for an index loaded from disk.)doc" + R"doc(Graph and rotation seed, or None for an index loaded from a file from a release up to 0.8.)doc" ) .def_property( "cp", diff --git a/python_bindings/ivf_bindings.cpp b/python_bindings/ivf_bindings.cpp index 3aa38cab..4304e74d 100644 --- a/python_bindings/ivf_bindings.cpp +++ b/python_bindings/ivf_bindings.cpp @@ -362,8 +362,12 @@ class IvfIndex { py::gil_scoped_release release; wrapper.index_->load(filename.c_str()); } - wrapper.cp_ = untrainable_parameters(wrapper.index_->metric_type()); wrapper.builds_by_construct_ = true; + wrapper.seed_ = wrapper.index_->seed(); // recorded by format-2 files + wrapper.cp_ = default_training_parameters( + wrapper.index_->metric_type(), + wrapper.seed_.value_or(rabitqkmeans::RaBitQKMeansParameters{}.seed) + ); return wrapper; } @@ -461,7 +465,7 @@ class IvfIndex { mutable IndexAccess access_; std::unique_ptr index_; rabitqkmeans::RaBitQKMeansParameters cp_; - std::optional seed_; // Unknown for point-count and loaded indexes. + std::optional seed_; // Unknown for point-count indexes and 0.8 files. // Point-count and loaded indexes build by construct, never by training. bool builds_by_construct_ = false; }; @@ -813,7 +817,7 @@ accompanying ``.hnsw`` file.)doc" .def_property_readonly( "seed", &IvfIndex::seed, - R"doc(Rotation seed, or None for an index loaded from disk or created in the 0.7 form.)doc" + R"doc(Rotation seed, or None for an index loaded from a file from a release up to 0.8 or created in the 0.7 form.)doc" ) .def_property( "cp", diff --git a/python_bindings/symqg_bindings.cpp b/python_bindings/symqg_bindings.cpp index 3dc00723..bdfb239b 100644 --- a/python_bindings/symqg_bindings.cpp +++ b/python_bindings/symqg_bindings.cpp @@ -167,9 +167,14 @@ class SymqgIndex { wrapper.max_degree_ = wrapper.index_->degree_bound(); wrapper.metric_type_ = wrapper.index_->metric_type(); wrapper.quantization_bits_ = wrapper.index_->quantization_bits(); - // Files store neither the seed nor the construction settings. - wrapper.ef_construction_.reset(); + // Format-2 files record the seed and the construction settings; earlier files + // record neither. + wrapper.seed_ = wrapper.index_->seed(); + wrapper.ef_construction_ = wrapper.index_->ef_construction(); wrapper.init_.reset(); + if (const auto init = wrapper.index_->initialization(); init.has_value()) { + wrapper.init_ = *init == symqg::QGInitialization::PiPNN ? "pipnn" : "random"; + } return wrapper; } @@ -314,7 +319,7 @@ class SymqgIndex { // Settings of the current graph, or of the next add; unknown after load. std::optional ef_construction_ = kDefaultEfConstruction; std::optional init_ = "pipnn"; - std::optional seed_; // Unknown for loaded indexes. + std::optional seed_; // Unknown for files from releases up to 0.8. bool seed_given_ = false; // The user chose the seed. mutable IndexAccess access_; std::unique_ptr> index_; @@ -510,7 +515,8 @@ path : str, bytes, bytearray, or os.PathLike Notes ----- -The saved state includes the rotation used to build the index.)doc" +The saved state includes the rotation used to build the index and its seed; +releases up to 0.8 cannot read the file.)doc" ) .def_static( "load", @@ -548,12 +554,12 @@ index : SymqgIndex .def_property_readonly( "ef_construction", &SymqgIndex::ef_construction, - R"doc(Construction search breadth of the current graph or the next add, or None for an index loaded from disk.)doc" + R"doc(Construction search breadth of the current graph or the next add, or None for an index loaded from a file from a release up to 0.8.)doc" ) .def_property_readonly( "init", &SymqgIndex::init, - R"doc(Graph initialization method of the current graph or the next add, or None for an index loaded from disk.)doc" + R"doc(Graph initialization method of the current graph or the next add, or None for an index loaded from a file from a release up to 0.8.)doc" ) .def_property_readonly( "num_points", @@ -585,6 +591,6 @@ index : SymqgIndex .def_property_readonly( "seed", &SymqgIndex::seed, - R"doc(Rotation and graph-construction seed, or None for an index loaded from disk.)doc" + R"doc(Rotation and graph-construction seed, or None for an index loaded from a file from a release up to 0.8.)doc" ); } diff --git a/src/index/format2.cpp b/src/index/format2.cpp index 8f557382..c66e0159 100644 --- a/src/index/format2.cpp +++ b/src/index/format2.cpp @@ -20,34 +20,6 @@ constexpr uint32_t kKnownSectionFlags = kSectionRequired | kSectionChecksummed; constexpr size_t kChecksumOffset = 108; constexpr size_t kChunkBytes = size_t{1} << 16; -void put_u32(uint8_t* out, uint32_t value) { - for (size_t i = 0; i < 4; ++i) { - out[i] = static_cast(value >> (8 * i)); - } -} - -void put_u64(uint8_t* out, uint64_t value) { - for (size_t i = 0; i < 8; ++i) { - out[i] = static_cast(value >> (8 * i)); - } -} - -uint32_t get_u32(const uint8_t* in) { - uint32_t value = 0; - for (size_t i = 0; i < 4; ++i) { - value |= static_cast(in[i]) << (8 * i); - } - return value; -} - -uint64_t get_u64(const uint8_t* in) { - uint64_t value = 0; - for (size_t i = 0; i < 8; ++i) { - value |= static_cast(in[i]) << (8 * i); - } - return value; -} - // Round up to a multiple of kSectionAlignment; the caller has bounded `value`. uint64_t align_section(uint64_t value) { return (value + kSectionAlignment - 1) / kSectionAlignment * kSectionAlignment; diff --git a/src/index/format2.hpp b/src/index/format2.hpp index 705e7031..78b1a243 100644 --- a/src/index/format2.hpp +++ b/src/index/format2.hpp @@ -46,6 +46,35 @@ enum class RawType : uint32_t { None = 0, Float32 = 1, Uint8 = 2 }; inline constexpr uint32_t kSectionRequired = 1U << 0; inline constexpr uint32_t kSectionChecksummed = 1U << 1; +// Little-endian fields of fixed width, for section contents. +inline void put_u32(uint8_t* out, uint32_t value) { + for (size_t i = 0; i < 4; ++i) { + out[i] = static_cast(value >> (8 * i)); + } +} + +inline void put_u64(uint8_t* out, uint64_t value) { + for (size_t i = 0; i < 8; ++i) { + out[i] = static_cast(value >> (8 * i)); + } +} + +[[nodiscard]] inline uint32_t get_u32(const uint8_t* in) { + uint32_t value = 0; + for (size_t i = 0; i < 4; ++i) { + value |= static_cast(in[i]) << (8 * i); + } + return value; +} + +[[nodiscard]] inline uint64_t get_u64(const uint8_t* in) { + uint64_t value = 0; + for (size_t i = 0; i < 8; ++i) { + value |= static_cast(in[i]) << (8 * i); + } + return value; +} + // The section kind every index shares: its rotator blob. inline constexpr uint32_t kRotatorSection = 1; diff --git a/src/index/hnsw.cpp b/src/index/hnsw.cpp index 72dc4fa8..4e38ae31 100644 --- a/src/index/hnsw.cpp +++ b/src/index/hnsw.cpp @@ -1,6 +1,7 @@ #include "rabitqlib/index/hnsw/hnsw.hpp" #include +#include #include #include #include @@ -14,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -22,7 +24,9 @@ #include #include +#include "../core/blob.hpp" #include "file_replacement.hpp" +#include "format2.hpp" #include "rabitqlib/clustering/rabitqkmeans.hpp" #include "rabitqlib/core/data_layout.hpp" #include "rabitqlib/core/encoder.hpp" @@ -45,6 +49,16 @@ namespace rabitqlib::hnsw { +namespace { +// Index format 2 sections of HNSW-RaBitQ (docs/formats.md, "Section kinds"). +constexpr uint32_t kParamsSection = 512; +constexpr uint32_t kCentroidsSection = 513; +constexpr uint32_t kRecordsSection = 514; +constexpr uint32_t kLevelsSection = 515; +constexpr uint32_t kUpperLinksSection = 516; +constexpr size_t kParamsBytes = 72; +} // namespace + HierarchicalNSW::HierarchicalNSW() = default; HierarchicalNSW::~HierarchicalNSW() = default; void HierarchicalNSW::AlignedDeleter::operator()(char* pointer) const noexcept { @@ -118,6 +132,7 @@ HierarchicalNSW::HierarchicalNSW( ); level_generator_.seed(random_seed); + seed_ = random_seed; visited_set_pool_ = std::make_unique(1, max_elements_); @@ -149,69 +164,107 @@ void HierarchicalNSW::save(const char* filename) const { if (filename == nullptr || filename[0] == '\0') { throw std::invalid_argument("HNSW index filename must not be empty"); } + check_writable("save"); index_detail::save_replacing(filename, {}, [this](const std::string& path) { write_file(path.c_str()); }); } void HierarchicalNSW::write_file(const char* filename) const { + namespace f2 = index_detail::format2; std::ofstream output(io_impl::filesystem_path(filename), std::ios::binary); if (!output.is_open()) { throw std::runtime_error("HNSW cannot open index file for writing"); } - - // Serialize one count value rather than the bytes of the atomic object. - const size_t num_stored_points = num_stored_points_.load(); - output.write(reinterpret_cast(&max_elements_), sizeof(size_t)); - output.write(reinterpret_cast(&num_stored_points), sizeof(size_t)); - - output.write(reinterpret_cast(&dim_), sizeof(size_t)); - output.write(reinterpret_cast(&padded_dim_), sizeof(size_t)); - output.write(reinterpret_cast(&num_clusters_), sizeof(size_t)); - output.write(reinterpret_cast(&ex_bits_), sizeof(size_t)); - output.write(reinterpret_cast(&metric_type_), sizeof(metric_type_)); - - output.write(reinterpret_cast(&bin_data_bytes_), sizeof(size_t)); - output.write(reinterpret_cast(&ex_data_bytes_), sizeof(size_t)); - output.write(reinterpret_cast(&level0_links_bytes_), sizeof(size_t)); - output.write(reinterpret_cast(&bin_data_offset_), sizeof(size_t)); - output.write(reinterpret_cast(&ex_data_offset_), sizeof(size_t)); - output.write(reinterpret_cast(&label_offset_), sizeof(PID)); - output.write(reinterpret_cast(&level0_record_bytes_), sizeof(size_t)); - output.write(reinterpret_cast(&upper_layer_links_bytes_), sizeof(size_t)); - - output.write(reinterpret_cast(&max_level_), sizeof(int)); - output.write(reinterpret_cast(&entry_point_id_), sizeof(PID)); - - output.write(reinterpret_cast(&M_), sizeof(size_t)); - output.write(reinterpret_cast(&upper_layer_max_degree_), sizeof(size_t)); - output.write(reinterpret_cast(&base_layer_max_degree_), sizeof(size_t)); - output.write(reinterpret_cast(&level_multiplier_), sizeof(double)); - output.write(reinterpret_cast(&ef_construction_), sizeof(size_t)); - - output.write( - reinterpret_cast(centroids_memory_.get()), - static_cast(num_clusters_ * padded_dim_ * sizeof(float)) + const size_t element_count = num_stored_points_.load(); + + f2::Header header; + header.index_kind = f2::IndexKind::Hnsw; + header.metric = metric_type_ == METRIC_IP ? f2::Metric::InnerProduct : f2::Metric::L2; + header.dim = dim_; + header.padded_dim = padded_dim_; + header.code_bits = static_cast(ex_bits_ + 1); + header.id_bits = 32; + header.seed = seed_; + header.num_points = element_count; + + // The rotation was drawn from the low 32 bits of the seed. + std::vector rotator( + rabitqlib::detail::rotator_blob_bytes(RotatorType::FhtKacRotator, dim_, padded_dim_) ); - - output.write( - reinterpret_cast(data_level0_memory_.get()), - static_cast(num_stored_points * level0_record_bytes_) + rabitqlib::detail::encode_rotator_blob( + *rotator_, + RotatorType::FhtKacRotator, + dim_, + seed_.has_value() ? std::optional(static_cast(*seed_)) + : std::nullopt, + rotator.data() ); - - for (size_t i = 0; i < num_stored_points; ++i) { - uint32_t link_list_bytes = - element_levels_[i] > 0 ? upper_layer_links_bytes_ * element_levels_[i] : 0; - output.write(reinterpret_cast(&link_list_bytes), sizeof(uint32_t)); - if (link_list_bytes != 0) { - output.write(link_lists_[i].get(), link_list_bytes); - } + std::array params{}; + f2::put_u64(params.data(), max_elements_); + f2::put_u64(params.data() + 8, num_clusters_); + f2::put_u64(params.data() + 16, M_); + f2::put_u64(params.data() + 24, upper_layer_max_degree_); + f2::put_u64(params.data() + 32, base_layer_max_degree_); + f2::put_u64(params.data() + 40, ef_construction_); + uint64_t multiplier_bits = 0; + std::memcpy(&multiplier_bits, &level_multiplier_, sizeof(multiplier_bits)); + f2::put_u64(params.data() + 48, multiplier_bits); + f2::put_u32( + params.data() + 56, static_cast(static_cast(max_level_)) + ); + f2::put_u64(params.data() + 64, entry_point_id_); + std::vector levels(sizeof(uint32_t) * element_count); + uint64_t upper_bytes = 0; + for (size_t i = 0; i < element_count; ++i) { + f2::put_u32(levels.data() + (sizeof(uint32_t) * i), element_levels_[i]); + upper_bytes += uint64_t(element_levels_[i]) * upper_layer_links_bytes_; } - rotator_->save(output); + const auto bytes = [](const auto& buffer) { + return + [&buffer](f2::SectionSink& sink) { sink.write(buffer.data(), buffer.size()); }; + }; + const std::vector sections = { + {f2::kRotatorSection, f2::kSectionRequired, rotator.size(), bytes(rotator)}, + {kParamsSection, + f2::kSectionRequired | f2::kSectionChecksummed, + params.size(), + bytes(params)}, + {kCentroidsSection, + f2::kSectionRequired, + sizeof(float) * num_clusters_ * padded_dim_, + [this](f2::SectionSink& sink) { + sink.write( + centroids_memory_.get(), sizeof(float) * num_clusters_ * padded_dim_ + ); + }}, + {kRecordsSection, + f2::kSectionRequired, + element_count * level0_record_bytes_, + [&](f2::SectionSink& sink) { + sink.write(data_level0_memory_.get(), element_count * level0_record_bytes_); + }}, + {kLevelsSection, f2::kSectionRequired, levels.size(), bytes(levels)}, + {kUpperLinksSection, + f2::kSectionRequired, + upper_bytes, + [&](f2::SectionSink& sink) { + for (size_t i = 0; i < element_count; ++i) { + if (element_levels_[i] > 0) { + sink.write( + link_lists_[i].get(), + static_cast(element_levels_[i]) * upper_layer_links_bytes_ + ); + } + } + }}, + }; + // write_file checks the stream after flushing; close reports a late failure. + f2::write_file(output, header, sections, RABITQ_LIBRARY_VERSION); output.close(); if (!output) { - throw std::runtime_error("HNSW failed to write index file"); + throw std::runtime_error("HNSW index file cannot be written"); } } @@ -224,6 +277,66 @@ void HierarchicalNSW::load(const char* filename) { throw std::runtime_error("HNSW cannot open index file"); } + // Keep the current index intact until the complete file has been validated. + HierarchicalNSW loaded; + if (index_detail::format2::has_magic(input, index_detail::format2::IndexKind::Hnsw)) { + loaded.read_format2(input); + } else { + loaded.read_legacy(input); + } + + loaded.visited_set_pool_ = std::make_unique(1, loaded.max_elements_); + loaded.raw_dist_func_ = + (loaded.metric_type_ == METRIC_IP) ? dot_product_dis : euclidean_sqr; + loaded.ip_func_ = select_excode_ipfunc(loaded.ex_bits_); + loaded.query_config_ = + quant::faster_config(loaded.padded_dim_, SplitSingleQuery::kNumBits); + + // All swaps below are non-allocating. The temporary releases the old state. + using std::swap; + swap(construction_data_, loaded.construction_data_); + swap(max_elements_, loaded.max_elements_); + const size_t old_count = num_stored_points_.load(); + num_stored_points_ = loaded.num_stored_points_.load(); + loaded.num_stored_points_ = old_count; + swap(num_removed_, loaded.num_removed_); + swap(level0_record_bytes_, loaded.level0_record_bytes_); + swap(upper_layer_links_bytes_, loaded.upper_layer_links_bytes_); + swap(M_, loaded.M_); + swap(upper_layer_max_degree_, loaded.upper_layer_max_degree_); + swap(base_layer_max_degree_, loaded.base_layer_max_degree_); + swap(ef_construction_, loaded.ef_construction_); + swap(metric_type_, loaded.metric_type_); + swap(level_multiplier_, loaded.level_multiplier_); + swap(max_level_, loaded.max_level_); + link_list_locks_.swap(loaded.link_list_locks_); + swap(entry_point_id_, loaded.entry_point_id_); + swap(level0_links_bytes_, loaded.level0_links_bytes_); + swap(bin_data_offset_, loaded.bin_data_offset_); + swap(ex_data_offset_, loaded.ex_data_offset_); + swap(label_offset_, loaded.label_offset_); + swap(bin_data_bytes_, loaded.bin_data_bytes_); + swap(ex_data_bytes_, loaded.ex_data_bytes_); + swap(ex_bits_, loaded.ex_bits_); + swap(data_level0_memory_, loaded.data_level0_memory_); + swap(link_lists_, loaded.link_lists_); + element_levels_.swap(loaded.element_levels_); + swap(num_clusters_, loaded.num_clusters_); + swap(dim_, loaded.dim_); + swap(padded_dim_, loaded.padded_dim_); + swap(centroids_memory_, loaded.centroids_memory_); + label_lookup_.swap(loaded.label_lookup_); + swap(visited_set_pool_, loaded.visited_set_pool_); + swap(ip_func_, loaded.ip_func_); + swap(rotator_, loaded.rotator_); + swap(query_config_, loaded.query_config_); + swap(raw_dist_func_, loaded.raw_dist_func_); + swap(seed_, loaded.seed_); + swap(read_only_, loaded.read_only_); +} + +// The unversioned layout of releases up to 0.8. +void HierarchicalNSW::read_legacy(std::ifstream& input) { auto invalid_file = []() -> void { throw std::runtime_error("HNSW index file is invalid or truncated"); }; @@ -240,74 +353,65 @@ void HierarchicalNSW::load(const char* filename) { return left * right; }; - // Keep the current index intact until the complete file has been validated. - HierarchicalNSW loaded; size_t element_count = 0; PID label_offset = 0; - read_header(loaded.max_elements_); + read_header(max_elements_); read_header(element_count); - read_header(loaded.dim_); - read_header(loaded.padded_dim_); - read_header(loaded.num_clusters_); - read_header(loaded.ex_bits_); - read_header(loaded.metric_type_); - read_header(loaded.bin_data_bytes_); - read_header(loaded.ex_data_bytes_); - read_header(loaded.level0_links_bytes_); - read_header(loaded.bin_data_offset_); - read_header(loaded.ex_data_offset_); + read_header(dim_); + read_header(padded_dim_); + read_header(num_clusters_); + read_header(ex_bits_); + read_header(metric_type_); + read_header(bin_data_bytes_); + read_header(ex_data_bytes_); + read_header(level0_links_bytes_); + read_header(bin_data_offset_); + read_header(ex_data_offset_); read_header(label_offset); - loaded.label_offset_ = label_offset; - read_header(loaded.level0_record_bytes_); - read_header(loaded.upper_layer_links_bytes_); - read_header(loaded.max_level_); - read_header(loaded.entry_point_id_); - read_header(loaded.M_); - read_header(loaded.upper_layer_max_degree_); - read_header(loaded.base_layer_max_degree_); - read_header(loaded.level_multiplier_); - read_header(loaded.ef_construction_); - - if (loaded.max_elements_ == 0 || - loaded.max_elements_ > buffer::kSearchBufferMaxPointCount || - element_count > loaded.max_elements_ || - loaded.num_clusters_ > buffer::kSearchBufferMaxPointCount || - (element_count != 0 && loaded.num_clusters_ == 0) || loaded.dim_ < 64 || - loaded.dim_ > rotator_impl::FhtKacRotator::kMaxDim || - loaded.padded_dim_ < loaded.dim_ || loaded.padded_dim_ % 32 != 0 || - loaded.padded_dim_ > rotator_impl::FhtKacRotator::kMaxDim || loaded.ex_bits_ > 8 || - (loaded.metric_type_ != METRIC_L2 && loaded.metric_type_ != METRIC_IP) || - loaded.M_ < 2 || loaded.M_ > kMaxM || loaded.upper_layer_max_degree_ != loaded.M_ || - loaded.base_layer_max_degree_ != 2 * loaded.M_ || - loaded.ef_construction_ < loaded.M_ || - (element_count == 0 && - (loaded.max_level_ != -1 || loaded.entry_point_id_ != kPidMax)) || - (element_count != 0 && - (loaded.max_level_ < 0 || loaded.entry_point_id_ >= element_count))) { + label_offset_ = label_offset; + read_header(level0_record_bytes_); + read_header(upper_layer_links_bytes_); + read_header(max_level_); + read_header(entry_point_id_); + read_header(M_); + read_header(upper_layer_max_degree_); + read_header(base_layer_max_degree_); + read_header(level_multiplier_); + read_header(ef_construction_); + + if (max_elements_ == 0 || max_elements_ > buffer::kSearchBufferMaxPointCount || + element_count > max_elements_ || + num_clusters_ > buffer::kSearchBufferMaxPointCount || + (element_count != 0 && num_clusters_ == 0) || dim_ < 64 || + dim_ > rotator_impl::FhtKacRotator::kMaxDim || padded_dim_ < dim_ || + padded_dim_ % 32 != 0 || padded_dim_ > rotator_impl::FhtKacRotator::kMaxDim || + ex_bits_ > 8 || (metric_type_ != METRIC_L2 && metric_type_ != METRIC_IP) || + M_ < 2 || M_ > kMaxM || upper_layer_max_degree_ != M_ || + base_layer_max_degree_ != 2 * M_ || ef_construction_ < M_ || + (element_count == 0 && (max_level_ != -1 || entry_point_id_ != kPidMax)) || + (element_count != 0 && (max_level_ < 0 || entry_point_id_ >= element_count))) { invalid_file(); } // The stored layout must be the one the validated parameters derive. - const auto stored_layout = loaded.record_layout(); - loaded.set_record_layout(); - if (loaded.record_layout() != stored_layout || - !std::isfinite(loaded.level_multiplier_) || loaded.level_multiplier_ <= 0.0) { + const auto stored_layout = record_layout(); + set_record_layout(); + if (record_layout() != stored_layout || !std::isfinite(level_multiplier_) || + level_multiplier_ <= 0.0) { invalid_file(); } - const size_t centroids_bytes = checked_product( - checked_product(loaded.num_clusters_, loaded.padded_dim_), sizeof(float) - ); - const size_t data_capacity_bytes = - checked_product(loaded.max_elements_, loaded.level0_record_bytes_); - const size_t data_bytes = checked_product(element_count, loaded.level0_record_bytes_); + const size_t centroids_bytes = + checked_product(checked_product(num_clusters_, padded_dim_), sizeof(float)); + const size_t data_capacity_bytes = checked_product(max_elements_, level0_record_bytes_); + const size_t data_bytes = checked_product(element_count, level0_record_bytes_); // Validate pointer-table capacity before allocating it. - (void)checked_product(loaded.max_elements_, sizeof(LinkStorage)); + (void)checked_product(max_elements_, sizeof(LinkStorage)); const size_t link_headers_bytes = checked_product(element_count, sizeof(uint32_t)); - loaded.rotator_ = rotator_impl::make_rotator_for_loading( - loaded.dim_, RotatorType::FhtKacRotator, loaded.padded_dim_ + rotator_ = rotator_impl::make_rotator_for_loading( + dim_, RotatorType::FhtKacRotator, padded_dim_ ); - const size_t rotator_bytes = loaded.rotator_->dump_bytes(); + const size_t rotator_bytes = rotator_->dump_bytes(); const auto payload_start = input.tellg(); input.seekg(0, std::ios::end); @@ -337,85 +441,268 @@ void HierarchicalNSW::load(const char* filename) { }; if (centroids_bytes != 0) { - loaded.centroids_memory_.reset(memory::huge_page_allocate(centroids_bytes)); - read_payload(loaded.centroids_memory_.get(), centroids_bytes); + centroids_memory_.reset(memory::huge_page_allocate(centroids_bytes)); + read_payload(centroids_memory_.get(), centroids_bytes); + } + data_level0_memory_.reset(memory::huge_page_allocate(data_capacity_bytes)); + read_payload(data_level0_memory_.get(), data_bytes); + + element_levels_ = std::vector(max_elements_); + link_list_locks_ = std::vector(max_elements_); + link_lists_.reserve(max_elements_); + link_lists_.resize(element_count); + for (size_t i = 0; i < element_count; ++i) { + uint32_t link_list_bytes = 0; + read_payload(reinterpret_cast(&link_list_bytes), sizeof(link_list_bytes)); + if (link_list_bytes != 0) { + if (link_list_bytes % upper_layer_links_bytes_ != 0 || + link_list_bytes / upper_layer_links_bytes_ > + static_cast(max_level_) || + link_list_bytes > remaining) { + invalid_file(); + } + link_lists_[i].reset(static_cast(std::malloc(link_list_bytes))); + if (link_lists_[i] == nullptr) { + throw std::bad_alloc(); + } + element_levels_[i] = + static_cast(link_list_bytes / upper_layer_links_bytes_); + } + num_stored_points_ = i + 1; + if (link_list_bytes != 0) { + read_payload(link_lists_[i].get(), link_list_bytes); + } + } + validate_graph(); + + if (rotator_bytes > remaining) { + invalid_file(); + } + rotator_->load(input); + if (!input) { + invalid_file(); + } + remaining -= rotator_bytes; + if (remaining != 0) { + invalid_file(); + } +} + +void HierarchicalNSW::read_format2(std::istream& input) { + namespace f2 = index_detail::format2; + const std::vector known_kinds = { + f2::kRotatorSection, + kParamsSection, + kCentroidsSection, + kRecordsSection, + kLevelsSection, + kUpperLinksSection}; + f2::Reader reader( + input, f2::IndexKind::Hnsw, known_kinds, [](const f2::Header& header) { + if (header.metric == f2::Metric::Cosine) { + throw f2::FormatError( + "HNSW index file uses the cosine metric, which this release does not " + "support" + ); + } + if (header.id_bits != 32 || header.raw_type != f2::RawType::None) { + throw f2::FormatError("HNSW index file stores 64-bit IDs or raw vectors"); + } + if (header.code_bits == 0 || header.dim < 64) { + throw f2::FormatError( + "HNSW index file has an invalid code width or dimension" + ); + } + if (header.num_points == 0 || + header.num_points > buffer::kSearchBufferMaxPointCount) { + throw f2::FormatError( + "HNSW index file has an invalid point count " + + std::to_string(header.num_points) + ); + } + } + ); + const f2::Header& header = reader.header(); + metric_type_ = header.metric == f2::Metric::InnerProduct ? METRIC_IP : METRIC_L2; + dim_ = static_cast(header.dim); + padded_dim_ = static_cast(header.padded_dim); + ex_bits_ = header.code_bits - 1; + seed_ = header.seed; + const auto element_count = static_cast(header.num_points); + + const auto require = [&reader](uint32_t kind, std::optional size) { + const f2::Section* section = reader.find(kind); + if (section == nullptr) { + throw f2::FormatError( + "HNSW index file has no section kind " + std::to_string(kind) + ); + } + if (size.has_value() && section->size != *size) { + throw f2::FormatError( + "HNSW index file section kind " + std::to_string(kind) + " has " + + std::to_string(section->size) + " bytes; expected " + std::to_string(*size) + ); + } + return section; + }; + const auto read_all = [&reader](const f2::Section& section) { + std::vector bytes(static_cast(section.size)); + reader.read(section, 0, bytes.data(), bytes.size()); + return bytes; + }; + + const std::vector params = read_all(*require(kParamsSection, kParamsBytes)); + max_elements_ = static_cast(f2::get_u64(params.data())); + num_clusters_ = static_cast(f2::get_u64(params.data() + 8)); + M_ = static_cast(f2::get_u64(params.data() + 16)); + upper_layer_max_degree_ = static_cast(f2::get_u64(params.data() + 24)); + base_layer_max_degree_ = static_cast(f2::get_u64(params.data() + 32)); + ef_construction_ = static_cast(f2::get_u64(params.data() + 40)); + const uint64_t multiplier_bits = f2::get_u64(params.data() + 48); + std::memcpy(&level_multiplier_, &multiplier_bits, sizeof(level_multiplier_)); + max_level_ = static_cast(static_cast(f2::get_u32(params.data() + 56))); + const uint64_t entry_point = f2::get_u64(params.data() + 64); + if (max_elements_ < element_count || + max_elements_ > buffer::kSearchBufferMaxPointCount || num_clusters_ == 0 || + num_clusters_ > buffer::kSearchBufferMaxPointCount || M_ < 2 || M_ > kMaxM || + upper_layer_max_degree_ != M_ || base_layer_max_degree_ != 2 * M_ || + ef_construction_ < M_ || !std::isfinite(level_multiplier_) || + level_multiplier_ <= 0.0 || max_level_ < 0 || entry_point >= element_count || + f2::get_u32(params.data() + 60) != 0) { + throw f2::FormatError("HNSW index file has invalid graph parameters"); + } + entry_point_id_ = static_cast(entry_point); + set_record_layout(); + + const std::vector blob = read_all(*require(f2::kRotatorSection, std::nullopt)); + rabitqlib::detail::DecodedRotator rotator; + try { + rotator = rabitqlib::detail::decode_rotator_blob(blob.data(), blob.size()); + } catch (const std::invalid_argument& error) { + throw f2::FormatError( + std::string("HNSW index file has an invalid rotator (") + error.what() + ")" + ); + } + if (rotator.type != RotatorType::FhtKacRotator || rotator.dim != dim_ || + rotator.padded_dim != padded_dim_) { + throw f2::FormatError("HNSW index file rotator does not match its dimensions"); + } + rotator_ = std::move(rotator.rotator); + + const size_t centroids_bytes = sizeof(float) * num_clusters_ * padded_dim_; + const f2::Section* centroids = require(kCentroidsSection, centroids_bytes); + const f2::Section* records = + require(kRecordsSection, element_count * level0_record_bytes_); + const std::vector levels = + read_all(*require(kLevelsSection, sizeof(uint32_t) * element_count)); + uint64_t upper_bytes = 0; + element_levels_ = std::vector(max_elements_); + for (size_t i = 0; i < element_count; ++i) { + const uint32_t level = f2::get_u32(levels.data() + (sizeof(uint32_t) * i)); + if (level > static_cast(max_level_)) { + throw f2::FormatError("HNSW index file has a point above the maximum level"); + } + element_levels_[i] = static_cast(level); + // Guard the sum, so that a wrapped total cannot match the section size. + const uint64_t bytes = uint64_t{level} * upper_layer_links_bytes_; + if (bytes > std::numeric_limits::max() - upper_bytes) { + throw f2::FormatError("HNSW index file has an invalid level structure"); + } + upper_bytes += bytes; + } + const f2::Section* upper = require(kUpperLinksSection, upper_bytes); + + centroids_memory_.reset(memory::huge_page_allocate(centroids_bytes)); + f2::SectionInput centroids_input = reader.open(*centroids); + centroids_input.read(centroids_memory_.get(), centroids_bytes); + data_level0_memory_.reset( + memory::huge_page_allocate(max_elements_ * level0_record_bytes_) + ); + f2::SectionInput records_input = reader.open(*records); + records_input.read(data_level0_memory_.get(), element_count * level0_record_bytes_); + link_list_locks_ = std::vector(max_elements_); + link_lists_.reserve(max_elements_); + link_lists_.resize(element_count); + f2::SectionInput upper_input = reader.open(*upper); + for (size_t i = 0; i < element_count; ++i) { + const size_t bytes = + static_cast(element_levels_[i]) * upper_layer_links_bytes_; + if (bytes == 0) { + continue; + } + link_lists_[i].reset(static_cast(std::malloc(bytes))); + if (link_lists_[i] == nullptr) { + throw std::bad_alloc(); + } + upper_input.read(link_lists_[i].get(), bytes); + } + num_stored_points_ = element_count; + validate_graph(); + read_only_ = reader.read_only(); +} + +void HierarchicalNSW::check_writable(const char* operation) const { + if (read_only_) { + throw std::logic_error( + std::string( + "HNSW index was loaded from a file with features this release " + "cannot update, so it cannot " + ) + + operation + ); } - loaded.data_level0_memory_.reset(memory::huge_page_allocate(data_capacity_bytes)); - read_payload(loaded.data_level0_memory_.get(), data_bytes); +} - loaded.element_levels_ = std::vector(loaded.max_elements_); - loaded.link_list_locks_ = std::vector(loaded.max_elements_); - loaded.link_lists_.reserve(loaded.max_elements_); - loaded.link_lists_.resize(element_count); +void HierarchicalNSW::validate_graph() { + const auto invalid = []() -> void { + throw std::runtime_error("HNSW index file is invalid or truncated"); + }; + const size_t element_count = num_stored_points_; for (size_t i = 0; i < element_count; ++i) { - const char* row = - loaded.data_level0_memory_.get() + i * loaded.level0_record_bytes_; + const char* row = data_level0_memory_.get() + i * level0_record_bytes_; PID cluster_id = 0; PID label = 0; uint16_t level0_degree = 0; std::memcpy(&level0_degree, row, sizeof(level0_degree)); - std::memcpy(&cluster_id, row + loaded.level0_links_bytes_, sizeof(cluster_id)); - std::memcpy(&label, row + loaded.label_offset_, sizeof(label)); - if ((cluster_id & ~kRemovedMask) >= loaded.num_clusters_ || - level0_degree > loaded.base_layer_max_degree_) { - invalid_file(); + std::memcpy(&cluster_id, row + level0_links_bytes_, sizeof(cluster_id)); + std::memcpy(&label, row + label_offset_, sizeof(label)); + if ((cluster_id & ~kRemovedMask) >= num_clusters_ || + level0_degree > base_layer_max_degree_) { + invalid(); } - loaded.num_removed_ += (cluster_id & kRemovedMask) != 0; + num_removed_ += (cluster_id & kRemovedMask) != 0; for (size_t edge = 0; edge < level0_degree; ++edge) { PID neighbor = 0; std::memcpy(&neighbor, row + sizeof(PID) + edge * sizeof(PID), sizeof(PID)); if (neighbor >= element_count) { - invalid_file(); + invalid(); } } - if (!loaded.label_lookup_.emplace(label, static_cast(i)).second) { - invalid_file(); + if (!label_lookup_.emplace(label, static_cast(i)).second) { + invalid(); } - - uint32_t link_list_bytes = 0; - read_payload(reinterpret_cast(&link_list_bytes), sizeof(link_list_bytes)); - if (link_list_bytes != 0) { - if (link_list_bytes % loaded.upper_layer_links_bytes_ != 0 || - link_list_bytes / loaded.upper_layer_links_bytes_ > - static_cast(loaded.max_level_) || - link_list_bytes > remaining) { - invalid_file(); - } - loaded.link_lists_[i].reset(static_cast(std::malloc(link_list_bytes))); - if (loaded.link_lists_[i] == nullptr) { - throw std::bad_alloc(); + for (int level = 0; level < element_levels_[i]; ++level) { + const char* link = link_lists_[i].get() + level * upper_layer_links_bytes_; + uint16_t degree = 0; + std::memcpy(°ree, link, sizeof(degree)); + if (degree > upper_layer_max_degree_) { + invalid(); } - loaded.element_levels_[i] = - static_cast(link_list_bytes / loaded.upper_layer_links_bytes_); - } - loaded.num_stored_points_ = i + 1; - if (link_list_bytes != 0) { - read_payload(loaded.link_lists_[i].get(), link_list_bytes); - for (int level = 0; level < loaded.element_levels_[i]; ++level) { - const char* link = - loaded.link_lists_[i].get() + level * loaded.upper_layer_links_bytes_; - uint16_t degree = 0; - std::memcpy(°ree, link, sizeof(degree)); - if (degree > loaded.upper_layer_max_degree_) { - invalid_file(); - } - for (size_t edge = 0; edge < degree; ++edge) { - PID neighbor = 0; - std::memcpy( - &neighbor, link + sizeof(PID) + edge * sizeof(PID), sizeof(PID) - ); - if (neighbor >= element_count) { - invalid_file(); - } + for (size_t edge = 0; edge < degree; ++edge) { + PID neighbor = 0; + std::memcpy( + &neighbor, link + sizeof(PID) + edge * sizeof(PID), sizeof(PID) + ); + if (neighbor >= element_count) { + invalid(); } } } } for (size_t i = 0; i < element_count; ++i) { - for (int level = 1; level <= loaded.element_levels_[i]; ++level) { - const char* link = - loaded.link_lists_[i].get() + - (static_cast(level) - 1) * loaded.upper_layer_links_bytes_; + for (int level = 1; level <= element_levels_[i]; ++level) { + const char* link = link_lists_[i].get() + + (static_cast(level) - 1) * upper_layer_links_bytes_; uint16_t degree = 0; std::memcpy(°ree, link, sizeof(degree)); for (size_t edge = 0; edge < degree; ++edge) { @@ -423,75 +710,15 @@ void HierarchicalNSW::load(const char* filename) { std::memcpy( &neighbor, link + sizeof(PID) + edge * sizeof(PID), sizeof(PID) ); - if (loaded.element_levels_[neighbor] < level) { - invalid_file(); + if (element_levels_[neighbor] < level) { + invalid(); } } } } - if (element_count != 0 && - loaded.element_levels_[loaded.entry_point_id_] != loaded.max_level_) { - invalid_file(); - } - - if (rotator_bytes > remaining) { - invalid_file(); - } - loaded.rotator_->load(input); - if (!input) { - invalid_file(); - } - remaining -= rotator_bytes; - if (remaining != 0) { - invalid_file(); + if (element_count != 0 && element_levels_[entry_point_id_] != max_level_) { + invalid(); } - - loaded.visited_set_pool_ = std::make_unique(1, loaded.max_elements_); - loaded.raw_dist_func_ = - (loaded.metric_type_ == METRIC_IP) ? dot_product_dis : euclidean_sqr; - loaded.ip_func_ = select_excode_ipfunc(loaded.ex_bits_); - loaded.query_config_ = - quant::faster_config(loaded.padded_dim_, SplitSingleQuery::kNumBits); - - // All swaps below are non-allocating. The temporary releases the old state. - using std::swap; - swap(construction_data_, loaded.construction_data_); - swap(max_elements_, loaded.max_elements_); - const size_t old_count = num_stored_points_.load(); - num_stored_points_ = loaded.num_stored_points_.load(); - loaded.num_stored_points_ = old_count; - swap(num_removed_, loaded.num_removed_); - swap(level0_record_bytes_, loaded.level0_record_bytes_); - swap(upper_layer_links_bytes_, loaded.upper_layer_links_bytes_); - swap(M_, loaded.M_); - swap(upper_layer_max_degree_, loaded.upper_layer_max_degree_); - swap(base_layer_max_degree_, loaded.base_layer_max_degree_); - swap(ef_construction_, loaded.ef_construction_); - swap(metric_type_, loaded.metric_type_); - swap(level_multiplier_, loaded.level_multiplier_); - swap(max_level_, loaded.max_level_); - link_list_locks_.swap(loaded.link_list_locks_); - swap(entry_point_id_, loaded.entry_point_id_); - swap(level0_links_bytes_, loaded.level0_links_bytes_); - swap(bin_data_offset_, loaded.bin_data_offset_); - swap(ex_data_offset_, loaded.ex_data_offset_); - swap(label_offset_, loaded.label_offset_); - swap(bin_data_bytes_, loaded.bin_data_bytes_); - swap(ex_data_bytes_, loaded.ex_data_bytes_); - swap(ex_bits_, loaded.ex_bits_); - swap(data_level0_memory_, loaded.data_level0_memory_); - swap(link_lists_, loaded.link_lists_); - element_levels_.swap(loaded.element_levels_); - swap(num_clusters_, loaded.num_clusters_); - swap(dim_, loaded.dim_); - swap(padded_dim_, loaded.padded_dim_); - swap(centroids_memory_, loaded.centroids_memory_); - label_lookup_.swap(loaded.label_lookup_); - swap(visited_set_pool_, loaded.visited_set_pool_); - swap(ip_func_, loaded.ip_func_); - swap(rotator_, loaded.rotator_); - swap(query_config_, loaded.query_config_); - swap(raw_dist_func_, loaded.raw_dist_func_); } void HierarchicalNSW::construct( @@ -1112,6 +1339,7 @@ size_t HierarchicalNSW::remove(const PID* labels, size_t num_points) { rotator_ == nullptr || num_clusters_ == 0) { throw std::logic_error("HNSW index must be constructed or loaded before remove"); } + check_writable("remove"); if (num_points == 0) { return 0; } @@ -1319,6 +1547,7 @@ void HierarchicalNSW::resize(size_t new_max_elements) { if (level0_record_bytes_ == 0) { throw std::logic_error("HNSW index must be initialized before resize"); } + check_writable("resize"); if (new_max_elements == 0) { throw std::invalid_argument("HNSW resize capacity must be greater than zero"); } @@ -1379,6 +1608,7 @@ std::vector HierarchicalNSW::add( "HNSW index must be trained, constructed or loaded before add" ); } + check_writable("add"); if (num_points == 0) { return {}; } diff --git a/src/index/ivf.cpp b/src/index/ivf.cpp index 961a7dcf..f66f3d19 100644 --- a/src/index/ivf.cpp +++ b/src/index/ivf.cpp @@ -52,6 +52,11 @@ namespace rabitqlib::ivf { namespace { +using index_detail::format2::get_u32; +using index_detail::format2::get_u64; +using index_detail::format2::put_u32; +using index_detail::format2::put_u64; + // remove() marks a point by this f_add; save and load keep the marker. constexpr float kRemovedFAdd = std::numeric_limits::infinity(); // Files without routing metadata used HNSW routing from this many clusters. @@ -86,33 +91,6 @@ uint32_t routing_section(InitializerType type) { } } -void put_u32(uint8_t* out, uint32_t value) { - for (size_t i = 0; i < 4; ++i) { - out[i] = static_cast(value >> (8 * i)); - } -} - -void put_u64(uint8_t* out, uint64_t value) { - for (size_t i = 0; i < 8; ++i) { - out[i] = static_cast(value >> (8 * i)); - } -} - -uint32_t get_u32(const uint8_t* in) { - uint32_t value = 0; - for (size_t i = 0; i < 4; ++i) { - value |= static_cast(in[i]) << (8 * i); - } - return value; -} - -uint64_t get_u64(const uint8_t* in) { - uint64_t value = 0; - for (size_t i = 0; i < 8; ++i) { - value |= static_cast(in[i]) << (8 * i); - } - return value; -} } // namespace IVF::IVF() noexcept = default; @@ -202,6 +180,7 @@ void IVF::swap(IVF& other) noexcept { swap(raw_reranking_, other.raw_reranking_); swap(ready_, other.ready_); swap(read_only_, other.read_only_); + swap(seed_, other.seed_); swap(ex_bits_, other.ex_bits_); swap(rotator_type_, other.rotator_type_); swap(rotator_, other.rotator_); @@ -254,7 +233,8 @@ IVF::IVF( , raw_reranking_(total_bits == 32) , ex_bits_(total_bits == 32 ? 0 : total_bits - 1) , rotator_type_(rotator_type) - , metric_type_(metric_type) { + , metric_type_(metric_type) + , seed_(seed) { configure(total_bits); rotator_.reset(choose_rotator(dim, rotator_type, padded_dim_, seed)); } @@ -956,13 +936,14 @@ void IVF::write_file(const char* filename) const { header.code_bits = raw_reranking_ ? 1 : static_cast(ex_bits_ + 1); header.raw_type = raw_reranking_ ? f2::RawType::Float32 : f2::RawType::None; header.id_bits = 32; + header.seed = seed_; header.num_points = max_elements_; std::vector rotator( rabitqlib::detail::rotator_blob_bytes(rotator_type_, dim_, padded_dim_) ); rabitqlib::detail::encode_rotator_blob( - *rotator_, rotator_type_, dim_, std::nullopt, rotator.data() + *rotator_, rotator_type_, dim_, seed_, rotator.data() ); std::array params{}; put_u64(params.data(), num_clusters_); @@ -1275,6 +1256,10 @@ void IVF::read_format2(std::istream& input) { std::to_string(header.num_points) ); } + if (header.seed.has_value() && + *header.seed > std::numeric_limits::max()) { + throw f2::FormatError("IVF index file has a seed above 32 bits"); + } }); const f2::Header& header = reader.header(); metric_type_ = header.metric == f2::Metric::InnerProduct ? METRIC_IP : METRIC_L2; @@ -1283,6 +1268,9 @@ void IVF::read_format2(std::istream& input) { raw_reranking_ = header.raw_type == f2::RawType::Float32; ex_bits_ = raw_reranking_ ? 0 : header.code_bits - 1; max_elements_ = static_cast(header.num_points); + if (header.seed.has_value()) { + seed_ = static_cast(*header.seed); + } // A section of this kind, whose size, when given, must match before it is read. const auto require = [&reader](uint32_t kind, std::optional size) { diff --git a/src/index/qg.cpp b/src/index/qg.cpp index 02da4bed..a80ee9cf 100644 --- a/src/index/qg.cpp +++ b/src/index/qg.cpp @@ -1,6 +1,7 @@ #include "rabitqlib/index/symqg/qg.hpp" #include +#include #include #include #include @@ -15,7 +16,9 @@ #include #include +#include "../core/blob.hpp" #include "file_replacement.hpp" +#include "format2.hpp" #include "rabitqlib/core/data_layout.hpp" #include "rabitqlib/core/encoder.hpp" #include "rabitqlib/core/metric.hpp" @@ -45,6 +48,15 @@ namespace { // Native-endian numeric tag; little-endian files contain the bytes "QTIBARGQ". constexpr uint64_t kFormatMagic = 0x5147524142495451ULL; constexpr uint32_t kFormatVersion = 1; + +// Index format 2 sections of SymphonyQG (docs/formats.md, "Section kinds"). +constexpr uint32_t kParamsSection = 768; +constexpr uint32_t kCentroidSection = 769; +constexpr uint32_t kRecordsSection = 770; +constexpr size_t kParamsBytes = 32; +// The stored initialization: 0 is unknown. +constexpr uint32_t kInitPiPNN = 1; +constexpr uint32_t kInitRandom = 2; } // namespace QuantizedQuery::QuantizedQuery( @@ -139,7 +151,8 @@ QuantizedGraph::QuantizedGraph( , metric_type_(metric_type) , rotator_type_(rotator_type) , quantization_bits_(quantization_bits) - , seed_(seed) { + , seed_(seed) + , seed_known_(true) { validate_configuration(); padded_dim_ = padded_dimension(dim_); initialize(); @@ -262,54 +275,226 @@ void QuantizedGraph::save(const char* filename) const { if (filename == nullptr || filename[0] == '\0') { throw std::invalid_argument("QuantizedGraph save filename must not be empty"); } + if (read_only_) { + throw std::logic_error( + "QuantizedGraph was loaded from a file with features this release cannot " + "update, so it cannot save" + ); + } index_detail::save_replacing(filename, {}, [this](const std::string& path) { write_file(path.c_str()); }); } void QuantizedGraph::write_file(const char* filename) const { + namespace f2 = index_detail::format2; std::ofstream output(io_impl::filesystem_path(filename), std::ios::binary); if (!output.is_open()) { throw std::runtime_error("QuantizedGraph cannot open file for writing"); } output.exceptions(std::ios::badbit | std::ios::failbit); + f2::Header header; + header.index_kind = f2::IndexKind::SymphonyQg; + header.metric = metric_type_ == METRIC_IP ? f2::Metric::InnerProduct : f2::Metric::L2; + header.dim = dim_; + header.padded_dim = padded_dim_; + header.code_bits = static_cast(quantization_bits_); + header.raw_type = quantization_bits_ == 0 ? f2::RawType::Float32 : f2::RawType::None; + header.id_bits = 32; + if (seed_known_) { + header.seed = seed_; + } + header.num_points = max_elements_; + + std::vector rotator( + rabitqlib::detail::rotator_blob_bytes(rotator_type_, dim_, padded_dim_) + ); + rabitqlib::detail::encode_rotator_blob( + *rotator_, rotator_type_, dim_, seed(), rotator.data() + ); + std::array params{}; + f2::put_u64(params.data(), max_degree_); + f2::put_u64(params.data() + 8, entry_point_id_); + f2::put_u64(params.data() + 16, ef_construction_); + uint32_t init = 0; + if (initialization_.has_value()) { + init = *initialization_ == QGInitialization::PiPNN ? kInitPiPNN : kInitRandom; + } + f2::put_u32(params.data() + 24, init); + + const auto bytes = [](const auto& buffer) { + return + [&buffer](f2::SectionSink& sink) { sink.write(buffer.data(), buffer.size()); }; + }; + std::vector sections = { + {f2::kRotatorSection, f2::kSectionRequired, rotator.size(), bytes(rotator)}, + {kParamsSection, + f2::kSectionRequired | f2::kSectionChecksummed, + params.size(), + bytes(params)}, + }; if (quantization_bits_ != 0) { - output.write(reinterpret_cast(&kFormatMagic), sizeof(kFormatMagic)); - output.write( - reinterpret_cast(&kFormatVersion), sizeof(kFormatVersion) + sections.push_back( + {kCentroidSection, + f2::kSectionRequired, + padded_dim_ * sizeof(float), + [this](f2::SectionSink& sink) { + sink.write(centroid_.data(), padded_dim_ * sizeof(float)); + }} ); } + const size_t data_bytes = checked_multiply(max_elements_, record_bytes_); + sections.push_back( + {kRecordsSection, + f2::kSectionRequired, + data_bytes, + [this, data_bytes](f2::SectionSink& sink) { + sink.write(get_row_data(0), data_bytes); + }} + ); + f2::write_file(output, header, sections, RABITQ_LIBRARY_VERSION); + output.close(); +} - // Basic variants - output.write(reinterpret_cast(&max_elements_), sizeof(size_t)); - output.write(reinterpret_cast(&max_degree_), sizeof(size_t)); - output.write(reinterpret_cast(&dim_), sizeof(size_t)); - output.write(reinterpret_cast(&padded_dim_), sizeof(size_t)); - output.write(reinterpret_cast(&entry_point_id_), sizeof(PID)); - output.write(reinterpret_cast(&rotator_type_), sizeof(RotatorType)); - output.write(reinterpret_cast(&metric_type_), sizeof(MetricType)); - if (quantization_bits_ != 0) { - output.write( - reinterpret_cast(&quantization_bits_), sizeof(quantization_bits_) +void QuantizedGraph::read_format2(std::istream& input) { + namespace f2 = index_detail::format2; + const std::vector known_kinds = { + f2::kRotatorSection, kParamsSection, kCentroidSection, kRecordsSection}; + f2::Reader reader( + input, f2::IndexKind::SymphonyQg, known_kinds, [](const f2::Header& header) { + if (header.metric == f2::Metric::Cosine) { + throw f2::FormatError( + "QuantizedGraph index file uses the cosine metric, which this release " + "does not support" + ); + } + const bool raw = + header.code_bits == 0 && header.raw_type == f2::RawType::Float32; + const bool quantized = (header.code_bits == 4 || header.code_bits == 8) && + header.raw_type == f2::RawType::None; + if (header.id_bits != 32 || (!raw && !quantized)) { + throw f2::FormatError( + "QuantizedGraph index file stores neither raw vectors nor 4- or 8-bit " + "codes with 32-bit IDs" + ); + } + if (header.seed.has_value() && + *header.seed > std::numeric_limits::max()) { + throw f2::FormatError("QuantizedGraph index file has a seed above 32 bits"); + } + } + ); + const f2::Header& header = reader.header(); + metric_type_ = header.metric == f2::Metric::InnerProduct ? METRIC_IP : METRIC_L2; + dim_ = static_cast(header.dim); + padded_dim_ = static_cast(header.padded_dim); + quantization_bits_ = header.code_bits; + max_elements_ = static_cast(header.num_points); + seed_known_ = header.seed.has_value(); + seed_ = static_cast(header.seed.value_or(0)); + + const auto require = [&reader](uint32_t kind, std::optional size) { + const f2::Section* section = reader.find(kind); + if (section == nullptr) { + throw f2::FormatError( + "QuantizedGraph index file has no section kind " + std::to_string(kind) + ); + } + if (size.has_value() && section->size != *size) { + throw f2::FormatError( + "QuantizedGraph index file section kind " + std::to_string(kind) + " has " + + std::to_string(section->size) + " bytes; expected " + std::to_string(*size) + ); + } + return section; + }; + const auto read_all = [&reader](const f2::Section& section) { + std::vector bytes(static_cast(section.size)); + reader.read(section, 0, bytes.data(), bytes.size()); + return bytes; + }; + + const std::vector blob = read_all(*require(f2::kRotatorSection, std::nullopt)); + rabitqlib::detail::DecodedRotator rotator; + try { + rotator = rabitqlib::detail::decode_rotator_blob(blob.data(), blob.size()); + } catch (const std::invalid_argument& error) { + throw f2::FormatError( + std::string("QuantizedGraph index file has an invalid rotator (") + + error.what() + ")" + ); + } + if (rotator.dim != dim_ || rotator.padded_dim != padded_dim_) { + throw f2::FormatError( + "QuantizedGraph index file rotator does not match its dimensions" ); - output.write( - reinterpret_cast(centroid_.data()), - static_cast(padded_dim_ * sizeof(float)) + } + rotator_type_ = rotator.type; + + const std::vector params = read_all(*require(kParamsSection, kParamsBytes)); + const uint64_t max_degree = f2::get_u64(params.data()); + const uint64_t entry_point = f2::get_u64(params.data() + 8); + const uint32_t init = f2::get_u32(params.data() + 24); + if (max_degree > max_elements_ || entry_point >= max_elements_ || init > kInitRandom || + f2::get_u32(params.data() + 28) != 0) { + throw f2::FormatError("QuantizedGraph index file has invalid graph parameters"); + } + max_degree_ = static_cast(max_degree); + entry_point_id_ = static_cast(entry_point); + ef_construction_ = static_cast(f2::get_u64(params.data() + 16)); + if (init != 0) { + initialization_ = + init == kInitPiPNN ? QGInitialization::PiPNN : QGInitialization::Random; + } + try { + validate_configuration(); + } catch (const std::invalid_argument& error) { + throw f2::FormatError( + std::string("QuantizedGraph index file is invalid: ") + error.what() ); } - // Data - output.write( - get_row_data(0), - static_cast(checked_multiply(max_elements_, record_bytes_)) - ); + initialize_layout(); + const size_t data_bytes = checked_multiply(max_elements_, record_bytes_); + const f2::Section* centroid = nullptr; + if (quantization_bits_ != 0) { + centroid = require(kCentroidSection, padded_dim_ * sizeof(float)); + } else if (reader.find(kCentroidSection) != nullptr) { + throw f2::FormatError( + "QuantizedGraph index file has a centroid but stores raw vectors" + ); + } + const f2::Section* records = require(kRecordsSection, data_bytes); - // Rotator - rotator_->save(output); + initialize(false); + rotator_ = std::move(rotator.rotator); + if (centroid != nullptr) { + centroid_.resize(padded_dim_); + reader.open(*centroid).read(centroid_.data(), padded_dim_ * sizeof(float)); + } + reader.open(*records).read(get_row_data(0), data_bytes); + read_only_ = reader.read_only(); + for (PID source = 0; source < max_elements_; ++source) { + const auto neighbors = get_neighbors(source); + for (size_t i = 0; i < max_degree_; ++i) { + if (neighbors[i] >= max_elements_) { + throw std::runtime_error("QuantizedGraph neighbor ID is invalid"); + } + } + } +} - output.flush(); - output.close(); +std::optional QuantizedGraph::seed() const { + return seed_known_ ? std::optional(seed_) : std::nullopt; +} + +std::optional QuantizedGraph::ef_construction() const { + return ef_construction_ != 0 ? std::optional(ef_construction_) : std::nullopt; +} + +std::optional QuantizedGraph::initialization() const { + return initialization_; } void QuantizedGraph::load(const char* filename) { @@ -324,6 +509,17 @@ void QuantizedGraph::load(const char* filename) { if (!input.is_open()) { throw std::runtime_error("QuantizedGraph cannot open file"); } + if (index_detail::format2::has_magic( + input, index_detail::format2::IndexKind::SymphonyQg + )) { + QuantizedGraph loaded; + loaded.read_format2(input); + // ef_search is a runtime search setting rather than persisted index state. + loaded.default_ef_search_ = default_ef_search_; + loaded.ready_ = true; + *this = std::move(loaded); + return; + } auto read_exact = [&](void* destination, size_t bytes, const char* field) { if (bytes > static_cast(std::numeric_limits::max())) { diff --git a/src/index/qg_builder.cpp b/src/index/qg_builder.cpp index 470a2f78..4163f474 100644 --- a/src/index/qg_builder.cpp +++ b/src/index/qg_builder.cpp @@ -79,6 +79,11 @@ QGBuilder::QGBuilder( throw std::invalid_argument("QuantizedGraph initialization is invalid"); } initialization_ = init; + // The graph records its construction settings for its index file. Rebuilding replaces + // whatever unknown features a loaded file described. + qg_.ef_construction_ = ef_construction; + qg_.initialization_ = init; + qg_.read_only_ = false; reset(data); } diff --git a/tests/common/format2_file.hpp b/tests/common/format2_file.hpp index 99a29587..5c432532 100644 --- a/tests/common/format2_file.hpp +++ b/tests/common/format2_file.hpp @@ -18,9 +18,11 @@ namespace rabitq_test { namespace format2 = rabitqlib::index_detail::format2; -// Every IVF-RaBitQ section kind, for reading back an IVF file in full. +// Every section kind of each index, for reading back a file in full. inline const std::vector kIvfSections = { 1, 256, 257, 258, 259, 260, 261, 262, 263}; +inline const std::vector kHnswSections = {1, 512, 513, 514, 515, 516}; +inline const std::vector kQgSections = {1, 768, 769, 770}; // A whole format-2 file: the header and each known section with its bytes, in file order. struct Format2Contents { diff --git a/tests/fixtures/persistence/README.md b/tests/fixtures/persistence/README.md index 74da83f0..1273ab8f 100644 --- a/tests/fixtures/persistence/README.md +++ b/tests/fixtures/persistence/README.md @@ -29,8 +29,9 @@ v1 headers around the frozen historical payload, unknown versions, and invalid padding. That header test is distinct from release-produced fixtures. The small IVF-RaBitQ fixtures use Flat centroid routing. The [v0.8.3](v0.8.3/README.md) -set adds the last layout before index format 2, including Flat RaBitQ and HNSW routing -with its `.hnsw` sidecar. Historical auto-selected HNSW routing at 20,000 clusters is +set adds the last layouts before index format 2: IVF-RaBitQ with Flat RaBitQ and HNSW +routing (with its `.hnsw` sidecar), HNSW-RaBitQ at 1, 5 and 9 bits, and SymphonyQG raw, +4- and 8-bit with FHT/Kac and matrix rotation. Historical auto-selected HNSW routing at 20,000 clusters is not represented by fixtures; Python tests cover it with headers built around the v0.8.3 layout. diff --git a/tests/fixtures/persistence/v0.8.3/generate_graphs.cpp b/tests/fixtures/persistence/v0.8.3/generate_graphs.cpp index 8b023493..abe912cb 100644 --- a/tests/fixtures/persistence/v0.8.3/generate_graphs.cpp +++ b/tests/fixtures/persistence/v0.8.3/generate_graphs.cpp @@ -78,7 +78,9 @@ int main(int argc, char** argv) { const std::string name = c.name; hnsw::HierarchicalNSW index(kCount, kDim, c.total_bits, 8, 64, 42, c.metric); std::vector assigned = clusters; - index.construct(kClusters, centroids.data(), kCount, data.data(), assigned.data(), 1); + index.construct( + kClusters, centroids.data(), kCount, data.data(), assigned.data(), 1 + ); const PID removed = 0; index.remove(&removed, 1); index.save((directory / (name + ".index")).string().c_str()); @@ -107,14 +109,20 @@ int main(int argc, char** argv) { }; for (const auto& c : qg_cases) { const std::string name = c.name; - symqg::QuantizedGraph index(kCount, kDim, 32, c.metric, c.rotator, c.bits, 42); - symqg::QGBuilder builder(index, 64, data.data(), 1, symqg::QGInitialization::PiPNN, 42); + symqg::QuantizedGraph index( + kCount, kDim, 32, c.metric, c.rotator, c.bits, 42 + ); + symqg::QGBuilder builder( + index, 64, data.data(), 1, symqg::QGInitialization::PiPNN, 42 + ); builder.build(); index.save((directory / (name + ".index")).string().c_str()); std::vector ids(kQueries * kTopK); std::vector distances(ids.size()); index.set_ef(64); - index.search_batch(queries.data(), kQueries, kTopK, ids.data(), distances.data(), 1); + index.search_batch( + queries.data(), kQueries, kTopK, ids.data(), distances.data(), 1 + ); write_expected(name, ids, distances); } return 0; diff --git a/tests/python/format2_file.py b/tests/python/format2_file.py index 341b6812..8887df5e 100644 --- a/tests/python/format2_file.py +++ b/tests/python/format2_file.py @@ -14,6 +14,10 @@ IVF_CODES = 261 IVF_RERANK = 262 IVF_IDS = 263 +HNSW_PARAMS = 512 +QG_PARAMS = 768 +QG_CENTROID = 769 +QG_RECORDS = 770 def sections(data: bytes) -> dict[int, tuple[int, int]]: diff --git a/tests/python/test_historical_persistence.py b/tests/python/test_historical_persistence.py index 4af803a0..cf9b1225 100644 --- a/tests/python/test_historical_persistence.py +++ b/tests/python/test_historical_persistence.py @@ -169,3 +169,35 @@ def test_v083_ivf_loads_and_migrates_without_loss(name, initializer, tmp_path): assert sidecar == original_sidecar.read_bytes() else: assert sidecar is None and not original_sidecar.exists() + + +# The last HNSW-RaBitQ and SymphonyQG layouts before format 2. +V083_GRAPHS = ["hnsw1_l2", "hnsw5_ip", "hnsw9_l2", "qg0_ip", "qg4_l2", "qg8_ip"] +V083_GRAPHS += ["qg4_matrix_l2"] + + +@pytest.mark.parametrize("name", V083_GRAPHS) +def test_v083_graphs_load_and_migrate(name, tmp_path): + path = FIXTURES / "v0.8.3" / f"{name}.index" + lines = path.with_suffix(".expected").read_text().splitlines() + count, dim, nq, k = map(int, lines[0].split()) + queries = np.fromstring(lines[1], sep=" ", dtype=np.float32).reshape(nq, dim) + results = np.loadtxt(lines[2:]) + ids = results[:, 0].astype(np.uint32).reshape(nq, k) + distances = results[:, 1].astype(np.float32).reshape(nq, k) + cls = HnswIndex if name.startswith("hnsw") else SymqgIndex + + original = cls.load(str(path)) + assert original.seed is None # earlier layouts do not record it + if cls is SymqgIndex: + assert (original.ef_construction, original.init) == (None, None) + destination = tmp_path / "migrated.index" + original.save(str(destination)) + assert destination.read_bytes()[:8] == format2_file.MAGIC + for index in (original, cls.load(str(destination))): + assert index.num_stored_points == count + assert index.num_points == count - (1 if cls is HnswIndex else 0) + assert index.seed is None + actual_ids, actual_distances = index.search(queries, k, ef=64) + np.testing.assert_array_equal(actual_ids, ids) + np.testing.assert_allclose(actual_distances, distances, rtol=2e-4, atol=2e-4) diff --git a/tests/python/test_hnsw.py b/tests/python/test_hnsw.py index bf612c6d..24e2d8b5 100644 --- a/tests/python/test_hnsw.py +++ b/tests/python/test_hnsw.py @@ -3,6 +3,7 @@ import re from pathlib import Path +import format2_file import numpy as np import pytest from conftest import DIM, N_CLUSTERS, N_QUERIES, N_VECTORS, brute_force_knn, recall_at_k @@ -205,7 +206,7 @@ def test_save_rejects_unopenable_destination(built_hnsw, tmp_path): @pytest.mark.skipif(not Path("/dev/full").exists(), reason="/dev/full is unavailable") def test_save_reports_write_or_close_failure(built_hnsw): - with pytest.raises(RuntimeError, match="HNSW failed to write index file"): + with pytest.raises(RuntimeError, match="HNSW index file cannot be written"): built_hnsw.save("/dev/full") @@ -227,9 +228,15 @@ def test_save_load_roundtrip(built_hnsw, query_data, tmp_path): def test_load_rejects_count_larger_than_capacity(built_hnsw, tmp_path): path = tmp_path / "hnsw-invalid-count.index" built_hnsw.save(str(path)) - with path.open("r+b") as index_file: - index_file.write((1).to_bytes(np.dtype(np.uintp).itemsize, byteorder="little")) - with pytest.raises(RuntimeError, match="HNSW"): + # The capacity is the first field of the graph parameters, which are checksummed. + payload = bytearray(path.read_bytes()) + offset, _ = format2_file.sections(payload)[format2_file.HNSW_PARAMS] + payload[offset : offset + 8] = (1).to_bytes(8, "little") + format2_file.reseal(payload) + path.write_bytes(payload) + with pytest.raises( + RuntimeError, match="^HNSW index file has invalid graph parameters$" + ): HnswIndex.load(str(path)) @@ -557,7 +564,7 @@ def test_legacy_padding_round_trip_and_add(tmp_path): ) path = tmp_path / "migrated.index" index.save(str(path)) - assert int.from_bytes(path.read_bytes()[24:32], "little") == 128 + assert format2_file.padded_dim(path.read_bytes()) == 128 loaded = HnswIndex.load(str(path)) actual = loaded.search(query, k=3, ef=8) np.testing.assert_array_equal(actual[0], ids) diff --git a/tests/python/test_portable_indexes.py b/tests/python/test_portable_indexes.py index 29019aad..24309632 100644 --- a/tests/python/test_portable_indexes.py +++ b/tests/python/test_portable_indexes.py @@ -52,18 +52,8 @@ def check_index_round_trip(tmp_path, metric, dim, kind, bits): path = tmp_path / "portable.index" index.save(str(path)) payload = path.read_bytes() - if kind == "ivf": - assert payload[:8] == format2_file.MAGIC - multiple = 32 - stored_padded = format2_file.padded_dim(payload) - elif kind == "qg": - multiple = 32 - offset = (12 if bits else 0) + 3 * np.dtype(np.uintp).itemsize - stored_padded = int.from_bytes(payload[offset : offset + 8], "little") - else: - multiple = 32 - stored_padded = int.from_bytes(payload[24:32], "little") - assert stored_padded == (dim + multiple - 1) // multiple * multiple + assert payload[:8] == format2_file.MAGIC + assert format2_file.padded_dim(payload) == (dim + 31) // 32 * 32 loaded = type(index).load(str(path)) assert loaded.dim == dim assert loaded.metric == metric @@ -116,18 +106,15 @@ def test_rejects_development_only_padding(tmp_path, kind, bits): check_index_round_trip(tmp_path, "l2", 65, kind, bits) path = tmp_path / "portable.index" payload = bytearray(path.read_bytes()) - offset = {"ivf": 56, "hnsw": 24, "qg": 12 + 3 * np.dtype(np.uintp).itemsize}[kind] # 80 is a valid pad16 dimension but unsupported by the pad32 kernels. - payload[offset : offset + 8] = (80).to_bytes(8, "little") - if kind == "ivf": - format2_file.reseal(payload) + payload[56:64] = (80).to_bytes(8, "little") + format2_file.reseal(payload) path.write_bytes(payload) cls = {"ivf": IvfIndex, "hnsw": HnswIndex, "qg": SymqgIndex}[kind] - message = { - "ivf": "IVF index file has padded dimension 80, which must be a multiple of " - "32 from the dimension 65 to 65536", - "hnsw": "HNSW index file is invalid or truncated", - "qg": "QuantizedGraph file has an invalid padded dimension", - }[kind] + component = {"ivf": "IVF", "hnsw": "HNSW", "qg": "QuantizedGraph"}[kind] + message = ( + f"{component} index file has padded dimension 80, which must be a multiple of " + "32 from the dimension 65 to 65536" + ) with pytest.raises(RuntimeError, match=f"^{message}$"): cls.load(str(path)) diff --git a/tests/python/test_symqg.py b/tests/python/test_symqg.py index 220acd7b..b1be4f6e 100644 --- a/tests/python/test_symqg.py +++ b/tests/python/test_symqg.py @@ -3,8 +3,8 @@ import inspect import os import re -import struct +import format2_file import numpy as np import pytest from conftest import DIM, N_QUERIES, N_VECTORS, brute_force_knn, recall_at_k @@ -313,7 +313,7 @@ def test_save_load_roundtrip(built_symqg, query_data, tmp_path): @pytest.mark.parametrize("bits", [4, 8]) @pytest.mark.parametrize("metric", ["l2", "ip"]) -def test_code_only_build_preserves_v1_format(bits, metric, tmp_path): +def test_code_only_build_saves_the_specified_layout(bits, metric, tmp_path): rng = np.random.default_rng(851) data = rng.normal(size=(65, 65)).astype(np.float32) queries = rng.normal(size=(3, 65)).astype(np.float32) @@ -323,12 +323,19 @@ def test_code_only_build_preserves_v1_format(bits, metric, tmp_path): path = tmp_path / "code_only.index" index.save(str(path)) payload = path.read_bytes() - assert struct.unpack_from(" #include #include +#include #include #include #include @@ -82,6 +83,8 @@ TEST_P(HistoricalPersistenceTest, LoadsSearchesResavesAndPreservesStateAfterFail const auto exercise = [&](auto& index, const auto& search) { const size_t removed = name.find("qg") == 0 ? 0 : 1; const auto check_counts = [&] { + // Earlier layouts do not record the seed, and migrating them invents none. + EXPECT_FALSE(index.seed().has_value()); EXPECT_EQ(index.num_stored_points(), count); EXPECT_EQ(index.num_points(), count - removed); EXPECT_GE(index.max_elements(), index.num_stored_points()); @@ -94,20 +97,16 @@ TEST_P(HistoricalPersistenceTest, LoadsSearchesResavesAndPreservesStateAfterFail search(); compare(); index.save(output.string().c_str()); - const bool format2 = name.find("ivf") == 0; // the indexes that save format 2 - if (format2) { - EXPECT_EQ(read_bytes(output).substr(0, 8), "RABQINDX"); - } + // Every index saves format 2. + EXPECT_EQ(read_bytes(output).substr(0, 8), "RABQINDX"); index.load(output.string().c_str()); check_counts(); search(); compare(); - if (format2) { - // Reading format 2 back and saving again reproduces the file. - const std::string saved = read_bytes(output); - index.save(output.string().c_str()); - EXPECT_TRUE(read_bytes(output) == saved); - } + // Reading format 2 back and saving again reproduces the file. + const std::string saved = read_bytes(output); + index.save(output.string().c_str()); + EXPECT_TRUE(read_bytes(output) == saved); std::filesystem::resize_file(output, std::filesystem::file_size(output) - 1); EXPECT_THROW(index.load(output.string().c_str()), std::runtime_error); check_counts(); @@ -140,6 +139,9 @@ TEST_P(HistoricalPersistenceTest, LoadsSearchesResavesAndPreservesStateAfterFail symqg::QuantizedGraph index; exercise(index, [&] { EXPECT_EQ(index.padded_dim(), expected_padded_dim); + // Earlier layouts do not record the construction settings either. + EXPECT_FALSE(index.ef_construction().has_value()); + EXPECT_FALSE(index.initialization().has_value()); index.set_ef(64); index.search_batch(queries.data(), nq, k, ids.data(), distances.data(), 1); }); @@ -200,6 +202,73 @@ INSTANTIATE_TEST_SUITE_P( ), [](const auto& info) { return info.param.name; } ); +// The checks of the SymphonyQG layout before format 2, on a copy of its v0.8.3 file. +TEST(HistoricalPersistenceTest, RejectsMalformedEarlierGraphFilesWithoutChangingTarget) { + namespace fs = std::filesystem; + const fs::path fixture = fs::path(RABITQ_TEST_FIXTURE_ROOT) / "v0.8.3" / "qg4_l2.index"; + const fs::path path = + fs::temp_directory_path() / rabitq_test::process_unique("historical_qg_malformed"); + symqg::QuantizedGraph target(65, 64, 32, METRIC_IP); + const auto load_patched = [&](std::streamoff offset, size_t value) { + fs::copy_file(fixture, path, fs::copy_options::overwrite_existing); + { + std::fstream file(path, std::ios::binary | std::ios::in | std::ios::out); + ASSERT_TRUE(file.is_open()); + file.seekp(offset); + file.write(reinterpret_cast(&value), sizeof(value)); + ASSERT_TRUE(file.good()); + } + target.load(path.string().c_str()); + }; + // After the magic and version: point count, degree, dimension, padded dimension. + constexpr std::streamoff kPointCountOffset = sizeof(uint64_t) + sizeof(uint32_t); + constexpr std::streamoff kPaddedDimensionOffset = + kPointCountOffset + 3 * sizeof(size_t); + try { + load_patched(kPaddedDimensionOffset, 100); + ADD_FAILURE() << "an invalid padded dimension was accepted"; + } catch (const std::runtime_error& error) { + EXPECT_STREQ(error.what(), "QuantizedGraph file has an invalid padded dimension"); + } + EXPECT_THROW( + load_patched(kPointCountOffset, std::numeric_limits::max()), + std::invalid_argument + ); + EXPECT_EQ(target.num_vertices(), 65U); + EXPECT_EQ(target.metric_type(), METRIC_IP); + fs::remove(path); +} + +// The checks of the HNSW-RaBitQ layout before format 2, on a copy of its v0.8.3 file. +TEST(HistoricalPersistenceTest, RejectsMalformedEarlierHnswFilesWithoutChangingTarget) { + namespace fs = std::filesystem; + const fs::path fixture = + fs::path(RABITQ_TEST_FIXTURE_ROOT) / "v0.8.3" / "hnsw5_ip.index"; + const fs::path path = fs::temp_directory_path() / + rabitq_test::process_unique("historical_hnsw_malformed"); + hnsw::HierarchicalNSW target; + target.load(fixture.string().c_str()); + const auto expect_rejected = [&](const auto& damage) { + fs::copy_file(fixture, path, fs::copy_options::overwrite_existing); + damage(); + try { + target.load(path.string().c_str()); + ADD_FAILURE() << "a damaged file was accepted"; + } catch (const std::runtime_error& error) { + EXPECT_STREQ(error.what(), "HNSW index file is invalid or truncated"); + } + EXPECT_EQ(target.num_stored_points(), 130U); + }; + // The capacity is the first field. + expect_rejected([&] { + std::fstream file(path, std::ios::binary | std::ios::in | std::ios::out); + const size_t capacity = 1; + file.write(reinterpret_cast(&capacity), sizeof(capacity)); + }); + expect_rejected([&] { fs::resize_file(path, fs::file_size(path) - 1); }); + fs::remove(path); +} + // Files before format 2 keep the HNSW routing graph in .hnsw; loading reads it // from there, through the same path conversion as the index file. TEST(HistoricalPersistenceTest, LoadsTheHnswSidecarOfEarlierLayouts) { diff --git a/tests/unit/rabitqlib/index/hnsw_test.cpp b/tests/unit/rabitqlib/index/hnsw_test.cpp index 8835a2c4..815c098f 100644 --- a/tests/unit/rabitqlib/index/hnsw_test.cpp +++ b/tests/unit/rabitqlib/index/hnsw_test.cpp @@ -21,6 +21,7 @@ #include #include +#include "format2_file.hpp" #include "rabitqlib/clustering/rabitqkmeans.hpp" #include "rabitqlib/core/encoder.hpp" #include "rabitqlib/core/metric.hpp" @@ -906,7 +907,7 @@ TEST_F(HnswSaveTest, ReportsWriteOrCloseFailure) { index_.save("/dev/full"); FAIL() << "Saving to a full destination must fail"; } catch (const std::runtime_error& error) { - EXPECT_STREQ(error.what(), "HNSW failed to write index file"); + EXPECT_STREQ(error.what(), "HNSW index file cannot be written"); } } @@ -925,25 +926,188 @@ TEST_F(HnswSaveTest, SuccessfulSavePreservesSearchAfterLoading) { TEST_F(HnswSaveTest, RejectsPointCountLargerThanCapacity) { index_.save(path_.c_str()); - { - std::fstream file(path_, std::ios::binary | std::ios::in | std::ios::out); - const size_t invalid_capacity = 1; - file.write( - reinterpret_cast(&invalid_capacity), sizeof(invalid_capacity) - ); - ASSERT_TRUE(file.good()); - } + // The capacity is the first field of the graph parameters. + std::string params = rabitq_test::read_format2_section( + path_, rabitq_test::format2::IndexKind::Hnsw, rabitq_test::kHnswSections, 512 + ); + rabitq_test::format2::put_u64(reinterpret_cast(params.data()), 1); + rabitq_test::replace_format2_section( + path_, + rabitq_test::format2::IndexKind::Hnsw, + rabitq_test::kHnswSections, + 512, + params + ); HierarchicalNSW loaded; try { loaded.load(path_.c_str()); FAIL() << "Invalid HNSW point count must be rejected"; } catch (const std::runtime_error& error) { - EXPECT_NE(std::string(error.what()).find("HNSW"), std::string::npos); + EXPECT_STREQ(error.what(), "HNSW index file has invalid graph parameters"); + } +} + +// A graph with upper layers, saved in format 2 with a known seed. +class HnswFormat2Test : public ::testing::Test { + protected: + static constexpr size_t kDim = 64; + static constexpr size_t kCount = 200; + static constexpr size_t kSeed = 9; + std::vector data_{std::vector(kCount * kDim)}; + std::vector centroids_{std::vector(2 * kDim)}; + std::vector cluster_ids_{std::vector(kCount)}; + HierarchicalNSW index_{kCount, kDim, 5, 4, 16, kSeed, METRIC_IP}; + std::string path_; + + void SetUp() override { + path_ = ::testing::TempDir() + "rabitq_hnsw_format2_" + + ::testing::UnitTest::GetInstance()->current_test_info()->name() + ".index"; + std::mt19937 generator(3); + std::normal_distribution normal; + for (auto& value : data_) { + value = normal(generator); + } + for (size_t i = 0; i < kCount; ++i) { + cluster_ids_[i] = static_cast(i % 2); + } + index_.construct( + 2, centroids_.data(), kCount, data_.data(), cluster_ids_.data(), 1, false + ); + const PID removed = 5; + index_.remove(&removed, 1); + index_.save(path_.c_str()); + } + + void TearDown() override { std::remove(path_.c_str()); } + + [[nodiscard]] rabitq_test::Format2Contents contents() const { + return rabitq_test::read_format2_file( + path_, rabitq_test::format2::IndexKind::Hnsw, rabitq_test::kHnswSections + ); + } + + void expect_rejected( + const rabitq_test::Format2Contents& damaged, const std::string& message + ) { + rabitq_test::write_format2_file(path_, damaged); + HierarchicalNSW loaded; + try { + loaded.load(path_.c_str()); + ADD_FAILURE() << "accepted: " << message; + } catch (const std::runtime_error& error) { + EXPECT_EQ(std::string(error.what()), message); + } } +}; + +TEST_F(HnswFormat2Test, RoundTripsTheGraphAndRecordsTheSeed) { + const auto saved = contents(); + EXPECT_EQ(saved.header.index_kind, rabitq_test::format2::IndexKind::Hnsw); + EXPECT_EQ(saved.header.metric, rabitq_test::format2::Metric::InnerProduct); + EXPECT_EQ(saved.header.code_bits, 5U); + EXPECT_EQ(saved.header.num_points, kCount); + ASSERT_TRUE(saved.header.seed.has_value()); + EXPECT_EQ(*saved.header.seed, kSeed); + std::vector kinds; + for (const auto& entry : saved.sections) { + kinds.push_back(entry.first.kind); + } + EXPECT_EQ(kinds, (std::vector{1, 512, 513, 514, 515, 516})); + EXPECT_FALSE(rabitq_test::Format2Contents(saved).bytes(516).empty()); // upper layers + + HierarchicalNSW loaded; + loaded.load(path_.c_str()); + EXPECT_EQ(loaded.seed(), std::optional(kSeed)); + EXPECT_EQ(loaded.num_points(), kCount - 1); + EXPECT_EQ(loaded.max_elements(), kCount); + EXPECT_EQ( + loaded.search(data_.data(), 4, 5, 32, 1), index_.search(data_.data(), 4, 5, 32, 1) + ); + const std::string first = rabitq_test::Format2Contents(saved).bytes(514); + loaded.save(path_.c_str()); + EXPECT_EQ(contents().bytes(514), first); +} + +TEST_F(HnswFormat2Test, RejectsContentsThatDoNotFitTheGraph) { + const auto saved = contents(); + auto damaged = saved; + damaged.remove(515); + expect_rejected(damaged, "HNSW index file has no section kind 515"); + + damaged = saved; + damaged.bytes(516).pop_back(); + const size_t upper_bytes = rabitq_test::Format2Contents(saved).bytes(516).size(); + expect_rejected( + damaged, + "HNSW index file section kind 516 has " + std::to_string(upper_bytes - 1) + + " bytes; expected " + std::to_string(upper_bytes) + ); + + damaged = saved; + rabitq_test::format2::put_u32( + reinterpret_cast(damaged.bytes(515).data()), 1000 + ); + expect_rejected(damaged, "HNSW index file has a point above the maximum level"); + + damaged = saved; + rabitq_test::format2::put_u32( + reinterpret_cast(damaged.bytes(512).data()) + 60, 1 + ); + expect_rejected(damaged, "HNSW index file has invalid graph parameters"); + + damaged = saved; + damaged.header.metric = rabitq_test::format2::Metric::Cosine; + expect_rejected( + damaged, + "HNSW index file uses the cosine metric, which this release does not support" + ); + + damaged = saved; + damaged.bytes(515).resize(4 * (kCount - 1)); + expect_rejected( + damaged, + "HNSW index file section kind 515 has " + std::to_string(4 * (kCount - 1)) + + " bytes; expected " + std::to_string(4 * kCount) + ); + + // A neighbor beyond the stored points, in the first base-layer record. + damaged = saved; + char* record = damaged.bytes(514).data(); + rabitq_test::format2::put_u32(reinterpret_cast(record), 1); // one neighbor + rabitq_test::format2::put_u32(reinterpret_cast(record) + 4, kCount); + expect_rejected(damaged, "HNSW index file is invalid or truncated"); +} + +TEST_F(HnswFormat2Test, UnknownReadOnlyFeaturesAllowSearchButNoChanges) { + rabitq_test::patch_format2_header(path_, 24, 1, 8); // an unknown ro_compat bit + HierarchicalNSW loaded; + ASSERT_NO_THROW(loaded.load(path_.c_str())); + EXPECT_EQ( + loaded.search(data_.data(), 2, 5, 32, 1), index_.search(data_.data(), 2, 5, 32, 1) + ); + const auto expect_refused = [](const auto& operation, const std::string& name) { + try { + operation(); + ADD_FAILURE() << name << " was allowed"; + } catch (const std::logic_error& error) { + EXPECT_EQ( + std::string(error.what()), + "HNSW index was loaded from a file with features this release cannot " + "update, so it cannot " + + name + ); + } + }; + const PID label = 7; + expect_refused([&] { loaded.add(data_.data(), 1, &label, false, 1); }, "add"); + expect_refused([&] { loaded.resize(kCount + 10); }, "resize"); + expect_refused([&] { loaded.remove(&label, 1); }, "remove"); + expect_refused([&] { loaded.save((path_ + ".copy").c_str()); }, "save"); + EXPECT_EQ(loaded.num_points(), kCount - 1); } -TEST_F(HnswSaveTest, RejectsTruncatedRotatorWithoutLosingExistingIndex) { +TEST_F(HnswSaveTest, RejectsTruncatedFileWithoutLosingExistingIndex) { index_.save(path_.c_str()); HierarchicalNSW loaded; loaded.load(path_.c_str()); @@ -952,7 +1116,7 @@ TEST_F(HnswSaveTest, RejectsTruncatedRotatorWithoutLosingExistingIndex) { std::filesystem::resize_file(path_, std::filesystem::file_size(path_) - 1); try { loaded.load(path_.c_str()); - FAIL() << "Truncated HNSW rotator must be rejected"; + FAIL() << "A truncated HNSW file must be rejected"; } catch (const std::runtime_error& error) { EXPECT_NE(std::string(error.what()).find("HNSW"), std::string::npos); } diff --git a/tests/unit/rabitqlib/index/ivf_test.cpp b/tests/unit/rabitqlib/index/ivf_test.cpp index 25a027e4..5632641b 100644 --- a/tests/unit/rabitqlib/index/ivf_test.cpp +++ b/tests/unit/rabitqlib/index/ivf_test.cpp @@ -53,6 +53,8 @@ struct IvfTestAccess { static std::unique_ptr>& rotator(IVF& index) { return index.rotator_; } + static std::optional& seed(IVF& index) { return index.seed_; } + // Rotated centroid as the initializer routes with it. static std::vector centroid(const IVF& index, PID i) { const float* centroid = index.initializer_->centroid(i); @@ -1808,13 +1810,15 @@ TEST(IvfTrainTest, SetCentroidsAndAddMatchConstructByteForByte) { << "metric=" << (metric == METRIC_L2 ? "l2" : "ip") << " bits=" << bits ); IVF constructed(kCount, kDynDim, kDynNumClusters, bits, metric); - // Give the point-count constructor the rotation the seed selects. + // Give the point-count constructor the rotation the seed selects, and the + // seed itself, which the file records. IvfTestAccess::rotator(constructed) .reset( choose_rotator( kDynDim, RotatorType::FhtKacRotator, constructed.padded_dim(), kSeed ) ); + IvfTestAccess::seed(constructed) = kSeed; constructed.construct( points.data(), centroids.data(), assigned.data(), false, 1 ); diff --git a/tests/unit/rabitqlib/index/qg_test.cpp b/tests/unit/rabitqlib/index/qg_test.cpp index a35edca4..e1da7afd 100644 --- a/tests/unit/rabitqlib/index/qg_test.cpp +++ b/tests/unit/rabitqlib/index/qg_test.cpp @@ -23,6 +23,7 @@ #include #include +#include "format2_file.hpp" #include "rabitqlib/core/data_layout.hpp" #include "rabitqlib/core/metric.hpp" #include "rabitqlib/core/rotator.hpp" @@ -1398,6 +1399,199 @@ TEST(QgConfigurationTest, RejectsOutOfRangeEntryPoint) { EXPECT_EQ(graph.entry_point(), 32U); } +TEST(QgFormat2Test, RecordsTheSeedAndConstructionSettings) { + constexpr size_t kCount = 65, kDim = 64, kDegree = 32; + std::vector data(kCount * kDim); + std::mt19937 generator(5); + std::normal_distribution normal; + for (auto& value : data) { + value = normal(generator); + } + const rabitq_test::TempFile path_file("rabitq_qg_format2.index"); + const std::string& path = path_file.path(); + for (const size_t bits : {0UL, 4UL, 8UL}) { + SCOPED_TRACE(bits); + QuantizedGraph source( + kCount, kDim, kDegree, METRIC_L2, RotatorType::FhtKacRotator, bits, 7 + ); + QGBuilder builder(source, 40, data.data(), 1, QGInitialization::Random, 7); + builder.build(); + source.save(path.c_str()); + + const auto saved = rabitq_test::read_format2_file( + path, rabitq_test::format2::IndexKind::SymphonyQg, rabitq_test::kQgSections + ); + EXPECT_EQ(saved.header.code_bits, bits); + EXPECT_EQ( + saved.header.raw_type, + bits == 0 ? rabitq_test::format2::RawType::Float32 + : rabitq_test::format2::RawType::None + ); + std::vector kinds; + for (const auto& entry : saved.sections) { + kinds.push_back(entry.first.kind); + } + const std::vector expected_kinds = + bits == 0 ? std::vector{1, 768, 770} + : std::vector{1, 768, 769, 770}; + EXPECT_EQ(kinds, expected_kinds); + + QuantizedGraph loaded; + loaded.load(path.c_str()); + EXPECT_EQ(loaded.seed(), std::optional(7)); + EXPECT_EQ(loaded.ef_construction(), std::optional(40)); + EXPECT_EQ(loaded.initialization(), std::optional(QGInitialization::Random)); + EXPECT_EQ(loaded.quantization_bits(), bits); + std::vector expected(5), actual(5); + std::vector expected_distances(5), actual_distances(5); + source.set_ef(64); + loaded.set_ef(64); + source.search(data.data(), 5, expected.data(), expected_distances.data()); + loaded.search(data.data(), 5, actual.data(), actual_distances.data()); + EXPECT_EQ(actual, expected); + EXPECT_EQ(actual_distances, expected_distances); + + // A graph without a builder or a recorded seed stores them as unknown. + QuantizedGraph unknown; + EXPECT_FALSE(unknown.seed().has_value()); + EXPECT_FALSE(unknown.ef_construction().has_value()); + EXPECT_FALSE(unknown.initialization().has_value()); + } +} + +TEST(QgFormat2Test, RejectsContentsThatDoNotFitTheGraph) { + constexpr size_t kCount = 65, kDim = 64, kDegree = 32; + std::vector data(kCount * kDim, 0.5F); + for (size_t i = 0; i < data.size(); ++i) { + data[i] = static_cast((i * 13) % 17) / 8.0F; + } + const rabitq_test::TempFile path_file("rabitq_qg_format2_reject.index"); + const std::string& path = path_file.path(); + QuantizedGraph source( + kCount, kDim, kDegree, METRIC_IP, RotatorType::FhtKacRotator, 4, 3 + ); + QGBuilder builder(source, 40, data.data(), 1, QGInitialization::PiPNN, 3); + builder.build(); + source.save(path.c_str()); + const auto saved = rabitq_test::read_format2_file( + path, rabitq_test::format2::IndexKind::SymphonyQg, rabitq_test::kQgSections + ); + QuantizedGraph target; + target.load(path.c_str()); + const auto expect_rejected = + [&](const char* name, auto edit, const std::string& message) { + SCOPED_TRACE(name); + auto damaged = saved; + edit(damaged); + rabitq_test::write_format2_file(path, damaged); + try { + target.load(path.c_str()); + ADD_FAILURE() << "accepted"; + } catch (const std::runtime_error& error) { + EXPECT_EQ(std::string(error.what()), message); + } + EXPECT_EQ(target.num_vertices(), kCount); + }; + const auto params = [](auto& c) { + return reinterpret_cast(c.bytes(768).data()); + }; + expect_rejected( + "missing centroid", + [](auto& c) { c.remove(769); }, + "QuantizedGraph index file has no section kind 769" + ); + expect_rejected( + "raw vectors with a centroid", + [](auto& c) { + c.header.code_bits = 0; + c.header.raw_type = rabitq_test::format2::RawType::Float32; + }, + "QuantizedGraph index file has a centroid but stores raw vectors" + ); + expect_rejected( + "unknown initialization", + [&](auto& c) { rabitq_test::format2::put_u32(params(c) + 24, 3); }, + "QuantizedGraph index file has invalid graph parameters" + ); + expect_rejected( + "entry point out of range", + [&](auto& c) { rabitq_test::format2::put_u64(params(c) + 8, kCount); }, + "QuantizedGraph index file has invalid graph parameters" + ); + expect_rejected( + "degree bound", + [&](auto& c) { rabitq_test::format2::put_u64(params(c), 48); }, + "QuantizedGraph index file is invalid: QuantizedGraph degree bound must be a " + "positive multiple of 32" + ); + expect_rejected( + "seed above 32 bits", + [](auto& c) { c.header.seed = uint64_t{1} << 40; }, + "QuantizedGraph index file has a seed above 32 bits" + ); + expect_rejected( + "short records", + [](auto& c) { c.bytes(770).pop_back(); }, + "QuantizedGraph index file section kind 770 has " + + std::to_string(rabitq_test::Format2Contents(saved).bytes(770).size() - 1) + + " bytes; expected " + + std::to_string(rabitq_test::Format2Contents(saved).bytes(770).size()) + ); + expect_rejected( + "neighbor out of range", + [](auto& c) { + std::string& records = c.bytes(770); + // The last 4 bytes are the last vertex's last neighbor ID. + rabitq_test::format2::put_u32( + reinterpret_cast(records.data()) + records.size() - 4, kCount + ); + }, + "QuantizedGraph neighbor ID is invalid" + ); + expect_rejected( + "1-bit codes", + [](auto& c) { c.header.code_bits = 1; }, + "QuantizedGraph index file stores neither raw vectors nor 4- or 8-bit codes with " + "32-bit IDs" + ); +} + +TEST(QgFormat2Test, UnknownReadOnlyFeaturesAllowSearchUntilRebuilt) { + constexpr size_t kCount = 65, kDim = 64, kDegree = 32; + std::vector data(kCount * kDim); + for (size_t i = 0; i < data.size(); ++i) { + data[i] = static_cast((i * 7) % 23) / 8.0F; + } + const rabitq_test::TempFile path_file("rabitq_qg_format2_read_only.index"); + const std::string& path = path_file.path(); + QuantizedGraph source( + kCount, kDim, kDegree, METRIC_L2, RotatorType::FhtKacRotator, 8, 2 + ); + QGBuilder(source, 40, data.data(), 1, QGInitialization::PiPNN, 2).build(); + source.save(path.c_str()); + rabitq_test::patch_format2_header(path, 24, 1, 8); // an unknown ro_compat bit + + QuantizedGraph loaded; + ASSERT_NO_THROW(loaded.load(path.c_str())); + std::vector ids(5); + std::vector distances(5); + loaded.set_ef(64); + ASSERT_NO_THROW(loaded.search(data.data(), 5, ids.data(), distances.data())); + try { + loaded.save(path.c_str()); + ADD_FAILURE() << "save was allowed"; + } catch (const std::logic_error& error) { + EXPECT_STREQ( + error.what(), + "QuantizedGraph was loaded from a file with features this release cannot " + "update, so it cannot save" + ); + } + // A builder replaces the data the unknown features described. + QGBuilder(loaded, 40, data.data(), 1, QGInitialization::PiPNN, 2).build(); + EXPECT_NO_THROW(loaded.save(path.c_str())); +} + TEST(QgPersistenceTest, RejectsMalformedPayloadWithoutChangingTarget) { constexpr size_t kCount = 33; constexpr size_t kDim = 64; @@ -1419,38 +1613,30 @@ TEST(QgPersistenceTest, RejectsMalformedPayloadWithoutChangingTarget) { EXPECT_EQ(target.num_vertices(), 65U); EXPECT_EQ(target.metric_type(), METRIC_IP); - source.save(path.c_str()); - { - std::fstream file(path, std::ios::binary | std::ios::in | std::ios::out); - ASSERT_TRUE(file.is_open()); - constexpr std::streamoff kPaddedDimensionOffset = - sizeof(uint64_t) + sizeof(uint32_t) + (3 * sizeof(size_t)); - file.seekp(kPaddedDimensionOffset); - const size_t invalid_padded_dim = kDim * 2; - file.write( - reinterpret_cast(&invalid_padded_dim), sizeof(invalid_padded_dim) - ); - ASSERT_TRUE(file.good()); - } - EXPECT_THROW(target.load(path.c_str()), std::runtime_error); - EXPECT_EQ(target.num_vertices(), 65U); - EXPECT_EQ(target.metric_type(), METRIC_IP); - - source.save(path.c_str()); - { - std::fstream file(path, std::ios::binary | std::ios::in | std::ios::out); - ASSERT_TRUE(file.is_open()); - constexpr std::streamoff kPointCountOffset = sizeof(uint64_t) + sizeof(uint32_t); - file.seekp(kPointCountOffset); - const size_t invalid_point_count = std::numeric_limits::max(); - file.write( - reinterpret_cast(&invalid_point_count), sizeof(invalid_point_count) - ); - ASSERT_TRUE(file.good()); - } - EXPECT_THROW(target.load(path.c_str()), std::invalid_argument); - EXPECT_EQ(target.num_vertices(), 65U); - EXPECT_EQ(target.metric_type(), METRIC_IP); + // Format-2 header fields that do not fit the graph; the earlier layout's are covered + // by the historical persistence tests. + const auto expect_rejected = + [&](size_t offset, uint64_t value, const std::string& message) { + source.save(path.c_str()); + rabitq_test::patch_format2_header(path, offset, value, sizeof(uint64_t)); + try { + target.load(path.c_str()); + ADD_FAILURE() << "accepted: " << message; + } catch (const std::runtime_error& error) { + EXPECT_EQ(std::string(error.what()), message); + } + EXPECT_EQ(target.num_vertices(), 65U); + EXPECT_EQ(target.metric_type(), METRIC_IP); + }; + expect_rejected( + 56, kDim * 2, "QuantizedGraph index file rotator does not match its dimensions" + ); + expect_rejected( + 88, + uint64_t{1} << 32, + "QuantizedGraph index file is invalid: QuantizedGraph point count exceeds the " + "search-buffer ID limit" + ); if (std::filesystem::exists("/dev/full")) { EXPECT_THROW(source.save("/dev/full"), std::ios_base::failure); From d2b25a1e3d67dbd2cddc0e36a6e949e2036368ee Mon Sep 17 00:00:00 2001 From: gouyt13clear Date: Sat, 7 Nov 2026 18:32:26 +0800 Subject: [PATCH 08/11] test(symqg): capture the point count for MSVC MSVC requires a lambda to capture a local constexpr variable it reads, which GCC and Clang do not. --- tests/unit/rabitqlib/index/qg_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/rabitqlib/index/qg_test.cpp b/tests/unit/rabitqlib/index/qg_test.cpp index e1da7afd..7ece24ca 100644 --- a/tests/unit/rabitqlib/index/qg_test.cpp +++ b/tests/unit/rabitqlib/index/qg_test.cpp @@ -1539,7 +1539,7 @@ TEST(QgFormat2Test, RejectsContentsThatDoNotFitTheGraph) { ); expect_rejected( "neighbor out of range", - [](auto& c) { + [&](auto& c) { std::string& records = c.bytes(770); // The last 4 bytes are the last vertex's last neighbor ID. rabitq_test::format2::put_u32( From cdc2dbde4048ecef319ad49cddda037d0a91bdef Mon Sep 17 00:00:00 2001 From: gouyt13clear Date: Sat, 7 Nov 2026 21:19:27 +0800 Subject: [PATCH 09/11] feat(index): raise rabitqlib.FormatError for rejected index files Every index load throws rabitqlib::FormatError, a std::runtime_error in the new public header rabitqlib/index/format_error.hpp, for a truncated, corrupt, mismatched or unsupported file in format 2 and every earlier layout. Failing to open a file stays a plain std::runtime_error. Python raises it as rabitqlib.FormatError, a subclass of both ValueError and RuntimeError, so existing except RuntimeError handlers keep working. rabitqlib.quant.FormatError becomes the same class; it was a ValueError only. An earlier-layout IVF file with an invalid metric byte and an earlier-layout SymphonyQG file with invalid settings threw std::invalid_argument; both now throw FormatError. This resolves the spec's open question on the Python error type. --- docs/development.md | 4 + docs/docs/index/usage.md | 6 + docs/docs/rabitq/quant_python.md | 2 +- docs/docs/releases/unreleased.md | 7 +- docs/formats.md | 5 +- include/rabitqlib/index/format_error.hpp | 19 + .../index/ivf/detail/hnsw_sidecar.hpp | 29 +- include/rabitqlib/index/ivf/initializer.hpp | 15 +- python_bindings/__init__.py | 2 + python_bindings/__init__.pyi | 4 + python_bindings/quant.py | 5 +- python_bindings/quant.pyi | 4 +- python_bindings/quant_bindings.cpp | 24 +- python_bindings/rabitq_bindings.cpp | 37 ++ src/index/format2.hpp | 9 +- src/index/hnsw.cpp | 5 +- src/index/ivf.cpp | 335 +++++++++--------- src/index/ivf_initializer.cpp | 5 +- src/index/qg.cpp | 23 +- tests/python/test_historical_persistence.py | 30 +- tests/python/test_quant_editor_support.py | 21 +- tests/python/test_quant_rotator.py | 1 + .../index/historical_persistence_test.cpp | 60 +++- 23 files changed, 431 insertions(+), 221 deletions(-) create mode 100644 include/rabitqlib/index/format_error.hpp diff --git a/docs/development.md b/docs/development.md index b7276203..3977d182 100644 --- a/docs/development.md +++ b/docs/development.md @@ -551,6 +551,10 @@ keep their provenance and hashes, and do not regenerate them with the current wr Index files are trusted input (see the usage guide): validate structure and sizes so that corrupt or mismatched files are reported before allocation, without treating a crafted file as a supported input. +Readers throw `rabitqlib::FormatError` (`include/rabitqlib/index/format_error.hpp`) for +any truncated, corrupt, mismatched or unsupported content, including errors raised by +shared validators while loading; only a file that cannot be opened throws a plain +`std::runtime_error`. Python maps it to `rabitqlib.FormatError`; rejection tests assert it. Do not silently reinterpret an old file. Add a magic/version discriminator, use fixed-width serialized fields for new formats (earlier layouts store several `size_t` fields; keep their readers unchanged), validate sizes before allocation, check every diff --git a/docs/docs/index/usage.md b/docs/docs/index/usage.md index a06f3328..869f98a8 100644 --- a/docs/docs/index/usage.md +++ b/docs/docs/index/usage.md @@ -178,6 +178,12 @@ Compatibility is format-specific; preserving old readers' ability to open new files is not guaranteed. Consult each index's format documentation before downgrading the library. +A truncated, corrupt, mismatched or unsupported file raises `rabitqlib.FormatError`, +a subclass of both `ValueError` and `RuntimeError`, so `except RuntimeError` +handlers still catch it. A file that cannot be opened raises a plain `RuntimeError`. +C++ loaders throw `rabitqlib::FormatError` from `rabitqlib/index/format_error.hpp`, +a subclass of `std::runtime_error`. + Index files are trusted input. Loaders validate structure and reject truncated, inconsistent, or unsupported files so that mistakes are reported, but they are not a security boundary: a crafted file can make `load` allocate its stored diff --git a/docs/docs/rabitq/quant_python.md b/docs/docs/rabitq/quant_python.md index 12df7922..0716cc8c 100644 --- a/docs/docs/rabitq/quant_python.md +++ b/docs/docs/rabitq/quant_python.md @@ -1,7 +1,7 @@ # Python quantization building blocks !!! warning "Provisional" - `rabitqlib.quant` is provisional in the sense of [PEP 411](https://peps.python.org/pep-0411/): any minor release may change or remove part of its API, with a note in the release notes, until it is declared stable. Byte formats are versioned from the first release, and bytes of an unknown version raise `quant.FormatError` instead of giving wrong results. + `rabitqlib.quant` is provisional in the sense of [PEP 411](https://peps.python.org/pep-0411/): any minor release may change or remove part of its API, with a note in the release notes, until it is declared stable. Byte formats are versioned from the first release, and bytes of an unknown version raise `quant.FormatError` instead of giving wrong results. It is `rabitqlib.FormatError`, which index loads also raise, a subclass of both `ValueError` and `RuntimeError`. `rabitqlib.quant` exposes the pieces the indexes are built from: rotation, quantization, correction factors, packed codes and distance estimation. Use it to store RaBitQ codes in your own database or file format and to score them with your own selection logic. For a ready-made index, use [IVF-RaBitQ](../index/ivf.md), [HNSW-RaBitQ](../index/hnsw.md) or [SymphonyQG](../index/qg.md) instead. diff --git a/docs/docs/releases/unreleased.md b/docs/docs/releases/unreleased.md index 55721e0a..de3b59c5 100644 --- a/docs/docs/releases/unreleased.md +++ b/docs/docs/releases/unreleased.md @@ -14,14 +14,19 @@ - Loading a format-2 file keeps the `seed` of every index, and `ef_construction` and `init` of SymphonyQG. They were `None` after `load`, and still are for files from releases up to 0.8. - With a recorded seed, `cp.seed` after `load` follows it as for a new index: the seed for IVF-RaBitQ, its low 32 bits for HNSW-RaBitQ. It was the k-means default. +- A new exception, `rabitqlib.FormatError`, subclasses both `ValueError` and `RuntimeError`: + - Every index `load` raises it for a truncated, corrupt, mismatched or unsupported file, in format 2 and in every earlier layout. These were `RuntimeError`, which still catches them. A file that cannot be opened still raises a plain `RuntimeError`. + - `quant.FormatError` is now the same class. It was a subclass of `ValueError` only, which still catches it. - Messages for damaged or mismatched files change: - - A format-2 file raises `RuntimeError` naming the problem, for example `IVF index file is 6916 bytes, but its sections end at 6852` for trailing bytes. + - A format-2 file names the problem, for example `IVF index file is 6916 bytes, but its sections end at 6852` for trailing bytes. - Errors in an HNSW routing graph name the graph rather than the sidecar: `Truncated HNSW centroid graph` and `HNSW centroid graph changed while loading` replace their `sidecar` forms. - A missing sidecar of an earlier file still raises `Cannot open HNSW centroid sidecar`. - A failed HNSW-RaBitQ write raises `HNSW index file cannot be written` instead of `HNSW failed to write index file`. ## C++ changes +- Loading an index throws `rabitqlib::FormatError`, a subclass of `std::runtime_error` in the new header `rabitqlib/index/format_error.hpp`, for a damaged, mismatched or unsupported file. Failing to open the file still throws `std::runtime_error`. A SymphonyQG file of an earlier layout with invalid settings threw `std::invalid_argument`; it now throws `FormatError`. + - `ivf::Initializer` and its subclasses: - `save(std::ofstream&, const char*)` becomes `write(std::ostream&)`, and `load(std::ifstream&, const char*)` becomes `read(std::istream&, uint64_t bytes)`. - The HNSW initializer no longer writes a sidecar itself. diff --git a/docs/formats.md b/docs/formats.md index cb5a57fb..7de3e37f 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -1,6 +1,6 @@ # Byte formats -This is the specification of the byte formats `rabitqlib.quant` reads and writes. It is shared with the planned index format revision, which will add its own blob kinds. While `rabitqlib.quant` is provisional, a release may stop reading an old format version; reading it then raises `quant.FormatError` instead of returning wrong results. A promise that every released version stays readable comes with stabilisation. +This is the specification of the byte formats `rabitqlib.quant` reads and writes. It is shared with the planned index format revision, which will add its own blob kinds. While `rabitqlib.quant` is provisional, a release may stop reading an old format version; reading it then raises `rabitqlib.FormatError` (also `quant.FormatError`) instead of returning wrong results. A promise that every released version stays readable comes with stabilisation. Every format has a version. A change to any layout is a new version, never an edit of an existing one. The golden files in `tests/python/fixtures/quant/` freeze each version and are never regenerated. @@ -354,6 +354,8 @@ A reader checks, in order, and validates every size before allocating. The loade Steps 1–7 belong to the container; step 8 and the index's header rules in step 4 belong to each index. Readers read through the stream buffer, so the stream's exception mask does not matter, and need seekable input. +A file that fails any of these checks, or a damaged file of an [earlier layout](#earlier-layouts), raises `rabitqlib::FormatError` in C++, a subclass of `std::runtime_error`, and `rabitqlib.FormatError` in Python, a subclass of both `ValueError` and `RuntimeError`. `rabitqlib.quant` raises the same class for blobs. A file that cannot be opened raises a plain `std::runtime_error` (Python `RuntimeError`). + Index files remain trusted input (see [Index file compatibility](docs/index/usage.md#index-file-compatibility)). These checks report truncated, corrupt or mismatched files; they do not make crafted files safe. ### Writing @@ -373,5 +375,4 @@ Readers dispatch on the leading bytes: `"RABQINDX"`, then each index's legacy ma ### Open questions -- Python error type for a rejected file: index loads raise `RuntimeError` today, `rabitqlib.quant` raises `FormatError` (a `ValueError`). Proposed: one top-level `rabitqlib.FormatError` that subclasses both `ValueError` and `RuntimeError`, raised by `load` and by `rabitqlib.quant`, so existing `except RuntimeError` handlers keep working. - Whether the header should be a [blob](#blobs) kind instead of its own magic. Proposed: its own magic, as above. A blob checksums its whole body, which is too costly for large index files, so index files get their own container and nest blobs in sections. diff --git a/include/rabitqlib/index/format_error.hpp b/include/rabitqlib/index/format_error.hpp new file mode 100644 index 00000000..104a7374 --- /dev/null +++ b/include/rabitqlib/index/format_error.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace rabitqlib { + +/** + * @brief An index file whose contents are invalid, or which this release cannot read. + * + * Loading any index throws it for a damaged, mismatched or unsupported file in every + * layout; failing to open the file is a plain std::runtime_error. Python raises it as + * rabitqlib.FormatError, a subclass of both ValueError and RuntimeError. + */ +class FormatError : public std::runtime_error { + public: + using std::runtime_error::runtime_error; +}; + +} // namespace rabitqlib diff --git a/include/rabitqlib/index/ivf/detail/hnsw_sidecar.hpp b/include/rabitqlib/index/ivf/detail/hnsw_sidecar.hpp index 0b3f07b6..080a42ef 100644 --- a/include/rabitqlib/index/ivf/detail/hnsw_sidecar.hpp +++ b/include/rabitqlib/index/ivf/detail/hnsw_sidecar.hpp @@ -10,6 +10,7 @@ #include #include +#include "rabitqlib/index/format_error.hpp" #include "rabitqlib/third/hnswlib/hnswlib.h" namespace rabitqlib::ivf::detail { @@ -27,7 +28,7 @@ inline void validate_hnsw_graph( ) { const std::streampos begin = input.tellg(); if (begin == std::streampos(std::streamoff(-1))) { - throw std::runtime_error("Cannot read HNSW centroid graph"); + throw FormatError("Cannot read HNSW centroid graph"); } // Stay within the graph's bytes, as a budget rather than by querying the position: // in an index file, other data follows them. @@ -35,7 +36,7 @@ inline void validate_hnsw_graph( const auto read = [&](auto& value) { if (remaining < static_cast(sizeof(value)) || !input.read(reinterpret_cast(&value), sizeof(value))) { - throw std::runtime_error("Truncated HNSW centroid graph"); + throw FormatError("Truncated HNSW centroid graph"); } remaining -= static_cast(sizeof(value)); }; @@ -60,12 +61,12 @@ inline void validate_hnsw_graph( read(ef_construction); if (count != expected_count || count == 0 || capacity < count || count > std::numeric_limits::max()) { - throw std::runtime_error("HNSW centroid count does not match IVF cluster count"); + throw FormatError("HNSW centroid count does not match IVF cluster count"); } constexpr size_t kMaxM = 10000; // hnswlib caps M at this value. if (m < 2 || m > kMaxM || max_m != m || max_m0 != 2 * m || !std::isfinite(mult) || mult <= 0 || ef_construction < m) { - throw std::runtime_error("Invalid HNSW centroid graph parameters"); + throw FormatError("Invalid HNSW centroid graph parameters"); } const size_t upper_layer_links_bytes = sizeof(hnswlib::linklistsizeint) + max_m * sizeof(hnswlib::tableint); @@ -75,13 +76,13 @@ inline void validate_hnsw_graph( data_offset > label_offset || label_offset > level0_record_bytes || label_offset - data_offset != vector_bytes || level0_record_bytes - label_offset != sizeof(hnswlib::labeltype)) { - throw std::runtime_error("HNSW centroid layout does not match IVF dimension"); + throw FormatError("HNSW centroid layout does not match IVF dimension"); } if (max_level < 0 || entry >= count) { - throw std::runtime_error("Invalid HNSW centroid entry point or maximum level"); + throw FormatError("Invalid HNSW centroid entry point or maximum level"); } if (count > static_cast(remaining) / level0_record_bytes) { - throw std::runtime_error("Truncated HNSW centroid base records"); + throw FormatError("Truncated HNSW centroid base records"); } const auto level0_records_bytes = static_cast(count * level0_record_bytes); @@ -93,7 +94,7 @@ inline void validate_hnsw_graph( read(link_bytes); if (link_bytes % upper_layer_links_bytes != 0 || link_bytes > static_cast(remaining)) { - throw std::runtime_error("Invalid HNSW centroid upper-link length"); + throw FormatError("Invalid HNSW centroid upper-link length"); } const size_t levels = link_bytes / upper_layer_links_bytes; highest_level = std::max(highest_level, levels); @@ -109,7 +110,7 @@ inline void validate_hnsw_graph( } if (!input || remaining != 0 || highest_level != static_cast(max_level) || entry_level != highest_level) { - throw std::runtime_error("Invalid HNSW centroid level structure"); + throw FormatError("Invalid HNSW centroid level structure"); } input.clear(); input.seekg(begin); @@ -120,11 +121,11 @@ inline void validate_hnsw_centroid_graph(const hnswlib::HierarchicalNSW& if (graph.enterpoint_node_ >= count || graph.maxlevel_ < 0 || graph.element_levels_[graph.enterpoint_node_] != graph.maxlevel_ || graph.num_deleted_ != 0) { - throw std::runtime_error("Invalid HNSW centroid entry point or deletion state"); + throw FormatError("Invalid HNSW centroid entry point or deletion state"); } for (size_t i = 0; i < count; ++i) { if (graph.element_levels_[i] < 0 || graph.element_levels_[i] > graph.maxlevel_) { - throw std::runtime_error("Invalid HNSW centroid node level"); + throw FormatError("Invalid HNSW centroid node level"); } for (int level = 0; level <= graph.element_levels_[i]; ++level) { const char* links = @@ -135,7 +136,7 @@ inline void validate_hnsw_centroid_graph(const hnswlib::HierarchicalNSW& uint16_t degree = 0; // The high two bytes hold flags, not the degree. std::memcpy(°ree, links, sizeof(degree)); if (degree > (level == 0 ? graph.maxM0_ : graph.maxM_)) { - throw std::runtime_error("Invalid HNSW centroid neighbor count"); + throw FormatError("Invalid HNSW centroid neighbor count"); } for (size_t j = 0; j < degree; ++j) { hnswlib::tableint neighbor = 0; @@ -145,9 +146,7 @@ inline void validate_hnsw_centroid_graph(const hnswlib::HierarchicalNSW& sizeof(neighbor) ); if (neighbor >= count || graph.element_levels_[neighbor] < level) { - throw std::runtime_error( - "Invalid HNSW centroid neighbor or target level" - ); + throw FormatError("Invalid HNSW centroid neighbor or target level"); } } } diff --git a/include/rabitqlib/index/ivf/initializer.hpp b/include/rabitqlib/index/ivf/initializer.hpp index aa18bddf..1104965a 100644 --- a/include/rabitqlib/index/ivf/initializer.hpp +++ b/include/rabitqlib/index/ivf/initializer.hpp @@ -17,6 +17,7 @@ #include "rabitqlib/core/metric.hpp" #include "rabitqlib/index/detail/parallel_for.hpp" +#include "rabitqlib/index/format_error.hpp" #include "rabitqlib/index/ivf/detail/hnsw_sidecar.hpp" #include "rabitqlib/index/ivf/types.hpp" // IWYU pragma: export #include "rabitqlib/index/types.hpp" @@ -66,7 +67,7 @@ class Initializer { // Write the routing data that read() restores. virtual void write(std::ostream& output) const = 0; // Read `bytes` bytes that write() wrote, from the current position of `input`. - // Data that does not fit this initializer raises std::runtime_error. + // Data that does not fit this initializer raises FormatError. virtual void read(std::istream& input, uint64_t bytes) = 0; }; inline Initializer::~Initializer() = default; @@ -131,13 +132,13 @@ class FlatInitializer : public Initializer { void read(std::istream& input, uint64_t bytes) override { if (bytes != sizeof(float) * dim_ * num_cluster_) { - throw std::runtime_error("IVF Flat initializer data has the wrong size"); + throw FormatError("IVF Flat initializer data has the wrong size"); } input.read( reinterpret_cast(centroids_.data()), static_cast(bytes) ); if (!input) { - throw std::runtime_error("IVF Flat initializer cannot be read"); + throw FormatError("IVF Flat initializer cannot be read"); } } }; @@ -314,10 +315,14 @@ class HNSWInitializer : public Initializer { graph.label_offset_ - graph.offsetData_ != space_->get_data_size() || graph.size_data_per_element_ - graph.label_offset_ != sizeof(hnswlib::labeltype)) { - throw std::runtime_error("HNSW centroid graph changed while loading"); + throw FormatError("HNSW centroid graph changed while loading"); } detail::validate_hnsw_centroid_graph(graph); - rebuild_label_lookup(); + try { + rebuild_label_lookup(); + } catch (const std::runtime_error& error) { + throw FormatError(error.what()); // stored labels that do not fit + } } ~HNSWInitializer() override = default; diff --git a/python_bindings/__init__.py b/python_bindings/__init__.py index b84c57a5..1c29aea8 100644 --- a/python_bindings/__init__.py +++ b/python_bindings/__init__.py @@ -9,6 +9,7 @@ from ._rabitqlib import ( FinalAssignmentMode, + FormatError, HnswIndex, IvfIndex, QGKMeansIterationStats, @@ -627,6 +628,7 @@ class RaBitQKMeans(_KMeans): __all__ = [ "FinalAssignmentMode", + "FormatError", "HnswIndex", "IvfIndex", "QGKMeans", diff --git a/python_bindings/__init__.pyi b/python_bindings/__init__.pyi index 3f44a278..ef5726d3 100644 --- a/python_bindings/__init__.pyi +++ b/python_bindings/__init__.pyi @@ -15,6 +15,7 @@ __version__: str __all__ = [ "FinalAssignmentMode", + "FormatError", "HnswIndex", "IvfIndex", "QGKMeans", @@ -35,6 +36,9 @@ _QGInit: TypeAlias = Literal["pipnn", "random"] _IntegerIds: TypeAlias = npt.NDArray[np.integer[Any]] | Sequence[int | np.integer[Any]] # pybind11 enums are not subclasses of enum.Enum. +class FormatError(ValueError, RuntimeError): + """A damaged or mismatched index file or serialized byte string, or one this release cannot read. A subclass of both ValueError and RuntimeError.""" + class FinalAssignmentMode: """Final centroid assignment policy. diff --git a/python_bindings/quant.py b/python_bindings/quant.py index c31cac2b..f11ff978 100644 --- a/python_bindings/quant.py +++ b/python_bindings/quant.py @@ -50,8 +50,9 @@ FASTSCAN_BLOCKS_VERSION = 1 """Version of the pack_fastscan_blocks layout.""" -# Classes report this module, so pickle and help() find them here. -for _public in (FormatError, Query, Rotator): +# Classes report this module, so pickle and help() find them here. FormatError is +# rabitqlib.FormatError, shared with index loading. +for _public in (Query, Rotator): _public.__module__ = __name__ del _public diff --git a/python_bindings/quant.pyi b/python_bindings/quant.pyi index 9cdbf2f6..b50851b6 100644 --- a/python_bindings/quant.pyi +++ b/python_bindings/quant.pyi @@ -8,6 +8,7 @@ from typing import Any, Literal, overload import numpy as np import numpy.typing as npt +from rabitqlib import FormatError as FormatError __all__ = [ "ESTIMATE_DTYPE", @@ -37,9 +38,6 @@ PACKED_CODES_VERSION: int FACTORS_VERSION: int FASTSCAN_BLOCKS_VERSION: int -class FormatError(ValueError): - """Corrupt or incompatible serialized bytes. A subclass of ValueError.""" - class Rotator: """A seeded rotation from the original dimension to the padded working space.""" diff --git a/python_bindings/quant_bindings.cpp b/python_bindings/quant_bindings.cpp index a713042d..a364262c 100644 --- a/python_bindings/quant_bindings.cpp +++ b/python_bindings/quant_bindings.cpp @@ -50,11 +50,14 @@ namespace py = pybind11; namespace rabitqlib::python_bindings { namespace { -// Raised for corrupt or incompatible serialized bytes; a ValueError in Python. +// Raised for corrupt or incompatible serialized bytes; rabitqlib.FormatError in Python. struct FormatError : std::invalid_argument { using std::invalid_argument::invalid_argument; }; +// rabitqlib.FormatError, owned by the parent module. +PyObject* quant_format_error_type = nullptr; + // The blob format and both rotation methods share the FHT/Kac limit. constexpr size_t kMaxRotatorDim = rotator_impl::FhtKacRotator::kMaxDim; constexpr size_t kMaxTotalBits = 9; @@ -1823,11 +1826,20 @@ void register_quant(py::module_& m) { "_quant", "Native backend of rabitqlib.quant. Import rabitqlib.quant instead of this module." ); - py::register_exception( - quant, "FormatError", py::module_::import("builtins").attr("ValueError") - ); - quant.attr("FormatError").attr("__doc__") = - "Corrupt or incompatible serialized bytes. A subclass of ValueError."; + // Rejected bytes raise rabitqlib.FormatError, which the parent module created. + quant.attr("FormatError") = m.attr("FormatError"); + quant_format_error_type = m.attr("FormatError").ptr(); + // pybind11 translators take std::exception_ptr by value. + // NOLINTNEXTLINE(performance-unnecessary-value-param) + py::register_exception_translator([](std::exception_ptr exception) { + try { + if (exception) { + std::rethrow_exception(exception); + } + } catch (const FormatError& error) { + PyErr_SetString(quant_format_error_type, error.what()); + } + }); quant.attr("FACTORS_DTYPE") = factors_dtype(); quant.attr("ESTIMATE_DTYPE") = estimate_dtype(); diff --git a/python_bindings/rabitq_bindings.cpp b/python_bindings/rabitq_bindings.cpp index c609ca06..bb4984fb 100644 --- a/python_bindings/rabitq_bindings.cpp +++ b/python_bindings/rabitq_bindings.cpp @@ -3,10 +3,20 @@ #include "rabitqlib/core/metric.hpp" #include "rabitqlib/core/rotator.hpp" +#include "rabitqlib/index/format_error.hpp" #include "rabitqlib/utils/tools.hpp" namespace py = pybind11; +namespace { +// rabitqlib.FormatError; the module keeps it alive for the interpreter's lifetime. +PyObject* format_error_type = nullptr; + +constexpr const char* kFormatErrorDoc = + "A damaged or mismatched index file or serialized byte string, or one this release " + "cannot read. A subclass of both ValueError and RuntimeError."; +} // namespace + // Forward declarations of registration functions implemented in other cpp files void register_hnsw(py::module_& m); void register_ivf(py::module_& m); @@ -18,6 +28,33 @@ PYBIND11_MODULE(_rabitqlib, m) { m.doc() = "RabitQ Python bindings combined module"; m.attr("__version__") = RABITQ_VERSION; + + // One exception for every rejected file or byte string, registered before the + // modules that raise it. + { + const py::object builtins = py::module_::import("builtins"); + const py::tuple bases = + py::make_tuple(builtins.attr("ValueError"), builtins.attr("RuntimeError")); + PyObject* type = PyErr_NewExceptionWithDoc( + "rabitqlib.FormatError", kFormatErrorDoc, bases.ptr(), nullptr + ); + if (type == nullptr) { + throw py::error_already_set(); + } + m.attr("FormatError") = py::reinterpret_steal(type); + format_error_type = type; + } + // pybind11 translators take std::exception_ptr by value. + // NOLINTNEXTLINE(performance-unnecessary-value-param) + py::register_exception_translator([](std::exception_ptr exception) { + try { + if (exception) { + std::rethrow_exception(exception); + } + } catch (const rabitqlib::FormatError& error) { + PyErr_SetString(format_error_type, error.what()); + } + }); m.def("_available_cpu_count", &rabitqlib::total_threads); m.def("_default_thread_count", &rabitqlib::default_threads); diff --git a/src/index/format2.hpp b/src/index/format2.hpp index 78b1a243..7bf2df90 100644 --- a/src/index/format2.hpp +++ b/src/index/format2.hpp @@ -13,17 +13,16 @@ #include #include +#include "rabitqlib/index/format_error.hpp" + // The index container of format 2, specified in docs/formats.md ("Index files, // format 2"). This layer reads and writes the header, the section table and section // bytes; each index defines its own sections and validates their contents. namespace rabitqlib::index_detail::format2 { // A file that is not a valid format-2 container, or uses something this release -// does not support. -class FormatError : public std::runtime_error { - public: - using std::runtime_error::runtime_error; -}; +// does not support: the library-wide index file error. +using FormatError = ::rabitqlib::FormatError; inline constexpr std::array kMagic = {'R', 'A', 'B', 'Q', 'I', 'N', 'D', 'X'}; inline constexpr uint32_t kVersion = 2; diff --git a/src/index/hnsw.cpp b/src/index/hnsw.cpp index 4e38ae31..a1756bec 100644 --- a/src/index/hnsw.cpp +++ b/src/index/hnsw.cpp @@ -39,6 +39,7 @@ #include "rabitqlib/index/detail/hnsw_dispatch.hpp" #include "rabitqlib/index/detail/parallel_for.hpp" #include "rabitqlib/index/detail/visited_pool.hpp" +#include "rabitqlib/index/format_error.hpp" #include "rabitqlib/index/hnsw/hnsw_quant.hpp" #include "rabitqlib/index/types.hpp" #include "rabitqlib/utils/input_checks.hpp" @@ -338,7 +339,7 @@ void HierarchicalNSW::load(const char* filename) { // The unversioned layout of releases up to 0.8. void HierarchicalNSW::read_legacy(std::ifstream& input) { auto invalid_file = []() -> void { - throw std::runtime_error("HNSW index file is invalid or truncated"); + throw FormatError("HNSW index file is invalid or truncated"); }; auto read_header = [&](auto& value) { input.read(reinterpret_cast(&value), sizeof(value)); @@ -655,7 +656,7 @@ void HierarchicalNSW::check_writable(const char* operation) const { void HierarchicalNSW::validate_graph() { const auto invalid = []() -> void { - throw std::runtime_error("HNSW index file is invalid or truncated"); + throw FormatError("HNSW index file is invalid or truncated"); }; const size_t element_count = num_stored_points_; for (size_t i = 0; i < element_count; ++i) { diff --git a/src/index/ivf.cpp b/src/index/ivf.cpp index f66f3d19..28c77d65 100644 --- a/src/index/ivf.cpp +++ b/src/index/ivf.cpp @@ -40,6 +40,7 @@ #include "rabitqlib/index/detail/buffer.hpp" #include "rabitqlib/index/detail/parallel_exception.hpp" #include "rabitqlib/index/detail/parallel_for.hpp" +#include "rabitqlib/index/format_error.hpp" #include "rabitqlib/index/ivf/cluster.hpp" #include "rabitqlib/index/ivf/initializer.hpp" #include "rabitqlib/index/training.hpp" @@ -231,10 +232,10 @@ IVF::IVF( , padded_dim_(dim) , num_clusters_(num_clusters) , raw_reranking_(total_bits == 32) + , seed_(seed) , ex_bits_(total_bits == 32 ? 0 : total_bits - 1) , rotator_type_(rotator_type) - , metric_type_(metric_type) - , seed_(seed) { + , metric_type_(metric_type) { configure(total_bits); rotator_.reset(choose_rotator(dim, rotator_type, padded_dim_, seed)); } @@ -1037,181 +1038,191 @@ void IVF::load(const char* filename) { return; } - // An earlier layout: RABQIDX1, the raw-vector v1 layout or the unversioned one. - IVF loaded; - input.exceptions(std::ios::failbit | std::ios::badbit); - input.seekg(0, std::ios::end); - const auto file_bytes = static_cast(input.tellg()); - input.seekg(0); - uint64_t magic = 0; - input.read(reinterpret_cast(&magic), sizeof(magic)); - const bool explicit_padding = magic == kFormatMagic; - if (explicit_padding) { - uint32_t version = 0, flags = 0; - uint64_t stored_padded_dim = 0; - input.read(reinterpret_cast(&version), sizeof(version)); - input.read(reinterpret_cast(&flags), sizeof(flags)); - input.read(reinterpret_cast(&stored_padded_dim), sizeof(stored_padded_dim)); - if (version != 1 && version != kFormatVersion) { - throw std::runtime_error("Unsupported IVF index version"); + // An earlier layout. Its reader lets the stream report truncation. + try { + // An earlier layout: RABQIDX1, the raw-vector v1 layout or the unversioned one. + IVF loaded; + input.exceptions(std::ios::failbit | std::ios::badbit); + input.seekg(0, std::ios::end); + const auto file_bytes = static_cast(input.tellg()); + input.seekg(0); + uint64_t magic = 0; + input.read(reinterpret_cast(&magic), sizeof(magic)); + const bool explicit_padding = magic == kFormatMagic; + if (explicit_padding) { + uint32_t version = 0, flags = 0; + uint64_t stored_padded_dim = 0; + input.read(reinterpret_cast(&version), sizeof(version)); + input.read(reinterpret_cast(&flags), sizeof(flags)); + input.read( + reinterpret_cast(&stored_padded_dim), sizeof(stored_padded_dim) + ); + if (version != 1 && version != kFormatVersion) { + throw FormatError("Unsupported IVF index version"); + } + if (flags > 1 || stored_padded_dim > std::numeric_limits::max()) { + throw FormatError("Invalid IVF index metadata"); + } + loaded.raw_reranking_ = flags == 1; + loaded.padded_dim_ = static_cast(stored_padded_dim); + if (version >= 2) { + uint32_t initializer = 0; + input.read(reinterpret_cast(&initializer), sizeof(initializer)); + if (initializer < static_cast(InitializerType::Flat) || + initializer > static_cast(InitializerType::HNSW)) { + throw FormatError("Invalid IVF initializer type in index file"); + } + loaded.initializer_type_ = static_cast(initializer); + } + } else if (magic == kRawFormatMagic) { + loaded.raw_reranking_ = true; + uint32_t version = 0; + input.read(reinterpret_cast(&version), sizeof(version)); + if (version != kRawFormatVersion) { + throw FormatError("Unsupported raw IVF index version"); + } + } else { + input.seekg(0); + } + + // Load metadata + input.read(reinterpret_cast(&loaded.max_elements_), sizeof(size_t)); + input.read(reinterpret_cast(&loaded.dim_), sizeof(size_t)); + input.read(reinterpret_cast(&loaded.num_clusters_), sizeof(size_t)); + input.read(reinterpret_cast(&loaded.ex_bits_), sizeof(size_t)); + input.read( + reinterpret_cast(&loaded.rotator_type_), sizeof(loaded.rotator_type_) + ); + input.read( + reinterpret_cast(&loaded.metric_type_), sizeof(loaded.metric_type_) + ); + // Missing routing metadata means a historical file. Its payload layout must + // follow the original threshold, independently of the current Auto policy. + if (loaded.initializer_type_ == InitializerType::Auto) { + loaded.initializer_type_ = loaded.num_clusters_ < kHistoricalHnswMinClusters + ? InitializerType::Flat + : InitializerType::HNSW; + } + if ((loaded.metric_type_ != METRIC_L2 && loaded.metric_type_ != METRIC_IP) || + loaded.max_elements_ == 0 || + loaded.max_elements_ > buffer::kSearchBufferMaxPointCount || loaded.dim_ == 0 || + loaded.num_clusters_ == 0 || + loaded.num_clusters_ > buffer::kSearchBufferMaxPointCount || + loaded.ex_bits_ > 8 || (loaded.raw_reranking_ && loaded.ex_bits_ != 0) || + loaded.dim_ > (std::numeric_limits::max() / 32) - 64) { + throw FormatError("Invalid IVF index metadata"); + } + if (!explicit_padding) { + // Both historical formats omit padded_dim and always used 64-coordinate blocks. + loaded.padded_dim_ = round_up_to_multiple(loaded.dim_, 64); } - if (flags > 1 || stored_padded_dim > std::numeric_limits::max()) { - throw std::runtime_error("Invalid IVF index metadata"); + if (loaded.padded_dim_ < loaded.dim_ || loaded.padded_dim_ % 32 != 0 || + loaded.padded_dim_ > (std::numeric_limits::max() / 32) - 64) { + throw FormatError("Invalid padded dimension in IVF index file"); } - loaded.raw_reranking_ = flags == 1; - loaded.padded_dim_ = static_cast(stored_padded_dim); - if (version >= 2) { - uint32_t initializer = 0; - input.read(reinterpret_cast(&initializer), sizeof(initializer)); - if (initializer < static_cast(InitializerType::Flat) || - initializer > static_cast(InitializerType::HNSW)) { - throw std::runtime_error("Invalid IVF initializer type in index file"); + // Bound every payload by the actual file before allocating or multiplying sizes. + size_t remaining = file_bytes - static_cast(input.tellg()); + const auto consume = [&remaining](size_t count, size_t bytes) { + if (bytes != 0 && count > remaining / bytes) { + throw FormatError("Invalid or truncated IVF index payload"); } - loaded.initializer_type_ = static_cast(initializer); + remaining -= count * bytes; + }; + consume(loaded.num_clusters_, sizeof(size_t)); + consume(loaded.max_elements_, sizeof(PID)); + consume(loaded.max_elements_, loaded.rerank_vector_bytes()); + if (loaded.rotator_type_ == RotatorType::MatrixRotator) { + consume(loaded.dim_, sizeof(float) * loaded.padded_dim_); + } else if (loaded.rotator_type_ == RotatorType::FhtKacRotator) { + consume(1, loaded.padded_dim_ / 2); + } else { + throw FormatError("Invalid IVF rotator type"); } - } else if (magic == kRawFormatMagic) { - loaded.raw_reranking_ = true; - uint32_t version = 0; - input.read(reinterpret_cast(&version), sizeof(version)); - if (version != kRawFormatVersion) { - throw std::runtime_error("Unsupported raw IVF index version"); + if (loaded.initializer_type_ != InitializerType::HNSW) { + consume(loaded.num_clusters_, sizeof(float) * loaded.padded_dim_); } - } else { - input.seekg(0); - } - - // Load metadata - input.read(reinterpret_cast(&loaded.max_elements_), sizeof(size_t)); - input.read(reinterpret_cast(&loaded.dim_), sizeof(size_t)); - input.read(reinterpret_cast(&loaded.num_clusters_), sizeof(size_t)); - input.read(reinterpret_cast(&loaded.ex_bits_), sizeof(size_t)); - input.read( - reinterpret_cast(&loaded.rotator_type_), sizeof(loaded.rotator_type_) - ); - input.read(reinterpret_cast(&loaded.metric_type_), sizeof(loaded.metric_type_)); - validate_metric_type(loaded.metric_type_); - // Missing routing metadata means a historical file. Its payload layout must - // follow the original threshold, independently of the current Auto policy. - if (loaded.initializer_type_ == InitializerType::Auto) { - loaded.initializer_type_ = loaded.num_clusters_ < kHistoricalHnswMinClusters - ? InitializerType::Flat - : InitializerType::HNSW; - } - if (loaded.max_elements_ == 0 || - loaded.max_elements_ > buffer::kSearchBufferMaxPointCount || loaded.dim_ == 0 || - loaded.num_clusters_ == 0 || - loaded.num_clusters_ > buffer::kSearchBufferMaxPointCount || loaded.ex_bits_ > 8 || - (loaded.raw_reranking_ && loaded.ex_bits_ != 0) || - loaded.dim_ > (std::numeric_limits::max() / 32) - 64) { - throw std::runtime_error("Invalid IVF index metadata"); - } - if (!explicit_padding) { - // Both historical formats omit padded_dim and always used 64-coordinate blocks. - loaded.padded_dim_ = round_up_to_multiple(loaded.dim_, 64); - } - if (loaded.padded_dim_ < loaded.dim_ || loaded.padded_dim_ % 32 != 0 || - loaded.padded_dim_ > (std::numeric_limits::max() / 32) - 64) { - throw std::runtime_error("Invalid padded dimension in IVF index file"); - } - // Bound every payload by the actual file before allocating or multiplying sizes. - size_t remaining = file_bytes - static_cast(input.tellg()); - const auto consume = [&remaining](size_t count, size_t bytes) { - if (bytes != 0 && count > remaining / bytes) { - throw std::runtime_error("Invalid or truncated IVF index payload"); + if (loaded.initializer_type_ == InitializerType::FlatRaBitQ) { + consume(loaded.padded_dim_, sizeof(float)); + consume( + div_round_up(loaded.num_clusters_, fastscan::kBatchSize), + loaded.batch_bytes() + ); } - remaining -= count * bytes; - }; - consume(loaded.num_clusters_, sizeof(size_t)); - consume(loaded.max_elements_, sizeof(PID)); - consume(loaded.max_elements_, loaded.rerank_vector_bytes()); - if (loaded.rotator_type_ == RotatorType::MatrixRotator) { - consume(loaded.dim_, sizeof(float) * loaded.padded_dim_); - } else if (loaded.rotator_type_ == RotatorType::FhtKacRotator) { - consume(1, loaded.padded_dim_ / 2); - } else { - throw std::runtime_error("Invalid IVF rotator type"); - } - if (loaded.initializer_type_ != InitializerType::HNSW) { - consume(loaded.num_clusters_, sizeof(float) * loaded.padded_dim_); - } - if (loaded.initializer_type_ == InitializerType::FlatRaBitQ) { - consume(loaded.padded_dim_, sizeof(float)); - consume( - div_round_up(loaded.num_clusters_, fastscan::kBatchSize), loaded.batch_bytes() - ); - } - // Load the number of points in each cluster - std::vector cluster_sizes(loaded.num_clusters_, 0); - input.read( - reinterpret_cast(cluster_sizes.data()), - static_cast(sizeof(size_t) * loaded.num_clusters_) - ); + // Load the number of points in each cluster + std::vector cluster_sizes(loaded.num_clusters_, 0); + input.read( + reinterpret_cast(cluster_sizes.data()), + static_cast(sizeof(size_t) * loaded.num_clusters_) + ); - size_t total = 0; - for (size_t size : cluster_sizes) { - if (size > loaded.max_elements_ - total) { - throw std::runtime_error("Invalid cluster counts in IVF index file"); + size_t total = 0; + for (size_t size : cluster_sizes) { + if (size > loaded.max_elements_ - total) { + throw FormatError("Invalid cluster counts in IVF index file"); + } + total += size; + consume(div_round_up(size, fastscan::kBatchSize), loaded.batch_bytes()); } - total += size; - consume(div_round_up(size, fastscan::kBatchSize), loaded.batch_bytes()); - } - if (total != loaded.max_elements_) { - throw std::runtime_error("Invalid cluster counts in IVF index file"); - } - loaded.rotator_ = rotator_impl::make_rotator_for_loading( - loaded.dim_, loaded.rotator_type_, loaded.padded_dim_ - ); - - // Load rotator - loaded.rotator_->load(input); - - // Load data: initializer payload, then all batch data, all ex data and all IDs, - // each in cluster order - loaded.allocate_storage(cluster_sizes); - if (loaded.initializer_type_ == InitializerType::HNSW) { - // These layouts kept the routing graph in a sidecar file beside the index. - const std::string sidecar = std::string(filename) + detail::kHnswSidecarSuffix; - std::ifstream graph( - io_impl::filesystem_path(sidecar), std::ios::binary | std::ios::ate - ); - if (!graph) { - throw std::runtime_error("Cannot open HNSW centroid sidecar"); + if (total != loaded.max_elements_) { + throw FormatError("Invalid cluster counts in IVF index file"); } - const auto graph_bytes = - static_cast(static_cast(graph.tellg())); - graph.seekg(0); - loaded.initializer_->read(graph, graph_bytes); - } else { - loaded.initializer_->read(input, loaded.flat_routing_bytes()); - } - for (size_t i = 0; i < loaded.num_clusters_; ++i) { - input.read( - loaded.cluster_storage_[i].batch_data(), - static_cast(loaded.batch_data_bytes(cluster_sizes[i])) + loaded.rotator_ = rotator_impl::make_rotator_for_loading( + loaded.dim_, loaded.rotator_type_, loaded.padded_dim_ ); - } - if (loaded.has_rerank_data()) { + + // Load rotator + loaded.rotator_->load(input); + + // Load data: initializer payload, then all batch data, all ex data and all IDs, + // each in cluster order + loaded.allocate_storage(cluster_sizes); + if (loaded.initializer_type_ == InitializerType::HNSW) { + // These layouts kept the routing graph in a sidecar file beside the index. + const std::string sidecar = std::string(filename) + detail::kHnswSidecarSuffix; + std::ifstream graph( + io_impl::filesystem_path(sidecar), std::ios::binary | std::ios::ate + ); + if (!graph) { + throw std::runtime_error("Cannot open HNSW centroid sidecar"); + } + const auto graph_bytes = + static_cast(static_cast(graph.tellg())); + graph.seekg(0); + loaded.initializer_->read(graph, graph_bytes); + } else { + loaded.initializer_->read(input, loaded.flat_routing_bytes()); + } for (size_t i = 0; i < loaded.num_clusters_; ++i) { input.read( - loaded.cluster_storage_[i].rerank_data(), - static_cast(loaded.rerank_data_bytes(cluster_sizes[i])) + loaded.cluster_storage_[i].batch_data(), + static_cast(loaded.batch_data_bytes(cluster_sizes[i])) ); } - } - for (size_t i = 0; i < loaded.num_clusters_; ++i) { - input.read( - reinterpret_cast(loaded.cluster_storage_[i].ids()), - static_cast(ids_bytes(cluster_sizes[i])) - ); - } - if (remaining != 0) { - throw std::runtime_error("IVF index file has unexpected trailing data"); - } - loaded.complete_load(cluster_sizes); + if (loaded.has_rerank_data()) { + for (size_t i = 0; i < loaded.num_clusters_; ++i) { + input.read( + loaded.cluster_storage_[i].rerank_data(), + static_cast(loaded.rerank_data_bytes(cluster_sizes[i])) + ); + } + } + for (size_t i = 0; i < loaded.num_clusters_; ++i) { + input.read( + reinterpret_cast(loaded.cluster_storage_[i].ids()), + static_cast(ids_bytes(cluster_sizes[i])) + ); + } + if (remaining != 0) { + throw FormatError("IVF index file has unexpected trailing data"); + } + loaded.complete_load(cluster_sizes); - input.close(); - swap(loaded); + input.close(); + swap(loaded); + } catch (const std::ios_base::failure&) { + throw FormatError("IVF index file is truncated"); + } } void IVF::read_format2(std::istream& input) { @@ -1412,7 +1423,7 @@ void IVF::complete_load(const std::vector& cluster_sizes) { for (size_t i = 0; i < num_clusters_; ++i) { for (PID id : cluster_storage_[i].id_storage) { if (id >= max_elements_ || id >= buffer::kSearchBufferCheckedMask) { - throw std::runtime_error("Invalid point ID in IVF index file"); + throw FormatError("Invalid point ID in IVF index file"); } } } diff --git a/src/index/ivf_initializer.cpp b/src/index/ivf_initializer.cpp index 1a10f8c3..4fb51a55 100644 --- a/src/index/ivf_initializer.cpp +++ b/src/index/ivf_initializer.cpp @@ -19,6 +19,7 @@ #include "rabitqlib/fastscan/query.hpp" #include "rabitqlib/index/detail/buffer.hpp" #include "rabitqlib/index/detail/parallel_for.hpp" +#include "rabitqlib/index/format_error.hpp" #include "rabitqlib/index/ivf/initializer.hpp" #include "rabitqlib/index/types.hpp" #include "rabitqlib/utils/space.hpp" @@ -186,7 +187,7 @@ void FlatRaBitQInitializer::write(std::ostream& output) const { void FlatRaBitQInitializer::read(std::istream& input, uint64_t bytes) { if (bytes != sizeof(float) * (centroids_.size() + global_centroid_.size()) + batch_data_.size()) { - throw std::runtime_error("IVF Flat RaBitQ initializer data has the wrong size"); + throw FormatError("IVF Flat RaBitQ initializer data has the wrong size"); } input.read( reinterpret_cast(centroids_.data()), @@ -201,7 +202,7 @@ void FlatRaBitQInitializer::read(std::istream& input, uint64_t bytes) { static_cast(batch_data_.size()) ); if (!input) { - throw std::runtime_error("IVF Flat RaBitQ initializer cannot be read"); + throw FormatError("IVF Flat RaBitQ initializer cannot be read"); } } } // namespace rabitqlib::ivf diff --git a/src/index/qg.cpp b/src/index/qg.cpp index a80ee9cf..a299141b 100644 --- a/src/index/qg.cpp +++ b/src/index/qg.cpp @@ -32,6 +32,7 @@ #include "rabitqlib/index/detail/buffer.hpp" #include "rabitqlib/index/detail/parallel_exception.hpp" #include "rabitqlib/index/detail/visited_set.hpp" +#include "rabitqlib/index/format_error.hpp" #include "rabitqlib/index/symqg/detail/search_scratch.hpp" #include "rabitqlib/index/types.hpp" #include "rabitqlib/utils/bitops.hpp" @@ -479,7 +480,7 @@ void QuantizedGraph::read_format2(std::istream& input) { const auto neighbors = get_neighbors(source); for (size_t i = 0; i < max_degree_; ++i) { if (neighbors[i] >= max_elements_) { - throw std::runtime_error("QuantizedGraph neighbor ID is invalid"); + throw FormatError("QuantizedGraph neighbor ID is invalid"); } } } @@ -523,13 +524,13 @@ void QuantizedGraph::load(const char* filename) { auto read_exact = [&](void* destination, size_t bytes, const char* field) { if (bytes > static_cast(std::numeric_limits::max())) { - throw std::runtime_error("QuantizedGraph field is too large to read"); + throw FormatError("QuantizedGraph field is too large to read"); } input.read( reinterpret_cast(destination), static_cast(bytes) ); if (!input) { - throw std::runtime_error( + throw FormatError( std::string("QuantizedGraph file is truncated while reading ") + field ); } @@ -544,7 +545,7 @@ void QuantizedGraph::load(const char* filename) { uint32_t version = 0; read_value(version, "format version"); if (version != kFormatVersion) { - throw std::runtime_error("QuantizedGraph file version is unsupported"); + throw FormatError("QuantizedGraph file version is unsupported"); } } else { // Files produced before qg-quant have no header and always contain raw vectors. @@ -567,9 +568,13 @@ void QuantizedGraph::load(const char* filename) { loaded.quantization_bits_ = 0; } - loaded.validate_configuration(); + try { + loaded.validate_configuration(); + } catch (const std::invalid_argument& error) { + throw FormatError(error.what()); // stored settings that do not fit + } if (stored_padded_dim < loaded.dim_ || stored_padded_dim % 32 != 0) { - throw std::runtime_error("QuantizedGraph file has an invalid padded dimension"); + throw FormatError("QuantizedGraph file has an invalid padded dimension"); } // Older files use 64-coordinate padding. Their saved rotation and codes must // retain that dimension, including when initialize() allocates the row storage. @@ -597,7 +602,7 @@ void QuantizedGraph::load(const char* filename) { const size_t payload_offset = static_cast(payload_position); if (payload_offset > file_size || file_size - payload_offset != expected_payload_bytes) { - throw std::runtime_error("QuantizedGraph payload size is invalid"); + throw FormatError("QuantizedGraph payload size is invalid"); } loaded.initialize(false); @@ -613,14 +618,14 @@ void QuantizedGraph::load(const char* filename) { const auto neighbors = loaded.get_neighbors(source); for (size_t i = 0; i < loaded.max_degree_; ++i) { if (neighbors[i] >= loaded.max_elements_) { - throw std::runtime_error("QuantizedGraph neighbor ID is invalid"); + throw FormatError("QuantizedGraph neighbor ID is invalid"); } } } loaded.rotator_->load(input); if (!input) { - throw std::runtime_error("QuantizedGraph file is truncated while reading rotator"); + throw FormatError("QuantizedGraph file is truncated while reading rotator"); } input.close(); diff --git a/tests/python/test_historical_persistence.py b/tests/python/test_historical_persistence.py index cf9b1225..6cdbfa11 100644 --- a/tests/python/test_historical_persistence.py +++ b/tests/python/test_historical_persistence.py @@ -8,7 +8,8 @@ import format2_file import numpy as np import pytest -from rabitqlib import HnswIndex, IvfIndex, SymqgIndex +import rabitqlib +from rabitqlib import HnswIndex, IvfIndex, SymqgIndex, quant FIXTURES = Path(__file__).resolve().parents[1] / "fixtures" / "persistence" NAMES = [ @@ -201,3 +202,30 @@ def test_v083_graphs_load_and_migrate(name, tmp_path): actual_ids, actual_distances = index.search(queries, k, ef=64) np.testing.assert_array_equal(actual_ids, ids) np.testing.assert_allclose(actual_distances, distances, rtol=2e-4, atol=2e-4) + + +def test_rejected_files_raise_format_error(tmp_path): + assert issubclass(rabitqlib.FormatError, ValueError) + assert issubclass(rabitqlib.FormatError, RuntimeError) + assert rabitqlib.FormatError.__module__ == "rabitqlib" + assert quant.FormatError is rabitqlib.FormatError + for name, cls in [ + ("ivf4_flat_l2", IvfIndex), + ("hnsw1_l2", HnswIndex), + ("qg4_l2", SymqgIndex), + ]: + legacy = (FIXTURES / "v0.8.3" / f"{name}.index").read_bytes() + damaged = tmp_path / f"{name}.index" + damaged.write_bytes(legacy[: len(legacy) // 2]) + with pytest.raises(rabitqlib.FormatError): + cls.load(str(damaged)) + migrated = tmp_path / f"{name}-format2.index" + cls.load(str(FIXTURES / "v0.8.3" / f"{name}.index")).save(str(migrated)) + payload = migrated.read_bytes() + migrated.write_bytes(payload[:-1]) + with pytest.raises(rabitqlib.FormatError, match="index file"): + cls.load(str(migrated)) + # Failing to open a file is not a format error. + with pytest.raises(RuntimeError) as missing: + IvfIndex.load(str(tmp_path / "missing.index")) + assert not isinstance(missing.value, rabitqlib.FormatError) diff --git a/tests/python/test_quant_editor_support.py b/tests/python/test_quant_editor_support.py index 65462890..7f77b44b 100644 --- a/tests/python/test_quant_editor_support.py +++ b/tests/python/test_quant_editor_support.py @@ -71,6 +71,13 @@ def test_module_docs_and_exports(): n.name for n in tree.body if isinstance(n, (ast.ClassDef, ast.FunctionDef)) } declared.update(n.target.id for n in tree.body if isinstance(n, ast.AnnAssign)) + declared.update( # explicit re-exports such as FormatError + alias.asname + for n in tree.body + if isinstance(n, ast.ImportFrom) + for alias in n.names + if alias.asname == alias.name + ) assert set(quant.__all__) == declared for name in quant.__all__: assert hasattr(quant, name), name @@ -98,8 +105,18 @@ def test_function_signatures_and_docs(name): def test_classes_match_runtime(): - classes = {n.name: n for n in installed_stub().body if isinstance(n, ast.ClassDef)} - assert set(classes) == {"FormatError", "Query", "Rotator"} + stub = installed_stub().body + classes = {n.name: n for n in stub if isinstance(n, ast.ClassDef)} + assert set(classes) == {"Query", "Rotator"} + # FormatError is rabitqlib.FormatError, which the stub re-exports. + assert quant.FormatError is rabitqlib.FormatError + reexports = { + alias.name: alias.asname + for node in stub + if isinstance(node, ast.ImportFrom) and node.module == "rabitqlib" + for alias in node.names + } + assert reexports.get("FormatError") == "FormatError" for name, cls in classes.items(): runtime = getattr(quant, name) assert runtime.__module__ == "rabitqlib.quant" diff --git a/tests/python/test_quant_rotator.py b/tests/python/test_quant_rotator.py index d96a0686..82f4e700 100644 --- a/tests/python/test_quant_rotator.py +++ b/tests/python/test_quant_rotator.py @@ -123,6 +123,7 @@ def test_corrupt_and_unsupported_blobs(): with pytest.raises(quant.FormatError, match=message): quant.Rotator.from_bytes(data) assert issubclass(quant.FormatError, ValueError) + assert issubclass(quant.FormatError, RuntimeError) unknown_seed = quant.Rotator.from_bytes(patched(patched(blob, 20, 0), 56, 0)) assert unknown_seed.seed is None with pytest.raises(TypeError, match="contiguous bytes-like"): diff --git a/tests/unit/rabitqlib/index/historical_persistence_test.cpp b/tests/unit/rabitqlib/index/historical_persistence_test.cpp index 3f022b1d..3cd73797 100644 --- a/tests/unit/rabitqlib/index/historical_persistence_test.cpp +++ b/tests/unit/rabitqlib/index/historical_persistence_test.cpp @@ -14,6 +14,7 @@ #include #include "rabitqlib/core/metric.hpp" +#include "rabitqlib/index/format_error.hpp" #include "rabitqlib/index/hnsw/hnsw.hpp" #include "rabitqlib/index/ivf/initializer.hpp" #include "rabitqlib/index/ivf/ivf.hpp" @@ -108,7 +109,7 @@ TEST_P(HistoricalPersistenceTest, LoadsSearchesResavesAndPreservesStateAfterFail index.save(output.string().c_str()); EXPECT_TRUE(read_bytes(output) == saved); std::filesystem::resize_file(output, std::filesystem::file_size(output) - 1); - EXPECT_THROW(index.load(output.string().c_str()), std::runtime_error); + EXPECT_THROW(index.load(output.string().c_str()), FormatError); check_counts(); search(); compare(); @@ -202,6 +203,60 @@ INSTANTIATE_TEST_SUITE_P( ), [](const auto& info) { return info.param.name; } ); +// Truncated files of the earlier layouts raise FormatError like format-2 files. +TEST(HistoricalPersistenceTest, TruncatedEarlierFilesRaiseFormatError) { + namespace fs = std::filesystem; + const fs::path path = + fs::temp_directory_path() / rabitq_test::process_unique("historical_truncated"); + for (const char* name : {"ivf4_flat_l2", "hnsw1_l2", "qg4_l2"}) { + SCOPED_TRACE(name); + fs::copy_file( + fs::path(RABITQ_TEST_FIXTURE_ROOT) / "v0.8.3" / (std::string(name) + ".index"), + path, + fs::copy_options::overwrite_existing + ); + fs::resize_file(path, fs::file_size(path) / 2); + const std::string file = path.string(); + if (name[0] == 'i') { + ivf::IVF index; + EXPECT_THROW(index.load(file.c_str()), FormatError); + } else if (name[0] == 'h') { + hnsw::HierarchicalNSW index; + EXPECT_THROW(index.load(file.c_str()), FormatError); + } else { + symqg::QuantizedGraph index; + EXPECT_THROW(index.load(file.c_str()), FormatError); + } + } + // A metric byte that is neither L2 nor IP, at offset 61 of an RABQIDX1 v2 file. + fs::copy_file( + fs::path(RABITQ_TEST_FIXTURE_ROOT) / "v0.8.3" / "ivf4_flat_l2.index", + path, + fs::copy_options::overwrite_existing + ); + { + std::fstream file(path, std::ios::binary | std::ios::in | std::ios::out); + file.seekp(61); + file.put(static_cast(2)); + } + ivf::IVF bad_metric; + try { + bad_metric.load(path.string().c_str()); + ADD_FAILURE() << "an invalid metric was loaded"; + } catch (const FormatError& error) { + EXPECT_STREQ(error.what(), "Invalid IVF index metadata"); + } + fs::remove(path); + ivf::IVF missing; + try { + missing.load((path.string() + ".missing").c_str()); + ADD_FAILURE() << "a missing file was loaded"; + } catch (const FormatError&) { + ADD_FAILURE() << "a missing file is not a format error"; + } catch (const std::runtime_error&) { // "Cannot open IVF index file" + } +} + // The checks of the SymphonyQG layout before format 2, on a copy of its v0.8.3 file. TEST(HistoricalPersistenceTest, RejectsMalformedEarlierGraphFilesWithoutChangingTarget) { namespace fs = std::filesystem; @@ -231,8 +286,7 @@ TEST(HistoricalPersistenceTest, RejectsMalformedEarlierGraphFilesWithoutChanging EXPECT_STREQ(error.what(), "QuantizedGraph file has an invalid padded dimension"); } EXPECT_THROW( - load_patched(kPointCountOffset, std::numeric_limits::max()), - std::invalid_argument + load_patched(kPointCountOffset, std::numeric_limits::max()), FormatError ); EXPECT_EQ(target.num_vertices(), 65U); EXPECT_EQ(target.metric_type(), METRIC_IP); From da1760c5ab59f4caeaaafc16d576326b4217f9ac Mon Sep 17 00:00:00 2001 From: gouyt13clear Date: Sat, 7 Nov 2026 21:38:16 +0800 Subject: [PATCH 10/11] feat(python): use int64 point IDs with -1 for missing results Closes the Python half of #211. search returns int64 IDs, and slots without a neighbor hold -1 instead of 2**32 - 1, with distance inf as before. add returns int64 IDs; remove already took any integer IDs and still rejects -1 and other out-of-range values. C++ point IDs stay 32-bit PID values with kPidMax for a missing result. The bindings search into a PID buffer and widen it once per batch to int64, mapping kPidMax to -1. That adds one pass and a temporary buffer of num_queries * k IDs per search call; this change makes no performance claim. txtai 9.14.0 and earlier filter with uid != 0xFFFFFFFF; the release notes point to #213 for the first compatible txtai release. --- docs/docs/index/hnsw.md | 6 ++--- docs/docs/index/ivf.md | 4 ++-- docs/docs/index/qg.md | 2 +- docs/docs/python_migration.md | 3 ++- docs/docs/quick_start.md | 2 +- docs/docs/releases/unreleased.md | 6 +++++ docs/formats.md | 2 +- python_bindings/__init__.pyi | 36 ++++++++++++++--------------- python_bindings/bindings_common.hpp | 29 +++++++++++++++++------ python_bindings/hnsw_bindings.cpp | 15 ++++++------ python_bindings/ivf_bindings.cpp | 16 ++++++------- python_bindings/symqg_bindings.cpp | 23 ++++++++---------- tests/python/test_editor_support.py | 2 +- tests/python/test_hnsw.py | 4 ++-- tests/python/test_index_counts.py | 2 +- tests/python/test_ivf.py | 14 +++++------ tests/python/test_symqg.py | 2 +- tests/python/test_symqg_api.py | 4 ++-- tests/python/test_train_api.py | 20 ++++++++++++++-- tests/python/typing/public_api.py | 10 ++++---- 20 files changed, 118 insertions(+), 84 deletions(-) diff --git a/docs/docs/index/hnsw.md b/docs/docs/index/hnsw.md index 583e2e52..a5fe8e53 100644 --- a/docs/docs/index/hnsw.md +++ b/docs/docs/index/hnsw.md @@ -82,7 +82,7 @@ available CPU, so **default builds are not reproducible**: pass `num_threads=1` still change results. Loading restores the saved rotation. Results use squared L2 or `1 - dot` estimates, ordered from smallest to largest. -Python fills missing results with ID `2**32 - 1` and distance `+inf`; C++ returns +Python returns `int64` IDs and fills missing results with ID `-1` and distance `+inf`; C++ returns only the available pairs. See [result handling](../quick_start.md#read-the-results) and the shared [input, count and concurrency contracts](usage.md). @@ -271,8 +271,8 @@ removed = index.remove(new_ids[:10]) # IDs in [0, num_stored_points) A removed point keeps its codes, its links, and its place in the graph. Search still walks through it and `add` may link new points to it, which keeps the graph connected, as in hnswlib. Search never returns it, so a query can get fewer than -`k` results once points are removed: in Python the missing slots hold `kPidMax` -(`2**32 - 1`) with an infinite distance. Removed points still count in +`k` results once points are removed: in Python the missing slots hold ID `-1` +with an infinite distance. Removed points still count in `num_stored_points()` and cannot be restored. Removal requires the same exclusive access as other updates. diff --git a/docs/docs/index/ivf.md b/docs/docs/index/ivf.md index fdef96c7..2047825e 100644 --- a/docs/docs/index/ivf.md +++ b/docs/docs/index/ivf.md @@ -56,7 +56,7 @@ See [C++ integration](../cpp_integration.md) for build setup and | `high_accuracy` | Python defaults to `None`: HACC for 4–9 bits, standard FastScan otherwise. | Results contain IDs and squared L2 or `1 - dot` distances, ordered from smallest -to largest. Quantized distances are estimates. Unfilled slots use ID `2**32 - 1` +to largest. Quantized distances are estimates. IDs are `int64` in Python. Unfilled slots use ID `-1` and distance `+inf`; see [result handling](../quick_start.md#read-the-results). See the shared [input and concurrency contracts](usage.md). @@ -450,7 +450,7 @@ enters the result buffer and never triggers reranking. Any code that estimates IVF-RaBitQ distances must keep this behavior and must never turn `+inf` into NaN. Search may return fewer than `k` results once points are removed: the missing slots -hold `kPidMax` (`2**32 - 1` in Python) with an infinite distance, as when the probed +hold `kPidMax` (`-1` in Python) with an infinite distance, as when the probed clusters hold fewer than `k` points. The removal marker lives in the ordinary batch data and needs no additional diff --git a/docs/docs/index/qg.md b/docs/docs/index/qg.md index 790b5643..8f73bb51 100644 --- a/docs/docs/index/qg.md +++ b/docs/docs/index/qg.md @@ -77,7 +77,7 @@ qg.save("qg_example.index"); | `init` | Python keyword: `"pipnn"` (default) or `"random"`. | | `metric` | Python keyword: `"l2"` (default) or `"ip"`. | | `seed` | Python keyword: rotation and graph-construction seed in `[0, 2**32 - 1]`. The default `None` draws one at random; `index.seed` reports it, and passing it back with `num_threads=1` rebuilds the same graph from the same data. | -| `k`, `ef` | `k` is at least 1 and may exceed the point count; slots the graph cannot fill are padded with ID `2**32 - 1` and distance `+inf`. Omitted or zero `ef` uses `max(k, 64)` in Python, and smaller values are raised to `k`, as for HNSW-RaBitQ. | +| `k`, `ef` | `k` is at least 1 and may exceed the point count; slots the graph cannot fill are padded with ID `-1` in Python (`kPidMax` in C++) and distance `+inf`. Omitted or zero `ef` uses `max(k, 64)` in Python, and smaller values are raised to `k`, as for HNSW-RaBitQ. | | `num_threads` | Python `add` defaults to every available logical CPU, capped by `OMP_NUM_THREADS`, and `search` to 1; `None` selects these defaults. | Python search returns `(ids, distances)` arrays. Distances are squared diff --git a/docs/docs/python_migration.md b/docs/docs/python_migration.md index 52fad117..85d2d0c3 100644 --- a/docs/docs/python_migration.md +++ b/docs/docs/python_migration.md @@ -54,7 +54,8 @@ work if you want to run k-means yourself. - **`k` above the point count.** 0.8.0 and 0.8.1 raised `ValueError` when `k` exceeded `num_stored_points` (HNSW-RaBitQ) or the point count (IVF-RaBitQ, SymphonyQG). Since 0.8.2, every index accepts any `k >= 1` and fills missing - slots with ID `2**32 - 1` and distance `inf`; test `np.isfinite(distances)`. + slots with ID `2**32 - 1` (`-1` since 0.9) and distance `inf`; test + `np.isfinite(distances)`, which works in both. - **HNSW-RaBitQ capacity grows.** `add` resizes the index, at least doubling it, instead of raising `ValueError` when the batch does not fit. This also applies to `add` on indexes created in the old form, and to the deprecated `build`, diff --git a/docs/docs/quick_start.md b/docs/docs/quick_start.md index c2be40f4..fae22f68 100644 --- a/docs/docs/quick_start.md +++ b/docs/docs/quick_start.md @@ -66,7 +66,7 @@ not exact distances to the original vectors. A search may return fewer than `k` valid neighbors, for example when `k` exceeds the number of points, after removals, or when the probed IVF-RaBitQ clusters contain too -few points. Unfilled slots contain ID `2**32 - 1` and distance `+inf`. Filter them with +few points. IDs are `int64`; unfilled slots contain ID `-1` and distance `+inf`. Filter them with `valid = np.isfinite(distances)` before using IDs to index your data. The `metric` argument accepts `"l2"` and `"ip"` (also `"innerproduct"`). For diff --git a/docs/docs/releases/unreleased.md b/docs/docs/releases/unreleased.md index de3b59c5..29ec64c6 100644 --- a/docs/docs/releases/unreleased.md +++ b/docs/docs/releases/unreleased.md @@ -12,6 +12,12 @@ ## Python changes +- Point IDs are `int64` at the Python boundary (#211): + - `search` returns `int64` IDs, and slots without a neighbor hold `-1` instead of `2**32 - 1`; their distance is still `inf`. Code that compares IDs with `2**32 - 1`, `0xFFFFFFFF` or `np.iinfo(np.uint32).max` must change; `np.isfinite(distances)`, recommended since 0.8.2, works before and after. + - `add` returns `int64` IDs. `remove` already took any integer IDs and still rejects `-1` and other out-of-range values without removing anything. + - IDs remain the row numbers the index assigns, and C++ point IDs remain 32-bit `PID` values with `kPidMax` for a missing result. + - txtai 9.14.0 and earlier filter results with `uid != 0xFFFFFFFF`, so with this release they return padded slots as ID `-1`; see #213 for the first compatible txtai release. + - Loading a format-2 file keeps the `seed` of every index, and `ef_construction` and `init` of SymphonyQG. They were `None` after `load`, and still are for files from releases up to 0.8. - With a recorded seed, `cp.seed` after `load` follows it as for a new index: the seed for IVF-RaBitQ, its low 32 bits for HNSW-RaBitQ. It was the k-means default. - A new exception, `rabitqlib.FormatError`, subclasses both `ValueError` and `RuntimeError`: diff --git a/docs/formats.md b/docs/formats.md index 7de3e37f..c3132475 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -196,7 +196,7 @@ L2 and inner product are as in [Distance conventions](development.md#distance-co ### Point IDs -Search results expose IDs as `int64`, and a missing result is `-1` (#211). `id_bits` records what the file stores. The in-memory indexes keep 32-bit row numbers internally and write `id_bits = 32`; their upper limit stays below 2^31 points (the search-buffer marker bit). IVF-SSD writes `id_bits = 64`. A reader rejects an `id_bits` its index kind does not support. +Python `search`, `add` and `remove` use `int64` IDs, and a missing search result is `-1` (#211). `id_bits` records what the file stores. The in-memory indexes keep 32-bit row numbers internally and write `id_bits = 32`; their upper limit stays below 2^31 points (the search-buffer marker bit). IVF-SSD writes `id_bits = 64`. A reader rejects an `id_bits` its index kind does not support. ### Section kinds diff --git a/python_bindings/__init__.pyi b/python_bindings/__init__.pyi index ef5726d3..61e110cc 100644 --- a/python_bindings/__init__.pyi +++ b/python_bindings/__init__.pyi @@ -316,7 +316,7 @@ class IvfIndex: cluster_ids: _IntegerIds | None = None, num_threads: SupportsIndex | None = None, fast_quantization: bool | np.bool_ = False, - ) -> npt.NDArray[np.uint32]: + ) -> npt.NDArray[np.int64]: """Add vectors, training the index first if needed. Parameters @@ -336,7 +336,7 @@ class IvfIndex: Returns ------- - ids : ndarray of uint32, shape (n,) + ids : ndarray of int64, shape (n,) Assigned point IDs. Notes @@ -362,7 +362,7 @@ class IvfIndex: cluster_ids: _IntegerIds | None, num_threads: SupportsIndex = ..., fast_quantization: bool | np.bool_ = ..., - ) -> npt.NDArray[np.uint32]: ... + ) -> npt.NDArray[np.int64]: ... def remove(self, ids: _IntegerIds) -> int: """Exclude point IDs from subsequent search results. @@ -389,7 +389,7 @@ class IvfIndex: nprobe: SupportsIndex | None = None, high_accuracy: bool | np.bool_ | None = None, num_threads: SupportsIndex | None = None, - ) -> tuple[npt.NDArray[np.uint32], npt.NDArray[np.float32]]: + ) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.float32]]: """Search a batch of query vectors. Parameters @@ -412,7 +412,7 @@ class IvfIndex: Returns ------- - ids : ndarray of uint32, shape (n_queries, k) + ids : ndarray of int64, shape (n_queries, k) Point IDs of the selected neighbors. distances : ndarray of float32, shape (n_queries, k) Lower values are better; squared L2 estimates for l2 and inner-product @@ -421,7 +421,7 @@ class IvfIndex: Notes ----- Requires a trained or loaded index. Returns (ids, distances), in that - order. Missing neighbors have ID 4294967295 and distance inf; test + order. Missing neighbors have ID -1 and distance inf; test np.isfinite(distances) to find them. Array shapes and value ranges are checked at runtime, not by static type checkers. """ @@ -756,7 +756,7 @@ class HnswIndex: cluster_ids: _IntegerIds | None = None, num_threads: SupportsIndex | None = None, fast_quantization: bool | np.bool_ = False, - ) -> npt.NDArray[np.uint32]: + ) -> npt.NDArray[np.int64]: """Add vectors, training the index first if needed. Parameters @@ -776,7 +776,7 @@ class HnswIndex: Returns ------- - ids : ndarray of uint32, shape (n,) + ids : ndarray of int64, shape (n,) Assigned point IDs. Notes @@ -804,7 +804,7 @@ class HnswIndex: cluster_ids: _IntegerIds | None, num_threads: SupportsIndex = ..., fast_quantization: bool | np.bool_ = ..., - ) -> npt.NDArray[np.uint32]: ... + ) -> npt.NDArray[np.int64]: ... def remove(self, ids: _IntegerIds) -> int: """Exclude point IDs from subsequent search results. @@ -830,7 +830,7 @@ class HnswIndex: k: SupportsIndex, ef: SupportsIndex | None = None, num_threads: SupportsIndex | None = None, - ) -> tuple[npt.NDArray[np.uint32], npt.NDArray[np.float32]]: + ) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.float32]]: """Search a batch of query vectors. Parameters @@ -850,7 +850,7 @@ class HnswIndex: Returns ------- - ids : ndarray of uint32, shape (n_queries, k) + ids : ndarray of int64, shape (n_queries, k) Point IDs of the selected neighbors. distances : ndarray of float32, shape (n_queries, k) Lower values are better; squared L2 estimates for l2 and inner-product @@ -859,7 +859,7 @@ class HnswIndex: Notes ----- Requires a trained or loaded index. Returns (ids, distances), in that - order. Missing neighbors have ID 4294967295 and distance inf; test + order. Missing neighbors have ID -1 and distance inf; test np.isfinite(distances) to find them. Array shapes and value ranges are checked at runtime, not by static type checkers. """ @@ -1118,7 +1118,7 @@ class SymqgIndex: def add( self, data: npt.ArrayLike, *, num_threads: SupportsIndex | None = None - ) -> npt.NDArray[np.uint32]: + ) -> npt.NDArray[np.int64]: """Build the graph from all vectors. Parameters @@ -1132,7 +1132,7 @@ class SymqgIndex: Returns ------- - ids : ndarray of uint32, shape (n,) + ids : ndarray of int64, shape (n,) Point IDs, the row numbers of data. Notes @@ -1180,7 +1180,7 @@ class SymqgIndex: k: SupportsIndex, ef: SupportsIndex | None = None, num_threads: SupportsIndex | None = None, - ) -> tuple[npt.NDArray[np.uint32], npt.NDArray[np.float32]]: + ) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.float32]]: """Search a batch of query vectors. Parameters @@ -1200,7 +1200,7 @@ class SymqgIndex: Returns ------- - ids : ndarray of uint32, shape (n_queries, k) + ids : ndarray of int64, shape (n_queries, k) Point IDs of the selected neighbors. distances : ndarray of float32, shape (n_queries, k) Lower values are better; squared L2 estimates for l2 and inner-product @@ -1209,7 +1209,7 @@ class SymqgIndex: Notes ----- Returns (ids, distances), in that order. When the graph reaches fewer than k - points, the remaining neighbors have ID 4294967295 and distance inf; test + points, the remaining neighbors have ID -1 and distance inf; test np.isfinite(distances) to find them. Array shapes and value ranges are checked at runtime, not by static type checkers. """ @@ -1221,7 +1221,7 @@ class SymqgIndex: k: SupportsIndex, ef: SupportsIndex, num_threads: SupportsIndex = 1, - ) -> tuple[npt.NDArray[np.uint32], npt.NDArray[np.float32]]: + ) -> tuple[npt.NDArray[np.int64], npt.NDArray[np.float32]]: """Deprecated since 0.8: the same as search(); use search().""" def save(self, path: _Path) -> None: diff --git a/python_bindings/bindings_common.hpp b/python_bindings/bindings_common.hpp index f88da6fc..03538ff9 100644 --- a/python_bindings/bindings_common.hpp +++ b/python_bindings/bindings_common.hpp @@ -353,13 +353,30 @@ RowView checked_output_rows( return make_row_view(std::move(array)); } +// Point IDs as Python sees them: int64, with -1 where the index reports kPidMax. +inline py::array_t python_ids(const PID* ids, std::vector shape) { + py::array_t result(std::move(shape)); + int64_t* out = result.mutable_data(); + for (py::ssize_t i = 0; i < result.size(); ++i) { + out[i] = ids[i] == kPidMax ? int64_t{-1} : static_cast(ids[i]); + } + return result; +} + +// (num_queries, k) outputs. The index writes row numbers into ids; slots without a +// neighbor keep kPidMax and +inf. Searches pad slots without a result, so k may exceed +// the number of points. struct SearchResults { - py::array_t ids; + std::vector shape; + std::vector ids; py::array_t distances; + + // (ids, distances) for Python, with int64 IDs and -1 for a missing neighbor. + [[nodiscard]] py::tuple to_python() const { + return py::make_tuple(python_ids(ids.data(), shape), distances); + } }; -// (num_queries, k) outputs. Slots without a neighbor keep kPidMax and +inf. -// Searches pad slots without a result, so k may exceed the number of points. inline void check_k(size_t k) { if (k == 0 || k > buffer::kSearchBufferMaxPointCount) { throw std::invalid_argument("k must be between 1 and 2**31"); @@ -369,10 +386,8 @@ inline void check_k(size_t k) { inline SearchResults unfilled_search_results(size_t num_queries, size_t k) { const auto shape = std::vector{ static_cast(num_queries), static_cast(k)}; - SearchResults results{py::array_t(shape), py::array_t(shape)}; - std::fill( - results.ids.mutable_data(), results.ids.mutable_data() + results.ids.size(), kPidMax - ); + SearchResults results{ + shape, std::vector(num_queries * k, kPidMax), py::array_t(shape)}; std::fill( results.distances.mutable_data(), results.distances.mutable_data() + results.distances.size(), diff --git a/python_bindings/hnsw_bindings.cpp b/python_bindings/hnsw_bindings.cpp index 5da36d1c..e2830b13 100644 --- a/python_bindings/hnsw_bindings.cpp +++ b/python_bindings/hnsw_bindings.cpp @@ -177,7 +177,7 @@ class HnswIndex { num_clusters_ = num_clusters; } - py::array_t add( + py::array_t add( py::handle data, const py::object& cluster_ids, std::optional num_threads, @@ -203,7 +203,7 @@ class HnswIndex { } // Allocated before the index is touched. - auto ids = py::array_t(static_cast(rows)); + auto ids = py::array_t(static_cast(rows)); const auto* data_ptr = data_array.data(); const size_t threads = num_threads.value_or(0); if (trains) { @@ -287,7 +287,6 @@ class HnswIndex { const size_t ef_search = ef.value_or(0) == 0 ? std::max(k, 64) : *ef; auto output = unfilled_search_results(static_cast(query_array.shape(0)), k); - auto result_ids_view = output.ids.mutable_unchecked<2>(); auto distances_view = output.distances.mutable_unchecked<2>(); const auto* queries_data = query_array.data(); @@ -305,13 +304,13 @@ class HnswIndex { k, results[static_cast(i)].size() )); ++j) { - result_ids_view(i, j) = + output.ids[(static_cast(i) * k) + static_cast(j)] = results[static_cast(i)][static_cast(j)].second; distances_view(i, j) = results[static_cast(i)][static_cast(j)].first; } } - return py::make_tuple(output.ids, output.distances); + return output.to_python(); } void save(py::handle path) const { @@ -533,7 +532,7 @@ fast_quantization : bool, optional Returns ------- -ids : ndarray of uint32, shape (n,) +ids : ndarray of int64, shape (n,) Assigned point IDs. Notes @@ -659,7 +658,7 @@ num_threads : int, optional Returns ------- -ids : ndarray of uint32, shape (n_queries, k) +ids : ndarray of int64, shape (n_queries, k) Point IDs of the selected neighbors. distances : ndarray of float32, shape (n_queries, k) Lower values are better; squared L2 estimates for l2 and inner-product @@ -668,7 +667,7 @@ distances : ndarray of float32, shape (n_queries, k) Notes ----- Requires a trained or loaded index. Returns (ids, distances), in that -order. Missing neighbors have ID 4294967295 and distance inf; test +order. Missing neighbors have ID -1 and distance inf; test np.isfinite(distances) to find them. Array shapes and value ranges are checked at runtime, not by static type checkers.)doc" ) diff --git a/python_bindings/ivf_bindings.cpp b/python_bindings/ivf_bindings.cpp index 4304e74d..3f384ee7 100644 --- a/python_bindings/ivf_bindings.cpp +++ b/python_bindings/ivf_bindings.cpp @@ -203,7 +203,7 @@ class IvfIndex { } } - py::array_t add( + py::array_t add( py::handle data, const py::object& cluster_ids, std::optional num_threads, @@ -237,10 +237,10 @@ class IvfIndex { const size_t first = index_->num_stored_points(); // Allocate the returned IDs before committing any changes to the index. - auto ids = py::array_t(static_cast(rows)); + auto ids = py::array_t(static_cast(rows)); auto* ids_data = ids.mutable_data(); for (size_t i = 0; i < rows; ++i) { - ids_data[i] = static_cast(first + i); + ids_data[i] = static_cast(first + i); } const auto* data_ptr = data_array.data(); @@ -322,7 +322,7 @@ class IvfIndex { const size_t num_queries = static_cast(query_array.shape(0)); auto output = unfilled_search_results(num_queries, k); - auto* ids_data = output.ids.mutable_data(); + auto* ids_data = output.ids.data(); auto* distances_data = output.distances.mutable_data(); const auto* queries_data = query_array.data(); @@ -340,7 +340,7 @@ class IvfIndex { ); } - return py::make_tuple(output.ids, output.distances); + return output.to_python(); } void save(py::handle path) const { @@ -539,7 +539,7 @@ fast_quantization : bool, optional Returns ------- -ids : ndarray of uint32, shape (n,) +ids : ndarray of int64, shape (n,) Assigned point IDs. Notes @@ -723,7 +723,7 @@ num_threads : int, optional Returns ------- -ids : ndarray of uint32, shape (n_queries, k) +ids : ndarray of int64, shape (n_queries, k) Point IDs of the selected neighbors. distances : ndarray of float32, shape (n_queries, k) Lower values are better; squared L2 estimates for l2 and inner-product @@ -732,7 +732,7 @@ distances : ndarray of float32, shape (n_queries, k) Notes ----- Requires a trained or loaded index. Returns (ids, distances), in that -order. Missing neighbors have ID 4294967295 and distance inf; test +order. Missing neighbors have ID -1 and distance inf; test np.isfinite(distances) to find them. Array shapes and value ranges are checked at runtime, not by static type checkers.)doc" ) diff --git a/python_bindings/symqg_bindings.cpp b/python_bindings/symqg_bindings.cpp index bdfb239b..0430494a 100644 --- a/python_bindings/symqg_bindings.cpp +++ b/python_bindings/symqg_bindings.cpp @@ -77,7 +77,7 @@ class SymqgIndex { check_coordinates(x_ptr, count, dim_, "x"); } - py::array_t add(py::handle data, std::optional num_threads) { + py::array_t add(py::handle data, std::optional num_threads) { const auto operation = access_.write(); if (built()) { throw std::runtime_error( @@ -89,8 +89,8 @@ class SymqgIndex { build_graph(data, *ef_construction_, num_threads.value_or(0), *init_, *seed_); // Point IDs are the row numbers of data. const size_t rows = index_->num_points(); - auto ids = py::array_t(static_cast(rows)); - std::iota(ids.mutable_data(), ids.mutable_data() + rows, PID{0}); + auto ids = py::array_t(static_cast(rows)); + std::iota(ids.mutable_data(), ids.mutable_data() + rows, int64_t{0}); return ids; } @@ -124,12 +124,9 @@ class SymqgIndex { ensure_dimension(query_array, dim_, "query dimension does not match index dim"); const size_t num_queries = static_cast(query_array.shape(0)); - const auto shape = std::vector{ - static_cast(num_queries), static_cast(k)}; - auto result_ids = py::array_t(shape); - auto distances = py::array_t(shape); - auto* ids_data = result_ids.mutable_data(); - auto* distances_data = distances.mutable_data(); + auto output = unfilled_search_results(num_queries, k); + auto* ids_data = output.ids.data(); + auto* distances_data = output.distances.mutable_data(); const auto* queries_data = query_array.data(); { py::gil_scoped_release release; @@ -144,7 +141,7 @@ class SymqgIndex { ); } - return py::make_tuple(result_ids, distances); + return output.to_python(); } void save(py::handle path) const { @@ -399,7 +396,7 @@ num_threads : int, optional Returns ------- -ids : ndarray of uint32, shape (n_queries, k) +ids : ndarray of int64, shape (n_queries, k) Point IDs of the selected neighbors. distances : ndarray of float32, shape (n_queries, k) Lower values are better; squared L2 estimates for l2 and inner-product @@ -408,7 +405,7 @@ distances : ndarray of float32, shape (n_queries, k) Notes ----- Returns (ids, distances), in that order. When the graph reaches fewer than k -points, the remaining neighbors have ID 4294967295 and distance inf; test +points, the remaining neighbors have ID -1 and distance inf; test np.isfinite(distances) to find them. Array shapes and value ranges are checked at runtime, not by static type checkers.)doc"; @@ -474,7 +471,7 @@ num_threads : int, optional Returns ------- -ids : ndarray of uint32, shape (n,) +ids : ndarray of int64, shape (n,) Point IDs, the row numbers of data. Notes diff --git a/tests/python/test_editor_support.py b/tests/python/test_editor_support.py index eb14d8c4..2bf26ab2 100644 --- a/tests/python/test_editor_support.py +++ b/tests/python/test_editor_support.py @@ -212,7 +212,7 @@ def test_documented_search_result_types(kind): result = index.search(data[:2], 3, ef=20) ids, distances = result assert ids.shape == distances.shape == (2, 3) - assert ids.dtype == np.uint32 + assert ids.dtype == np.int64 assert distances.dtype == np.float32 diff --git a/tests/python/test_hnsw.py b/tests/python/test_hnsw.py index 24e2d8b5..2abebba5 100644 --- a/tests/python/test_hnsw.py +++ b/tests/python/test_hnsw.py @@ -104,7 +104,7 @@ def test_parallel_build_preserves_self_retrieval(base_data, clusters): def test_search_output_contract(built_hnsw, query_data, nq, k): ids, dists = built_hnsw.search(query_data[:nq], k=k, ef=_EF) assert ids.shape == dists.shape == (nq, k) - assert ids.dtype == np.uint32 + assert ids.dtype == np.int64 assert dists.dtype == np.float32 assert np.all(ids < N_VECTORS) assert np.isfinite(dists).all() @@ -401,7 +401,7 @@ def test_added_points_survive_save_and_load( # ── remove ──────────────────────────────────────────────────────────────────── -_NO_ID = np.iinfo(np.uint32).max +_NO_ID = -1 def _removable(nbits=4, metric="l2", count=120, dim=64, seed=61): diff --git a/tests/python/test_index_counts.py b/tests/python/test_index_counts.py index a9448d9a..22287691 100644 --- a/tests/python/test_index_counts.py +++ b/tests/python/test_index_counts.py @@ -85,7 +85,7 @@ def test_count_contract_through_lifecycle(tmp_path, kind, bits): check_counts(index, capacity, stored, 0) args = {"nprobe": 1} if kind == "ivf" else {"ef": stored} hits, _ = index.search(data[:1], k=1, **args) - assert np.all(hits == np.iinfo(np.uint32).max) + assert np.all(hits == -1) np.testing.assert_array_equal(index.add(data[-1:], assignments[:1]), [stored]) stored += 1 diff --git a/tests/python/test_ivf.py b/tests/python/test_ivf.py index d2072124..22cd2497 100644 --- a/tests/python/test_ivf.py +++ b/tests/python/test_ivf.py @@ -46,7 +46,7 @@ def test_properties(built_ivf): def test_search_output_contract(built_ivf, query_data, nq, k, nprobe): ids, dists = built_ivf.search(query_data[:nq], k=k, nprobe=nprobe) assert ids.shape == dists.shape == (nq, k) - assert ids.dtype == np.uint32 + assert ids.dtype == np.int64 assert dists.dtype == np.float32 assert np.all(ids < N_VECTORS) assert np.isfinite(dists).all() @@ -86,7 +86,7 @@ def test_batch_search_matches_individual_queries(bits, metric): assert empty_ids.shape == empty_distances.shape == (0, 7) index.remove(np.arange(len(data))) ids, distances = index.search(queries, k=7, nprobe=4, num_threads=4) - assert np.all(ids == np.iinfo(np.uint32).max) + assert np.all(ids == -1) assert np.all(np.isinf(distances)) @@ -100,7 +100,7 @@ def test_unfilled_results_use_sentinels(): ids, dists = idx.search(data[:1], k=3, nprobe=1) assert ids[0, 0] == 0 - np.testing.assert_array_equal(ids[0, 1:], np.iinfo(np.uint32).max) + np.testing.assert_array_equal(ids[0, 1:], -1) assert np.all(np.isinf(dists[0, 1:])) @@ -575,7 +575,7 @@ def _dynamic_index(nbits=4, metric="l2", count=120, dim=65, clusters=3, seed=61) def test_add_returns_ids_and_makes_points_searchable(): idx, old, new, _ = _dynamic_index(nbits=32) added = idx.add(new) - assert added.dtype == np.uint32 + assert added.dtype == np.int64 np.testing.assert_array_equal(added, np.arange(len(old), len(old) + len(new))) assert idx.max_elements == len(old) + len(new) # k may now exceed the original point count; raw storage makes the ranking exact. @@ -716,7 +716,7 @@ def check(index): keep = ~np.isin(before_ids[q], removed) np.testing.assert_array_equal(ids[q, :alive], before_ids[q][keep]) np.testing.assert_array_equal(dists[q, :alive], before_dists[q][keep]) - assert np.all(ids[q, alive:] == np.iinfo(np.uint32).max) + assert np.all(ids[q, alive:] == -1) assert np.all(np.isinf(dists[q, alive:])) top, top_dists = index.search( queries, k=10, nprobe=3, high_accuracy=high_accuracy @@ -738,7 +738,7 @@ def check(index): queries, k=count + len(new), nprobe=3, high_accuracy=high_accuracy ) assert not np.isin(ids, removed).any() - assert np.count_nonzero(ids[0] != np.iinfo(np.uint32).max) == count + len(new) - 4 + assert np.count_nonzero(ids[0] != -1) == count + len(new) - 4 # A reloaded index grows exactly like the original. expected_ids, expected_dists = idx.search( @@ -771,7 +771,7 @@ def test_removing_every_point_leaves_a_searchable_index(): idx, old, new, _ = _dynamic_index() assert idx.remove(np.arange(len(old))) == len(old) ids, dists = idx.search(new[:2], k=5, nprobe=3) - assert np.all(ids == np.iinfo(np.uint32).max) + assert np.all(ids == -1) assert np.all(np.isinf(dists)) diff --git a/tests/python/test_symqg.py b/tests/python/test_symqg.py index b1be4f6e..e1a8b265 100644 --- a/tests/python/test_symqg.py +++ b/tests/python/test_symqg.py @@ -45,7 +45,7 @@ def test_properties(built_symqg): def test_search_output_contract(built_symqg, query_data, nq, k): ids, dists = built_symqg.search(query_data[:nq], k=k, ef=_EF) assert ids.shape == dists.shape == (nq, k) - assert ids.dtype == np.uint32 + assert ids.dtype == np.int64 assert dists.dtype == np.float32 assert np.all(ids < N_VECTORS) assert np.isfinite(dists).all() diff --git a/tests/python/test_symqg_api.py b/tests/python/test_symqg_api.py index a3ff3fac..a00f0f7a 100644 --- a/tests/python/test_symqg_api.py +++ b/tests/python/test_symqg_api.py @@ -124,7 +124,7 @@ def test_train_only_checks_its_input(base_data): def test_add_builds_once_and_search_finds_the_vectors(nbits, base_data, tmp_path): index = SymqgIndex(DIM, nbits=nbits, ef_construction=60, seed=5) ids = index.add(base_data) - assert ids.dtype == np.uint32 + assert ids.dtype == np.int64 np.testing.assert_array_equal(ids, np.arange(N_VECTORS)) assert len(index) == index.num_points == N_VECTORS ids, distances = index.search(base_data[:20], 1) @@ -196,5 +196,5 @@ def test_search_pads_k_beyond_the_points(base_data): ids, distances = index.search(base_data[:2], 45) assert np.isfinite(distances).sum(axis=1).tolist() == [40, 40] np.testing.assert_array_equal(np.sort(ids[0, :40]), np.arange(40)) - np.testing.assert_array_equal(ids[:, 40:], np.full((2, 5), 2**32 - 1)) + np.testing.assert_array_equal(ids[:, 40:], np.full((2, 5), -1)) assert np.isinf(distances[:, 40:]).all() diff --git a/tests/python/test_train_api.py b/tests/python/test_train_api.py index b6982803..9048d5a2 100644 --- a/tests/python/test_train_api.py +++ b/tests/python/test_train_api.py @@ -287,18 +287,34 @@ def test_search_pads_k_beyond_the_stored_points(kind, base_data): index = new_index(kind) index.train(base_data) ids, distances = search(index, base_data[:1], 3) - np.testing.assert_array_equal(ids, np.full((1, 3), 2**32 - 1)) + np.testing.assert_array_equal(ids, np.full((1, 3), -1)) assert np.isinf(distances).all() index.add(base_data[:10]) index.add(base_data[10:11]) ids, distances = search(index, base_data[:1], 13) assert np.isfinite(distances).sum() == 11 np.testing.assert_array_equal(np.sort(ids[np.isfinite(distances)]), np.arange(11)) - np.testing.assert_array_equal(ids[~np.isfinite(distances)], [2**32 - 1] * 2) + np.testing.assert_array_equal(ids[~np.isfinite(distances)], [-1] * 2) with pytest.raises(ValueError, match=r"^k must be between 1 and 2\*\*31$"): search(index, base_data[:1], 0) +@pytest.mark.parametrize("kind", KINDS) +def test_int64_ids_round_trip_from_add_and_search_to_remove(kind, base_data): + index = new_index(kind) + added = index.add(base_data[:10]) + assert added.dtype == np.int64 + ids, _ = search(index, base_data[:1], 12) + assert ids.dtype == np.int64 + assert (ids == -1).sum() == 2 + # The missing marker is not a point, so passing it to remove changes nothing. + with pytest.raises(ValueError, match=r"^ids must be in \[0, num_stored_points\)$"): + index.remove(ids[0]) + assert len(index) == 10 + assert index.remove(ids[0][ids[0] >= 0][:3]) == 3 + assert len(index) == 7 + + @pytest.mark.parametrize("kind", KINDS) def test_len_counts_live_points(kind, base_data): index = new_index(kind) diff --git a/tests/python/typing/public_api.py b/tests/python/typing/public_api.py index a0b27263..f835bbdb 100644 --- a/tests/python/typing/public_api.py +++ b/tests/python/typing/public_api.py @@ -36,16 +36,16 @@ def check(x: npt.NDArray[np.float32], labels: npt.NDArray[np.uint32]) -> None: index.set_centroids(x[:2]) assert_type(index.is_trained, bool) assert_type(index.seed, int | None) - assert_type(index.add(x, cluster_ids=labels), npt.NDArray[np.uint32]) + assert_type(index.add(x, cluster_ids=labels), npt.NDArray[np.int64]) assert_type(index.remove([0, 1]), int) assert_type(index.dim, int) assert_type(len(index), int) index.save("index.bin") ids, distances = ivf.search(x, k=5, nprobe=2) - assert_type(ids, npt.NDArray[np.uint32]) + assert_type(ids, npt.NDArray[np.int64]) assert_type(distances, npt.NDArray[np.float32]) assert_type( - hnsw.search(x, 5), tuple[npt.NDArray[np.uint32], npt.NDArray[np.float32]] + hnsw.search(x, 5), tuple[npt.NDArray[np.int64], npt.NDArray[np.float32]] ) graph.train(x) graph.add(x, num_threads=1) @@ -54,7 +54,7 @@ def check(x: npt.NDArray[np.float32], labels: npt.NDArray[np.uint32]) -> None: assert_type(len(graph), int) assert_type( graph.search(x, 5), - tuple[npt.NDArray[np.uint32], npt.NDArray[np.float32]], + tuple[npt.NDArray[np.int64], npt.NDArray[np.float32]], ) assert_type(IvfIndex.load("index.bin"), IvfIndex) assert_type(HnswIndex.load("index.bin"), HnswIndex) @@ -135,7 +135,7 @@ def check_numpy_scalars() -> None: ids, distances = ivf.search( x[:2], k=np.int64(3), nprobe=np.int64(1), high_accuracy=np.bool_(True) ) - assert_type(ids, npt.NDArray[np.uint32]) + assert_type(ids, npt.NDArray[np.int64]) assert_type(distances, npt.NDArray[np.float32]) hnsw.resize(np.int64(100)) hnsw.search(x[:2], np.int64(3), ef=np.int64(10)) From 5bf7b1ff294ad5e760c06f4d49520f12a6f22053 Mon Sep 17 00:00:00 2001 From: gouyt13clear Date: Sat, 7 Nov 2026 22:08:55 +0800 Subject: [PATCH 11/11] fix(index): raise FormatError for the remaining rejected headers An independent review found loads that still escaped the FormatError contract: - A format-2 SymphonyQG file whose point count and degree bound fit their limits but whose records exceed size_t threw std::length_error. Earlier SymphonyQG layouts with huge stored dimensions did the same. - Earlier IVF-RaBitQ and SymphonyQG layouts with a dimension the stored FHT/Kac rotation does not support threw std::invalid_argument from the rotator. - The earlier IVF-RaBitQ reader relied on the stream exception mask and caught std::ios_base::failure. libstdc++ before GCC 13 throws the old ABI type from its iostreams, which that catch does not match. The reader now checks each group of reads, which also undoes the reindentation of the previous commit. Each case has a C++ test that fails without this change and a Python boundary test. The docs say that a failed read after opening raises FormatError, the release notes list every converted exception, the IVF parameter table names -1, and the txtai compatibility test notes that its filter no longer matches a missing result. --- docs/docs/index/ivf.md | 2 +- docs/docs/index/usage.md | 3 +- docs/docs/releases/unreleased.md | 2 +- docs/formats.md | 2 +- python_bindings/__init__.pyi | 2 +- src/index/ivf.cpp | 336 +++++++++--------- src/index/qg.cpp | 52 ++- tests/python/test_historical_persistence.py | 20 ++ tests/python/test_txtai_compat.py | 3 + .../index/historical_persistence_test.cpp | 73 ++++ tests/unit/rabitqlib/index/qg_test.cpp | 28 ++ 11 files changed, 337 insertions(+), 186 deletions(-) diff --git a/docs/docs/index/ivf.md b/docs/docs/index/ivf.md index 2047825e..669892ce 100644 --- a/docs/docs/index/ivf.md +++ b/docs/docs/index/ivf.md @@ -52,7 +52,7 @@ See [C++ integration](../cpp_integration.md) for build setup and | `seed` | Rotation seed in `[0, 2**32 - 1]` and the initial `cp.seed`. Python's default `None` draws one at random; `index.seed` reports it, and passing it back reproduces the index. | | `metric` | Python `"l2"` or `"ip"`; defaults to `"l2"`. | | `initializer` | Defaults to `"auto"`; see [centroid routing](#centroid-routing). | -| `k`, `nprobe` | Positive result count and cluster probe count. `k` may exceed the stored point count; missing results are padded with `kPidMax` (`2**32 - 1` in Python) and `+inf`. Python's `nprobe` defaults to `num_clusters / 16`, rounded and at least 1, when omitted, `None` or 0; C++ requires a positive `nprobe`. | +| `k`, `nprobe` | Positive result count and cluster probe count. `k` may exceed the stored point count; missing results are padded with `kPidMax` (`-1` in Python) and `+inf`. Python's `nprobe` defaults to `num_clusters / 16`, rounded and at least 1, when omitted, `None` or 0; C++ requires a positive `nprobe`. | | `high_accuracy` | Python defaults to `None`: HACC for 4–9 bits, standard FastScan otherwise. | Results contain IDs and squared L2 or `1 - dot` distances, ordered from smallest diff --git a/docs/docs/index/usage.md b/docs/docs/index/usage.md index 869f98a8..bdfadbaa 100644 --- a/docs/docs/index/usage.md +++ b/docs/docs/index/usage.md @@ -180,7 +180,8 @@ downgrading the library. A truncated, corrupt, mismatched or unsupported file raises `rabitqlib.FormatError`, a subclass of both `ValueError` and `RuntimeError`, so `except RuntimeError` -handlers still catch it. A file that cannot be opened raises a plain `RuntimeError`. +handlers still catch it. A file that cannot be opened raises a plain `RuntimeError`; +a read that fails after opening looks like truncation and raises `FormatError`. C++ loaders throw `rabitqlib::FormatError` from `rabitqlib/index/format_error.hpp`, a subclass of `std::runtime_error`. diff --git a/docs/docs/releases/unreleased.md b/docs/docs/releases/unreleased.md index 29ec64c6..27b31180 100644 --- a/docs/docs/releases/unreleased.md +++ b/docs/docs/releases/unreleased.md @@ -31,7 +31,7 @@ ## C++ changes -- Loading an index throws `rabitqlib::FormatError`, a subclass of `std::runtime_error` in the new header `rabitqlib/index/format_error.hpp`, for a damaged, mismatched or unsupported file. Failing to open the file still throws `std::runtime_error`. A SymphonyQG file of an earlier layout with invalid settings threw `std::invalid_argument`; it now throws `FormatError`. +- Loading an index throws `rabitqlib::FormatError`, a subclass of `std::runtime_error` in the new header `rabitqlib/index/format_error.hpp`, for a damaged, mismatched or unsupported file. Failing to open the file still throws `std::runtime_error`. Files of earlier layouts that threw other exceptions for invalid contents now throw `FormatError` too: an IVF-RaBitQ file with an invalid metric or a dimension its rotation does not support, which threw `std::invalid_argument`, and a SymphonyQG file with invalid settings or dimensions, which threw `std::invalid_argument` or `std::length_error`. - `ivf::Initializer` and its subclasses: - `save(std::ofstream&, const char*)` becomes `write(std::ostream&)`, and `load(std::ifstream&, const char*)` becomes `read(std::istream&, uint64_t bytes)`. diff --git a/docs/formats.md b/docs/formats.md index c3132475..9634b590 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -354,7 +354,7 @@ A reader checks, in order, and validates every size before allocating. The loade Steps 1–7 belong to the container; step 8 and the index's header rules in step 4 belong to each index. Readers read through the stream buffer, so the stream's exception mask does not matter, and need seekable input. -A file that fails any of these checks, or a damaged file of an [earlier layout](#earlier-layouts), raises `rabitqlib::FormatError` in C++, a subclass of `std::runtime_error`, and `rabitqlib.FormatError` in Python, a subclass of both `ValueError` and `RuntimeError`. `rabitqlib.quant` raises the same class for blobs. A file that cannot be opened raises a plain `std::runtime_error` (Python `RuntimeError`). +A file that fails any of these checks, or a damaged file of an [earlier layout](#earlier-layouts), raises `rabitqlib::FormatError` in C++, a subclass of `std::runtime_error`, and `rabitqlib.FormatError` in Python, a subclass of both `ValueError` and `RuntimeError`. `rabitqlib.quant` raises the same class for blobs. A file that cannot be opened raises a plain `std::runtime_error` (Python `RuntimeError`). A read that fails after opening cannot be told from a truncated file, so it raises `FormatError`. Index files remain trusted input (see [Index file compatibility](docs/index/usage.md#index-file-compatibility)). These checks report truncated, corrupt or mismatched files; they do not make crafted files safe. diff --git a/python_bindings/__init__.pyi b/python_bindings/__init__.pyi index 61e110cc..ef53ad8a 100644 --- a/python_bindings/__init__.pyi +++ b/python_bindings/__init__.pyi @@ -35,10 +35,10 @@ _QGInit: TypeAlias = Literal["pipnn", "random"] # Rank, ID ranges and Python bool (an int subtype) are validated at runtime. _IntegerIds: TypeAlias = npt.NDArray[np.integer[Any]] | Sequence[int | np.integer[Any]] -# pybind11 enums are not subclasses of enum.Enum. class FormatError(ValueError, RuntimeError): """A damaged or mismatched index file or serialized byte string, or one this release cannot read. A subclass of both ValueError and RuntimeError.""" +# pybind11 enums are not subclasses of enum.Enum. class FinalAssignmentMode: """Final centroid assignment policy. diff --git a/src/index/ivf.cpp b/src/index/ivf.cpp index 28c77d65..32e8e5c2 100644 --- a/src/index/ivf.cpp +++ b/src/index/ivf.cpp @@ -1038,191 +1038,199 @@ void IVF::load(const char* filename) { return; } - // An earlier layout. Its reader lets the stream report truncation. - try { - // An earlier layout: RABQIDX1, the raw-vector v1 layout or the unversioned one. - IVF loaded; - input.exceptions(std::ios::failbit | std::ios::badbit); - input.seekg(0, std::ios::end); - const auto file_bytes = static_cast(input.tellg()); - input.seekg(0); - uint64_t magic = 0; - input.read(reinterpret_cast(&magic), sizeof(magic)); - const bool explicit_padding = magic == kFormatMagic; - if (explicit_padding) { - uint32_t version = 0, flags = 0; - uint64_t stored_padded_dim = 0; - input.read(reinterpret_cast(&version), sizeof(version)); - input.read(reinterpret_cast(&flags), sizeof(flags)); - input.read( - reinterpret_cast(&stored_padded_dim), sizeof(stored_padded_dim) - ); - if (version != 1 && version != kFormatVersion) { - throw FormatError("Unsupported IVF index version"); - } - if (flags > 1 || stored_padded_dim > std::numeric_limits::max()) { - throw FormatError("Invalid IVF index metadata"); - } - loaded.raw_reranking_ = flags == 1; - loaded.padded_dim_ = static_cast(stored_padded_dim); - if (version >= 2) { - uint32_t initializer = 0; - input.read(reinterpret_cast(&initializer), sizeof(initializer)); - if (initializer < static_cast(InitializerType::Flat) || - initializer > static_cast(InitializerType::HNSW)) { - throw FormatError("Invalid IVF initializer type in index file"); - } - loaded.initializer_type_ = static_cast(initializer); - } - } else if (magic == kRawFormatMagic) { - loaded.raw_reranking_ = true; - uint32_t version = 0; - input.read(reinterpret_cast(&version), sizeof(version)); - if (version != kRawFormatVersion) { - throw FormatError("Unsupported raw IVF index version"); - } - } else { - input.seekg(0); + // An earlier layout: RABQIDX1, the raw-vector v1 layout or the unversioned one. + IVF loaded; + // Every group of reads is checked before its values are used. + const auto check_read = [&input] { + if (!input) { + throw FormatError("IVF index file is truncated"); } - - // Load metadata - input.read(reinterpret_cast(&loaded.max_elements_), sizeof(size_t)); - input.read(reinterpret_cast(&loaded.dim_), sizeof(size_t)); - input.read(reinterpret_cast(&loaded.num_clusters_), sizeof(size_t)); - input.read(reinterpret_cast(&loaded.ex_bits_), sizeof(size_t)); - input.read( - reinterpret_cast(&loaded.rotator_type_), sizeof(loaded.rotator_type_) - ); - input.read( - reinterpret_cast(&loaded.metric_type_), sizeof(loaded.metric_type_) - ); - // Missing routing metadata means a historical file. Its payload layout must - // follow the original threshold, independently of the current Auto policy. - if (loaded.initializer_type_ == InitializerType::Auto) { - loaded.initializer_type_ = loaded.num_clusters_ < kHistoricalHnswMinClusters - ? InitializerType::Flat - : InitializerType::HNSW; + }; + input.seekg(0, std::ios::end); + const auto file_bytes = static_cast(input.tellg()); + input.seekg(0); + uint64_t magic = 0; + input.read(reinterpret_cast(&magic), sizeof(magic)); + check_read(); + const bool explicit_padding = magic == kFormatMagic; + if (explicit_padding) { + uint32_t version = 0, flags = 0; + uint64_t stored_padded_dim = 0; + input.read(reinterpret_cast(&version), sizeof(version)); + input.read(reinterpret_cast(&flags), sizeof(flags)); + input.read(reinterpret_cast(&stored_padded_dim), sizeof(stored_padded_dim)); + check_read(); + if (version != 1 && version != kFormatVersion) { + throw FormatError("Unsupported IVF index version"); } - if ((loaded.metric_type_ != METRIC_L2 && loaded.metric_type_ != METRIC_IP) || - loaded.max_elements_ == 0 || - loaded.max_elements_ > buffer::kSearchBufferMaxPointCount || loaded.dim_ == 0 || - loaded.num_clusters_ == 0 || - loaded.num_clusters_ > buffer::kSearchBufferMaxPointCount || - loaded.ex_bits_ > 8 || (loaded.raw_reranking_ && loaded.ex_bits_ != 0) || - loaded.dim_ > (std::numeric_limits::max() / 32) - 64) { + if (flags > 1 || stored_padded_dim > std::numeric_limits::max()) { throw FormatError("Invalid IVF index metadata"); } - if (!explicit_padding) { - // Both historical formats omit padded_dim and always used 64-coordinate blocks. - loaded.padded_dim_ = round_up_to_multiple(loaded.dim_, 64); - } - if (loaded.padded_dim_ < loaded.dim_ || loaded.padded_dim_ % 32 != 0 || - loaded.padded_dim_ > (std::numeric_limits::max() / 32) - 64) { - throw FormatError("Invalid padded dimension in IVF index file"); - } - // Bound every payload by the actual file before allocating or multiplying sizes. - size_t remaining = file_bytes - static_cast(input.tellg()); - const auto consume = [&remaining](size_t count, size_t bytes) { - if (bytes != 0 && count > remaining / bytes) { - throw FormatError("Invalid or truncated IVF index payload"); + loaded.raw_reranking_ = flags == 1; + loaded.padded_dim_ = static_cast(stored_padded_dim); + if (version >= 2) { + uint32_t initializer = 0; + input.read(reinterpret_cast(&initializer), sizeof(initializer)); + check_read(); + if (initializer < static_cast(InitializerType::Flat) || + initializer > static_cast(InitializerType::HNSW)) { + throw FormatError("Invalid IVF initializer type in index file"); } - remaining -= count * bytes; - }; - consume(loaded.num_clusters_, sizeof(size_t)); - consume(loaded.max_elements_, sizeof(PID)); - consume(loaded.max_elements_, loaded.rerank_vector_bytes()); - if (loaded.rotator_type_ == RotatorType::MatrixRotator) { - consume(loaded.dim_, sizeof(float) * loaded.padded_dim_); - } else if (loaded.rotator_type_ == RotatorType::FhtKacRotator) { - consume(1, loaded.padded_dim_ / 2); - } else { - throw FormatError("Invalid IVF rotator type"); + loaded.initializer_type_ = static_cast(initializer); } - if (loaded.initializer_type_ != InitializerType::HNSW) { - consume(loaded.num_clusters_, sizeof(float) * loaded.padded_dim_); - } - if (loaded.initializer_type_ == InitializerType::FlatRaBitQ) { - consume(loaded.padded_dim_, sizeof(float)); - consume( - div_round_up(loaded.num_clusters_, fastscan::kBatchSize), - loaded.batch_bytes() - ); + } else if (magic == kRawFormatMagic) { + loaded.raw_reranking_ = true; + uint32_t version = 0; + input.read(reinterpret_cast(&version), sizeof(version)); + check_read(); + if (version != kRawFormatVersion) { + throw FormatError("Unsupported raw IVF index version"); } + } else { + input.seekg(0); + } - // Load the number of points in each cluster - std::vector cluster_sizes(loaded.num_clusters_, 0); - input.read( - reinterpret_cast(cluster_sizes.data()), - static_cast(sizeof(size_t) * loaded.num_clusters_) + // Load metadata + input.read(reinterpret_cast(&loaded.max_elements_), sizeof(size_t)); + input.read(reinterpret_cast(&loaded.dim_), sizeof(size_t)); + input.read(reinterpret_cast(&loaded.num_clusters_), sizeof(size_t)); + input.read(reinterpret_cast(&loaded.ex_bits_), sizeof(size_t)); + input.read( + reinterpret_cast(&loaded.rotator_type_), sizeof(loaded.rotator_type_) + ); + input.read(reinterpret_cast(&loaded.metric_type_), sizeof(loaded.metric_type_)); + check_read(); + // Missing routing metadata means a historical file. Its payload layout must + // follow the original threshold, independently of the current Auto policy. + if (loaded.initializer_type_ == InitializerType::Auto) { + loaded.initializer_type_ = loaded.num_clusters_ < kHistoricalHnswMinClusters + ? InitializerType::Flat + : InitializerType::HNSW; + } + if ((loaded.metric_type_ != METRIC_L2 && loaded.metric_type_ != METRIC_IP) || + loaded.max_elements_ == 0 || + loaded.max_elements_ > buffer::kSearchBufferMaxPointCount || loaded.dim_ == 0 || + loaded.num_clusters_ == 0 || + loaded.num_clusters_ > buffer::kSearchBufferMaxPointCount || loaded.ex_bits_ > 8 || + (loaded.raw_reranking_ && loaded.ex_bits_ != 0) || + loaded.dim_ > (std::numeric_limits::max() / 32) - 64) { + throw FormatError("Invalid IVF index metadata"); + } + if (!explicit_padding) { + // Both historical formats omit padded_dim and always used 64-coordinate blocks. + loaded.padded_dim_ = round_up_to_multiple(loaded.dim_, 64); + } + if (loaded.padded_dim_ < loaded.dim_ || loaded.padded_dim_ % 32 != 0 || + loaded.padded_dim_ > (std::numeric_limits::max() / 32) - 64) { + throw FormatError("Invalid padded dimension in IVF index file"); + } + // Bound every payload by the actual file before allocating or multiplying sizes. + size_t remaining = file_bytes - static_cast(input.tellg()); + const auto consume = [&remaining](size_t count, size_t bytes) { + if (bytes != 0 && count > remaining / bytes) { + throw FormatError("Invalid or truncated IVF index payload"); + } + remaining -= count * bytes; + }; + consume(loaded.num_clusters_, sizeof(size_t)); + consume(loaded.max_elements_, sizeof(PID)); + consume(loaded.max_elements_, loaded.rerank_vector_bytes()); + if (loaded.rotator_type_ == RotatorType::MatrixRotator) { + consume(loaded.dim_, sizeof(float) * loaded.padded_dim_); + } else if (loaded.rotator_type_ == RotatorType::FhtKacRotator) { + consume(1, loaded.padded_dim_ / 2); + } else { + throw FormatError("Invalid IVF rotator type"); + } + if (loaded.initializer_type_ != InitializerType::HNSW) { + consume(loaded.num_clusters_, sizeof(float) * loaded.padded_dim_); + } + if (loaded.initializer_type_ == InitializerType::FlatRaBitQ) { + consume(loaded.padded_dim_, sizeof(float)); + consume( + div_round_up(loaded.num_clusters_, fastscan::kBatchSize), loaded.batch_bytes() ); + } - size_t total = 0; - for (size_t size : cluster_sizes) { - if (size > loaded.max_elements_ - total) { - throw FormatError("Invalid cluster counts in IVF index file"); - } - total += size; - consume(div_round_up(size, fastscan::kBatchSize), loaded.batch_bytes()); - } - if (total != loaded.max_elements_) { + // Load the number of points in each cluster + std::vector cluster_sizes(loaded.num_clusters_, 0); + input.read( + reinterpret_cast(cluster_sizes.data()), + static_cast(sizeof(size_t) * loaded.num_clusters_) + ); + check_read(); + + size_t total = 0; + for (size_t size : cluster_sizes) { + if (size > loaded.max_elements_ - total) { throw FormatError("Invalid cluster counts in IVF index file"); } + total += size; + consume(div_round_up(size, fastscan::kBatchSize), loaded.batch_bytes()); + } + if (total != loaded.max_elements_) { + throw FormatError("Invalid cluster counts in IVF index file"); + } + try { loaded.rotator_ = rotator_impl::make_rotator_for_loading( loaded.dim_, loaded.rotator_type_, loaded.padded_dim_ ); - - // Load rotator - loaded.rotator_->load(input); - - // Load data: initializer payload, then all batch data, all ex data and all IDs, - // each in cluster order - loaded.allocate_storage(cluster_sizes); - if (loaded.initializer_type_ == InitializerType::HNSW) { - // These layouts kept the routing graph in a sidecar file beside the index. - const std::string sidecar = std::string(filename) + detail::kHnswSidecarSuffix; - std::ifstream graph( - io_impl::filesystem_path(sidecar), std::ios::binary | std::ios::ate - ); - if (!graph) { - throw std::runtime_error("Cannot open HNSW centroid sidecar"); - } - const auto graph_bytes = - static_cast(static_cast(graph.tellg())); - graph.seekg(0); - loaded.initializer_->read(graph, graph_bytes); - } else { - loaded.initializer_->read(input, loaded.flat_routing_bytes()); - } - for (size_t i = 0; i < loaded.num_clusters_; ++i) { - input.read( - loaded.cluster_storage_[i].batch_data(), - static_cast(loaded.batch_data_bytes(cluster_sizes[i])) - ); - } - if (loaded.has_rerank_data()) { - for (size_t i = 0; i < loaded.num_clusters_; ++i) { - input.read( - loaded.cluster_storage_[i].rerank_data(), - static_cast(loaded.rerank_data_bytes(cluster_sizes[i])) - ); - } + } catch (const std::invalid_argument& error) { + // Dimensions the stored rotation does not support, such as FHT/Kac below 64. + throw FormatError(std::string("Invalid IVF rotator dimensions: ") + error.what()); + } + + // Load rotator + loaded.rotator_->load(input); + check_read(); + + // Load data: initializer payload, then all batch data, all ex data and all IDs, + // each in cluster order + loaded.allocate_storage(cluster_sizes); + if (loaded.initializer_type_ == InitializerType::HNSW) { + // These layouts kept the routing graph in a sidecar file beside the index. + const std::string sidecar = std::string(filename) + detail::kHnswSidecarSuffix; + std::ifstream graph( + io_impl::filesystem_path(sidecar), std::ios::binary | std::ios::ate + ); + if (!graph) { + throw std::runtime_error("Cannot open HNSW centroid sidecar"); } + const auto graph_bytes = + static_cast(static_cast(graph.tellg())); + graph.seekg(0); + loaded.initializer_->read(graph, graph_bytes); + } else { + loaded.initializer_->read(input, loaded.flat_routing_bytes()); + } + for (size_t i = 0; i < loaded.num_clusters_; ++i) { + input.read( + loaded.cluster_storage_[i].batch_data(), + static_cast(loaded.batch_data_bytes(cluster_sizes[i])) + ); + } + if (loaded.has_rerank_data()) { for (size_t i = 0; i < loaded.num_clusters_; ++i) { input.read( - reinterpret_cast(loaded.cluster_storage_[i].ids()), - static_cast(ids_bytes(cluster_sizes[i])) + loaded.cluster_storage_[i].rerank_data(), + static_cast(loaded.rerank_data_bytes(cluster_sizes[i])) ); } - if (remaining != 0) { - throw FormatError("IVF index file has unexpected trailing data"); - } - loaded.complete_load(cluster_sizes); - - input.close(); - swap(loaded); - } catch (const std::ios_base::failure&) { - throw FormatError("IVF index file is truncated"); } + for (size_t i = 0; i < loaded.num_clusters_; ++i) { + input.read( + reinterpret_cast(loaded.cluster_storage_[i].ids()), + static_cast(ids_bytes(cluster_sizes[i])) + ); + } + check_read(); + if (remaining != 0) { + throw FormatError("IVF index file has unexpected trailing data"); + } + loaded.complete_load(cluster_sizes); + + input.close(); + swap(loaded); } void IVF::read_format2(std::istream& input) { diff --git a/src/index/qg.cpp b/src/index/qg.cpp index a299141b..cff2ab74 100644 --- a/src/index/qg.cpp +++ b/src/index/qg.cpp @@ -456,8 +456,15 @@ void QuantizedGraph::read_format2(std::istream& input) { ); } - initialize_layout(); - const size_t data_bytes = checked_multiply(max_elements_, record_bytes_); + size_t data_bytes = 0; + try { + initialize_layout(); + data_bytes = checked_multiply(max_elements_, record_bytes_); + } catch (const std::length_error&) { + throw f2::FormatError( + "QuantizedGraph index file describes records too large to address" + ); + } const f2::Section* centroid = nullptr; if (quantization_bits_ != 0) { centroid = require(kCentroidSection, padded_dim_ * sizeof(float)); @@ -579,20 +586,26 @@ void QuantizedGraph::load(const char* filename) { // Older files use 64-coordinate padding. Their saved rotation and codes must // retain that dimension, including when initialize() allocates the row storage. loaded.padded_dim_ = stored_padded_dim; - loaded.initialize_layout(); - - const size_t centroid_bytes = loaded.quantization_bits_ == 0 - ? 0 - : checked_multiply(loaded.padded_dim_, sizeof(float)); - const size_t data_bytes = checked_multiply(loaded.max_elements_, loaded.record_bytes_); - const size_t rotator_bytes = - loaded.rotator_type_ == RotatorType::MatrixRotator - ? checked_multiply( - checked_multiply(sizeof(float), loaded.dim_), loaded.padded_dim_ - ) - : checked_multiply(loaded.padded_dim_, size_t{4}) / 8; - const size_t expected_payload_bytes = - checked_add(checked_add(centroid_bytes, data_bytes), rotator_bytes); + size_t centroid_bytes = 0; + size_t data_bytes = 0; + size_t expected_payload_bytes = 0; + try { + loaded.initialize_layout(); + centroid_bytes = loaded.quantization_bits_ == 0 + ? 0 + : checked_multiply(loaded.padded_dim_, sizeof(float)); + data_bytes = checked_multiply(loaded.max_elements_, loaded.record_bytes_); + const size_t rotator_bytes = + loaded.rotator_type_ == RotatorType::MatrixRotator + ? checked_multiply( + checked_multiply(sizeof(float), loaded.dim_), loaded.padded_dim_ + ) + : checked_multiply(loaded.padded_dim_, size_t{4}) / 8; + expected_payload_bytes = + checked_add(checked_add(centroid_bytes, data_bytes), rotator_bytes); + } catch (const std::length_error&) { + throw FormatError("QuantizedGraph file describes records too large to address"); + } const auto payload_position = input.tellg(); if (payload_position < 0) { @@ -605,7 +618,12 @@ void QuantizedGraph::load(const char* filename) { throw FormatError("QuantizedGraph payload size is invalid"); } - loaded.initialize(false); + try { + loaded.initialize(false); + } catch (const std::invalid_argument& error) { + // Dimensions the stored rotation does not support, such as FHT/Kac below 64. + throw FormatError(std::string("QuantizedGraph file is invalid: ") + error.what()); + } if (loaded.quantization_bits_ != 0) { loaded.centroid_.resize(loaded.padded_dim_); diff --git a/tests/python/test_historical_persistence.py b/tests/python/test_historical_persistence.py index 6cdbfa11..d6055841 100644 --- a/tests/python/test_historical_persistence.py +++ b/tests/python/test_historical_persistence.py @@ -225,6 +225,26 @@ def test_rejected_files_raise_format_error(tmp_path): migrated.write_bytes(payload[:-1]) with pytest.raises(rabitqlib.FormatError, match="index file"): cls.load(str(migrated)) + # Damaged headers that pass the size checks, as in the C++ historical tests. + ivf = (FIXTURES / "v0.8.3" / "ivf4_flat_l2.index").read_bytes() + qg = bytearray((FIXTURES / "v0.8.3" / "qg4_l2.index").read_bytes()) + struct.pack_into(" #include #include +#include #include #include #include @@ -11,6 +12,7 @@ #include #include #include +#include #include #include "rabitqlib/core/metric.hpp" @@ -203,6 +205,77 @@ INSTANTIATE_TEST_SUITE_P( ), [](const auto& info) { return info.param.name; } ); +// Damaged headers of the earlier layouts that pass the size checks raise FormatError +// rather than the errors of the code that would use them. +TEST(HistoricalPersistenceTest, MalformedEarlierHeadersRaiseFormatError) { + namespace fs = std::filesystem; + const fs::path path = + fs::temp_directory_path() / rabitq_test::process_unique("historical_header"); + struct Case { + const char* fixture; + size_t keep_bytes; // 0 keeps the whole file + std::vector> patches; // little-endian u64 fields + const char* message; + }; + const uint64_t huge = uint64_t{1} << 62; + const Case cases[] = { + // RABQIDX1 v2: the stream fails inside the fixed header. + {"ivf4_flat_l2", 20, {}, "IVF index file is truncated"}, + // An FHT/Kac dimension below 64 at offset 36. + {"ivf4_flat_l2", + 0, + {{36, 32}}, + "Invalid IVF rotator dimensions: Unsupported dimension for FhtKacRotator"}, + // SymphonyQG quantized layout: dimension at 28, padded dimension at 36. + {"qg4_l2", + 0, + {{28, 33}}, + "QuantizedGraph file is invalid: Unsupported dimension for FhtKacRotator"}, + {"qg4_l2", + 0, + {{28, huge}, {36, huge}}, + "QuantizedGraph file describes records too large to address"}, + // The headerless raw layout: dimension at 16, padded dimension at 24. + {"qg0_ip", + 0, + {{16, huge}, {24, huge}}, + "QuantizedGraph file describes records too large to address"}, + }; + for (const auto& c : cases) { + SCOPED_TRACE(c.message); + fs::copy_file( + fs::path(RABITQ_TEST_FIXTURE_ROOT) / "v0.8.3" / + (std::string(c.fixture) + ".index"), + path, + fs::copy_options::overwrite_existing + ); + if (c.keep_bytes != 0) { + fs::resize_file(path, c.keep_bytes); + } + { + std::fstream file(path, std::ios::binary | std::ios::in | std::ios::out); + for (const auto& [offset, value] : c.patches) { + file.seekp(static_cast(offset)); + for (size_t i = 0; i < sizeof(uint64_t); ++i) { + file.put(static_cast(value >> (8 * i))); + } + } + } + const std::string file = path.string(); + try { + if (c.fixture[0] == 'i') { + ivf::IVF index; + index.load(file.c_str()); + } else { + symqg::QuantizedGraph index; + index.load(file.c_str()); + } + ADD_FAILURE() << "accepted"; + } catch (const FormatError& error) { EXPECT_STREQ(error.what(), c.message); } + } + fs::remove(path); +} + // Truncated files of the earlier layouts raise FormatError like format-2 files. TEST(HistoricalPersistenceTest, TruncatedEarlierFilesRaiseFormatError) { namespace fs = std::filesystem; diff --git a/tests/unit/rabitqlib/index/qg_test.cpp b/tests/unit/rabitqlib/index/qg_test.cpp index 7ece24ca..7ba5967a 100644 --- a/tests/unit/rabitqlib/index/qg_test.cpp +++ b/tests/unit/rabitqlib/index/qg_test.cpp @@ -37,6 +37,7 @@ #include "rabitqlib/index/detail/buffer.hpp" #include "rabitqlib/index/detail/exact_nn.hpp" #include "rabitqlib/index/detail/visited_set.hpp" +#include "rabitqlib/index/format_error.hpp" #include "rabitqlib/index/symqg/detail/pipnn.hpp" #include "rabitqlib/index/symqg/qg_builder.hpp" #include "rabitqlib/index/types.hpp" @@ -1638,6 +1639,33 @@ TEST(QgPersistenceTest, RejectsMalformedPayloadWithoutChangingTarget) { "search-buffer ID limit" ); + // Settings within their limits whose records exceed size_t: 2**31 points with a + // degree bound of 2**30. + source.save(path.c_str()); + std::string params = rabitq_test::read_format2_section( + path, rabitq_test::format2::IndexKind::SymphonyQg, rabitq_test::kQgSections, 768 + ); + rabitq_test::format2::put_u64( + reinterpret_cast(params.data()), uint64_t{1} << 30 + ); + rabitq_test::replace_format2_section( + path, + rabitq_test::format2::IndexKind::SymphonyQg, + rabitq_test::kQgSections, + 768, + params + ); + rabitq_test::patch_format2_header(path, 88, uint64_t{1} << 31, sizeof(uint64_t)); + try { + target.load(path.c_str()); + ADD_FAILURE() << "accepted records too large to address"; + } catch (const FormatError& error) { + EXPECT_STREQ( + error.what(), "QuantizedGraph index file describes records too large to address" + ); + } + EXPECT_EQ(target.num_vertices(), 65U); + if (std::filesystem::exists("/dev/full")) { EXPECT_THROW(source.save("/dev/full"), std::ios_base::failure); }

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 CW@_UA literal 0 HcmV?d00001 diff --git a/tests/fixtures/persistence/v0.8.3/qg8_ip.expected b/tests/fixtures/persistence/v0.8.3/qg8_ip.expected new file mode 100644 index 00000000..328daa94 --- /dev/null +++ b/tests/fixtures/persistence/v0.8.3/qg8_ip.expected @@ -0,0 +1,17 @@ +130 65 3 5 +0.7734375 -1.265625 2.28125 -3.40234375 -1.609375 0.91796875 2.7109375 -0.20703125 0.31640625 0.62890625 1.296875 2.8203125 -1.46484375 -0.171875 -1.53515625 2.46875 3.79296875 -3.61328125 0.52734375 -3.21484375 2.94921875 2.4453125 0.7265625 -1.44921875 2.546875 -2.00390625 -2.8125 -1.609375 -2.1953125 -3.625 2.00390625 -1.5 -0.57421875 3.296875 -2.828125 -3.30078125 -3.03515625 3.25 -2.89453125 0.54296875 1.21875 -2.16015625 -3.34375 2.84765625 -2.24609375 -1.34765625 -0.1484375 -3.7265625 -3.87109375 -0.2890625 1.7421875 -3.08203125 0.66015625 -1.3359375 1.62109375 2.5 -2.8828125 1.203125 -2.82421875 2.0703125 1.05078125 -2.21875 -0.7578125 -1.1796875 1.86328125 -1.00390625 -2.02734375 -2.69140625 3.0703125 -0.796875 -0.78515625 -2.34375 3.60546875 -1.81640625 -1.3671875 -0.80078125 3.63671875 2.00390625 -3.828125 1.8125 1.34765625 0.5234375 0.8359375 3.71484375 1.19140625 0.54296875 -0.06640625 3.25390625 1.68359375 -1.59375 3.03515625 1.22265625 -3.2734375 0.05859375 -0.84765625 -0.96484375 1.046875 -2.58984375 -0.3828125 -1.765625 0.00390625 3.5 0.1484375 0.234375 -1.9609375 -0.375 -3.37109375 -0.40234375 1.34765625 -3.12109375 1 -1.16015625 1.83984375 2.46484375 1.25 -1.3125 -3.7734375 -0.9765625 -1.390625 -0.16015625 -1.6484375 -2.6015625 2.40234375 3.03515625 2.64453125 -1.49609375 -1.9765625 -2.3515625 -1.57421875 -0.18359375 1.37109375 1.4765625 2.46875 -0.75390625 -0.6171875 1.484375 2.1328125 -0.73046875 -1.640625 3.1875 0.6484375 2.80859375 2.79296875 -1.578125 -3.72265625 -3.0546875 -3.57421875 -3.1953125 -3.51171875 -0.9921875 1.171875 0.7734375 -3.08984375 -0.07421875 1.25390625 1.55078125 -2.6171875 -0.5703125 0.21484375 -0.90234375 -0.65234375 0.44140625 1.62109375 0.76171875 2.58984375 -2.8046875 0.90625 -3.09375 -1.3515625 0.5625 1.84765625 1.8515625 2.03125 1.51171875 -3.25 -1.32421875 0.94140625 -1.0390625 2.3984375 -3.0546875 -3.3828125 3.109375 0.01171875 -1.3203125 -0.328125 -0.94140625 0.4296875 2.43359375 -1.5390625 -2.58203125 2.53515625 -2.578125 0.0703125 2.79296875 -1.49609375 +128 -94.953392 +92 -82.2493134 +46 -81.0288467 +13 -71.8557434 +50 -71.6811447 +112 -74.0330429 +128 -73.8238602 +11 -71.6039658 +14 -64.0551224 +31 -59.4229736 +32 -72.4512024 +106 -66.6851273 +3 -59.6906853 +87 -57.5236473 +127 -53.835289 diff --git a/tests/fixtures/persistence/v0.8.3/qg8_ip.index b/tests/fixtures/persistence/v0.8.3/qg8_ip.index new file mode 100644 index 0000000000000000000000000000000000000000..09e1ffa29c9780890193328493136f2a4390b27b GIT binary patch literal 113850 zcmYhDc{o)4|Nn-wGRddzTj|8d88%=7hnyw`4Z(#S%BKp=!e z@QeKSl|VrM@7@0Y-h<$~goO0}eYbqWA-yue20iltX3f^X$r?^-dab#kO^vWRS@&1F zw=R9+sNV3dhJ9Kz$y(WawtAG`wR*g5T6$f#m)Gw8bD}n^vaY6N;18cs6IH97r=X{P zYhJhYz;I30O~;5DInG&L+AO1ud8(hL~Q?f!L{1!r$6YCUpnZP zb&2<9O;^@T?YGw>w&~SAzj(Qp`}=5Z$;18{)~{$?GN+3tceYrlHv4xSFzBYh0gTbHy`*DF>{H`t4)moIeE-K|>3C;Y6_@lDhA;E=0M zoQf!QX+rZYBhtccv;7mx58dTQH}3|eCr5&R#4s#4XPhi=8he=B4c&1qb(`*?rc&=G z>g+qaU8~MJ%71jQO1=8j-n}v*-C6#y-Vzb$5`O+~lTy9njJ7($mdh=Z2?3{zD+mV@(5v!5NqOV^) z%zEF-zu7cee92?{`-S^c?R+4F_tLqCMuKZ?%0wmD+r)7bFdWMo1(;rxECbwWHL zb{9{$iwnoyEi5dV-3s#ZBwGflTi#m1KS-roXA3{QvcP{!U5<(tiMcCOJ9O9cUEcn_ zyZvXicN;mKchT6i!Oy|L#YlOV|J}VB-(qIH4(Q~_dG9LW@3pa3k_w=bd{a&=T!%8g zpz|#r^MAzi75(i$2j~6_y{7K+mY3J|B`L_s`TLVl-=bD=L|jZvQ9ip_IQE2lu*WJx z^T3zenJq1ocaE&JWom>4pjlEoP$W1BXjGEXABc-!cyT_^U=V5Mmiy7H039^))oL1` zalmlGVw5bq2V+(7FtBhZmgTw^8=H|6uxpiRgwxs>_)`y9h1Lkd8v%q3GIa!h1(cv@ zi2<<&#(G8&WX=0vS$?MkEQuyS)Ja2$+L_pLMGmk^$Oeb#Pe7Yx1z?K>piY+y*0%8h zV_6Lt`&t7&PE-SLyGl@O_!PvXHGoORQxKT;0*qd$1S69#z=@F-uqnC`9RE@bWUA@` zlGg#sKD_`*P90!v=Ob|3W)l2(QwMY_grJZ)15j=+*o3u%bD#NO3;ev1f>0p%Cz3s6 zG2}eN4`K>&fgFSEhG;_0LP8+hAiE$c5Kjmb5&)5f?1QX;=s-3>bRqH(Ux)+55)uqC z&0p{S=<;nlHhPU2m7C)5eDsVk*T-6;DJcAgMo@i_Vz!TW)cL8hXK!u<^V_dPXw4t7 zY)aT#asCm#>0ShOw$o$Gvms&kA*1LM54qrQf0|zC^|AxO%hFpt*N6B?q_vG))_*g( zoL}M^R5#C^spb`ZFW>h`r{IkFQ40oh7s)5FVV4zSz_Hgvq2VWDck<8sQ~!PGJ0-FA z_L4vqe-WXZCqwXe9gc>v`l(>Tv|LYt**LlV4!W{PI_d0Gu_T)IU&>f z;KE*l0bOI6+=6h;>zZril$D7D>Si4|3i-h+?m}MEvi?9jiX8>_pTmq$N0S(v1bBX+}v6?u6l^3u2m-K znN4BYQ`8*&jGPQY>aA@|n-l^&eWrair&BC^nGB|luZ?f;be|26y2e5$nM9SBr}--#T0$F3$iviVnoYq zl$EQ1JxTmWQ1Uqk)0l`w6$2CmW4l8H1i3qa^@oJ+nO;p}wQa%xu?P#Wv=?;UOT~yA z>rik*1v_g+5)h7VMF?b){_h`Qf?>@M=&^OnXyhLQSbvrrO}D=UT-pB!*mLT@_}VHk z<5LT)8XkeD$IpSidMl_kYzJxY+dzt67l`}P3)H8^K}dByAblSM1Zod(%E5q7Z98~z ztN|?Ye+E7ld;l@)zko|kFTuKk*I-ih8R#@^2M)cxVCURdaHq5z7%lk-mMIT_D)#}f zAKvTg5CzCd2<`yxiz?&*gaug%F@WIS;ST)+v4I#t4nvkh0wD-wAq01G2?Y1b2ZGlQ zxL=A8+)>-!Wz~Z~hWn+*#OoCaPrXOw?&MwBp6PV+l04ehloUmHd|Bs7 zo|dkQ@RaS%LVxt|?R1~`b0BlCOj^fCuB)7wRLo$~|Cp>K+c@u{f~!eSNEnhPJ4vDxFluZ#nC zc5H3O!{2TB_wGN*h>lBT$7iqy^4p{PU#BXNL`;hmTX_;goiQ)$()-+3KRRTh%&GLo-fcqj%t*5`wSIH@OIJmk|H??U(%hLU(@+ z)$`vVI6G|W9 zoA??S8Wx$E)sN9-==zg7nMu0y6(KXBp*uq7XEg*%P{3UXGW;}Y1pYK&9oYpxQr2|FX`+WH7YE7yWQ zx&pBNG6pJZN`bQy0K30F281^lcy8SWgvBBd#}tCSt^#n>rW;tR^T5q_L%@5W0WAHu z4+Mj^U~SM_;CJK`$U+AJ@f*xC_a1P|z8{2r=mJ-Bn?T{#kKnd!8$jz`fMfCPvuqXQYD_`mrFf5H*InL*TpBv;x<(^6y6x)W0G2~&qSqFG zcAkB(hs(aikQ=_o11?y-JLo>ckI9Kk_B6XF2;?K*i#r8Rv)Lx886Jmk?hZcSXEfyI z;FDZ;JLCS|Q})SioK0+{Fn!+^1-^M)Z-(@h4~Xqb6$zG1&x)B4*A$yT&XCRpr4y;g zb=IR6G7ZcgnZXomx^)jR%%Y4}w5yvif|K02Vv3xhDZFFf9hshDOH;C3U z2YOQmMU2cPgb0>&O!N{CK~g$QnoTgg-?UO;jog?0&;BvGxLKYNFKS60Y#zw?N7&xq zGH@*uq2t$4x(OZsN{TKeAt3=j2scPCkU&X9gn&3pzpn53FJ5R@+i50f0_z&SJiahF zJv7wbE7D&7hdJvZ63z6z_#Hgy?*+v2d{!Sh(;D|H%SB^h|WY(&8x3?-a2)(5gzFoNYg!jjEJx-73@V||J= z0 zv#JIs|IG)t6>`DsBo{ckRDyDPD;Sul0mgPkV0n%JgmyN8r9<7|m1!5yR2Bh_Mm?zK z3c;^q9iSAnfiLH}KyDHbICRwl&C=(9zVanVE*1jW(JpWd4tKl57_Ud-xD+h4(t1Tk9csp5Y!TL#PnEF2voyUBW%u1Ho%Y+!4I)TMV&<;Ld45 zv>~{Q|3VHzG$0ot&JesS)`j41k|B7GZil4!xu?ewGCb})=ytvk;dAt|RXQ!^5Qm)H z$S(7`@IEo*p7+g@$oA`pmfUoAkBhE5bu6GX@9MdSmo#Q}IGgL4=;31K(jRj*sB|n^^Odv1amnE@UCtQ+X5!aF^6Df^h?QtgbW5~fp zue8rfv(hvUewB+Yt#!hre}`F7bJ3`H@e0j%OHWH3aL8Wav}lvxWkw2i{tX0ie1oXT-basFd_|?}Dp7(-5nvUq#PXA+Xn=PF`kzS^p7;bE*XW13aKD zXas(V^&ql92sC|#VEw8tV4~6m!mHZ=%@%+_sTZLAdK2(l*#vw8yMf>8J|IPJ0bUVL z!RX6wAo->p+%6XZn|pmAZ7T*Aq}Bu6G5j)z;B}oOgaL7b;5DHvLCa!&;=>PgAWZ;>G=UEPr|BjI z8fo!rX@y}KsfBSJH`-n@mK&~EplpCjAofOy$n%vSW@sigU9??g@5K`jCs!C|Msrd* z_?3R`?;CfVD=vCfT+M}7aWS`c-GI$>O0VreN-WcYp`5b5BW^@^L2M`N=zQP$zSG^A z6~15CKwl_ojXgNLEd{$H`!KVz$1TX$d$O#oKussEXuPP78mhm8@~c37-soMj^UR%@={o(d^P$?)bsqn@1b%-z ze4-37mwtkl3}>PDETm}!!d*VH^#Y1?`=Nlp7W-Vkk4CuJgt83wp#&=hZJ2urBj5jkmUM+-gblY4*4#makg8Bi_}q=KeC<$a)kvD!*UdDwkJdE8 zlVlWZ{sj^>Gcj}QGm7lmh}y9qf+(p+U_`SVBz07QTiGq(`D7JvjH?G$Wdd-rxgE$f zH-de48bSA$G4T4v8&J{t5qLd#4YJow1LwE_kVAh5y43ot+(0QAXGpsn%>WGrn19-}RQHr4@fcW{s3U;J;qs1M)qx)65;&n7Pj z-fN){+>N~u+@muPyf(!BSp>o7A_&9)f_vo&k%Q<#EFoqP+&Mh!lp&WPxQ9$no4|}v zw_2Yk8*L6;HnHAOX#CXm!l(gq$-D5u(1RQ29Y5y{UgcE!(Qo_Rwf5n?E%Hc+w2e)E z6lfW`J1XfcWvq_j<7IOx;N%SbsP)3sD^a(3snlDh2Dkm<8&+S*?^+(B|6w_qKcx4x zh!^lnOrM(n{ZMb$oYC(4<{pRxUazbB#UCyxV<_z?Hx8mN^X);MhQ}61R*em z^C!PBovcN%Hh$eN;(hlS0yih8iOBco4V|J~{ADD`#06A}Eakugg0c!FhcNH6_DHKI z($za;o@X@Y{))Qkgi(OsCPN$VO?lqVDnZ4Wu^+2`_Ool-VpsD9{)nnLoapqbbPkWh zVRNEWIgh>{@wg^2rCJ{}_4LzxolAELv$vPY6fwI+B1U^AQ|$isl6XWc{#7d$dx$;8 zRS#&?H>+L)F@!}@^=dUV29wFK5d{Y~*!cQ9H|Sv`7+1#>#boBUaXwYEVyf8F&Mr=d zPChntA9n*cCqp_b{3AM?Rf-a>uz`6^6qWJZAUOx+E?WIYzZhOKLvW&Q}E=A5UAEY1LC+EuncPk zkL+K8E$t)VU(a_y77PQG@K#_8=NTdD(3=ZFFcB{RGM2;O=ZR-vTJANtwP_5zb$JEu zyz2qAl|z6$`~tLw_kb$f_aJ#j2;BdD362CdgP6;G;MwOPEg>NP>Uq7mm!8+)O?2ngxyv?>tIAy6YOSt2=Rfe2 zci8N?c9UK2-l*eVx3f*Y9JwA<8F0*7GR@mN;AGmxjFWX7iK9Ni<&lP&#Lnm&x9sBn zeaF&&#aztywUGZTFE3%e#{2|{)NP^Qcil2NW_3%X#DA;>_bsKYnnb0(F9qmqkMxfi#pg&#WfdTTRx`whylWBGaQZ$bXf&QuqN01s8~`T5z|*}D0u zc^+>T%0FLSUAe4$OTp5r?fE$s`T1JHwbus^wRXgf#~$e~%)BO*&rPdk!?b-+{jeZi zt-2tYA0fAA36B%DlTTfqR$l%=WHZg|F)}%2cO+EEG!cr)F1h;)+b28K$otqwC@%vW z&&G0TIW#WUVO6-Ax{7nKfwP~#-)G->4~9?Zygx&e#2maO&ycquF)Y?uSctzDf1jD3 z5u1r$kBH|z#4=wgk1f>~zsvsao<~U})DcH}F+xjO9c%FzqPQ*w1wt9r^Y?8u>$pEH zb}Tf7fUP9Z!V#y9mpgNk`3x@=$Q!QIK`c z2_=M)0O4|e9T@IGSUOFBY{o*#$ka1(EQ$z;g-%SyKo)W$&D>mjgM={<;txn+K#8*C-< zL1aWHSh=PF{IqQWE0W9p)`;%gM}VbLH?XHR0>+IOK*P2HP-L4yR7fKr$->;s9|U-B zg=Z3e;SS)n;5rByg69^VQ+P&gfLwrVh2ZYsPU5+@5Q5J-@O})>Fx($Ih!h0R!UGW8 zTiiJ-h%IC-WCvtD#MLJAX4nI_iW@;*`!D5jo+M`P~nhz zgHx0ochcRg;jqo)OC^pcb52~!c8@-GyCKf)sz-XZe`&7crHFgyD-K&&dwa4j!a*eC zvEz-Q4FAijvJv{d7mWFAaikqQG7a zbaZCR264IBin)TZytmvfv0J&W8>Q&PF*^MU!JbYq&7fP47mDJ>H5=<^)FPT&C;G=> zwChyzc+|)66oI0nQr>^787 zOhJBZ!GmmW8duIy`?QLzgywDuWjU(ECQ`|XL6po`hY|O7VFjH`X2nS><2%2AAp7HFu3*n6UaZ-4BGO#!L<+F;J@9yVA;SJ=xyi&m9{-# zzhf`h1MhWwwt?4)XCQd);C&PB67CUR`{6zuhv04?5ZoOG1oy}TvKm5z;GQW!@LCl2 zauZ|$1g{Zs58WVmhA|;{{fN&-j2#U?NJ&iDJ9b85RDp4djSpS%{L>R_y`!JxyGAd) znfKJ~RMu^eM>lTS$I5YQ9$H6wiqAYxGCjT9#&GAMBbCfEoJQ}wjUhK%oHI^+vA#O= z_;`TR@p<8kidx^&%9e=KJt6uR|5?tz<`7pr@&4B}?s+q@_|y2@@&!v{mTaI<2<*cG zL3Ebd#%{XSB6$+YK+=Bsf-EXB;=fd@?vs|v_7laFTrEkJ$Go|^`IzmwrxFNs1=%JA>!vTJX>i^U_i#RKz=%`Y;x7B$D+evcGNAB&4C*rGLFprv)GjQ91cdO^;uaab!Njm7x^jfiMADCt7|h-!9k7b|b)Ljk z0>Zl+fVKG{AZ)V3x`wAQq~!|&=v4?S@B>1=S&1PlOA&JKJo@=fGG<>egJtn{U?tsm zP@tnM0MY#jtL`lTVcSqoD-QvIc@gPqdWNyoHy~N0L6q=ivw)!atIqt$Q;hIS1!Ha5 zfL!JSAb3#+Jkv`+(0_SgB(NHsrB;BaE~Q`_JY&ADUIXk}9|PwA6`FR&$ z=ez|sdq`5bfq4c z6c2)ZXWs&=o);j^a1eyT9|+!0orN5RI6&~s!Fw}2r|=xY{n!Y>vkUhUch4DO3bBW5 zh8%+6epy5CdJ{hnFo)nS8bk2RG=iLjEQ73qxI#vb&pf<*BI-uk;mpTzW;ZPT?giYt zu6;7*n)6l1$Bvfw9vZioUAP#}{*W1;>=Kb?9C|zCY;L*nZNH0;LJ|eaCF@-SgCE@D z)aHhi-;WHBJQW&n;MRR_w}*FblwEdBDvtOs1fEl8Xz}5ETMChofkVF+pHOYAc6ZTM zGKCv^S3Nu5!^Pdq&!_R!2t2@bbN}wwYdsz4a<%b5j!*E9O^VEa{mdNv99Hab_H%Yx zMce7&ETq@d=`syc28bcu45iaAv>YPGy>&OfiYe;c6TQv# z$Lg0yGc(%53|J{d?-4SkEL&ZjLQ$s_6_jZW)V`d$JylpE8hp~x*%B{&1Rap1B*0o; zDoT|QE-8sTr%Tdhq$Ex1F@m`iP5H4>M4FWnY42kHUHbiH9ywh7sj1eGii&J!kKEYv zf<@!!oDChxZ#Otgx>1}ODXS&q%q0?(h)S!7B;;t4M2qMNCnB1Cu;q62O=NI&SiV#k z$}(2K>_yEO_>Cbf_an$L8>*o2m^=;0=VAoT060rf5Fm-$QG$aAWbsu|Pn%(MFzo~l z*{Y7RQu!#ju8e**j1-WI=1_8U3aXYu5U_Zk5%T7J7`Qx-q@KKnc}AzBpy4eb(_Jyv zf^}%3#5hV2-9rq*3c%)y3Ls-%1lF8-4A!<3gIC9RV6$rtAf%N2ts^str69qs8OZ-9 z06ioOytvZ>eyeu_o^v12xCYPMj*{Nem}APC76|a1q>pd1Le5qAS9vzY*lLj zx;q9ym+2@-JTwi2geTy~w@y&W004JE5ki6BnS|GPxDR;lT!bu!;CZGD!TY#15EcYK ztG0yTH6|T`dxg*G_d@Vo!!r!`$N+-pmM;YVki&CM4}yD*d+Z(`o1Rb^aQmF|1=q;P z<_m(u7qE-Q93;gz*CD}{wj=j!U7k_hRpSIl30Fs}GY>AO-*vg+6>%%N(Uh4M{>79{ zFF1U!npt=AEIsy?eH4w^*5Z|LnGxZ|Pq}zLJTBsAcZmL0_9FiM z5PO206)sIt%$9x-PI;iLPE;wKfAgb8e1AS`P8ecS{5GA55s$5vz$B~)%&>M+MuUqH z&B*~5(OK2Wx$&fvt<8|BXsBB>b6NDK_qAx+biJHKz`9$e;#L@P*da5*y$fXXX=NXgC82Ux<#G+r<|(8 z(=4Z!@wCdSRmyazgd6fC`IO#c>qw%7siYLqsotUK9>(u!{{}d;5HT6&9VjXkayW&e z7EY$9tCPLvLyf+9U4G~6?6BP{#KZA5!?)B|VJLVuct*SL`LwS@^zu}tB}sgCRWvCz znicI3>Z<8>Kf8EG#4~S7<8JRNPE{ z6^_%$H={A~sat5`^VI_5=Ws@HcRNacqkxs%&qwW_U%`fpd(kYvrPvsIIc<1zF`EDM zjKKc>RvN1=0wDwovCnsw(ZUj|!ADvX*s!1qh%P(=FNb;HlW#e=`==TlhOj0NB#Xu@)6dYUg4k+3_1y-@oK$hSgc;V3t3}F5EYeg@3uGklZ`@cOL}rwOA0mx5B-^>qXoXdkF3j z?jD{$cp93vg}MtT(3l$=>)PHYPtiA19g7ix}_4s0U zxD>eE%|22rd>^-c;=KVpnKhuB8=P?~5*af-_6@X8^46XZz0Qc|zgmAgqeUbf9Q-)g z{#!IOks%yx$;_3NK*PhyTj5SbO5$}Hq<8)nuY8Hu7OPo)diDOFx7a|@bfNL`FcT9K z`notNHf)=O4J{45uT39|)(;9>o@@L2m(J?%*0~d8r9Ho#cQ8)sN*E#^8{M43`O-^J z6bhR&1_zsk{ctMT92YONYODEER~-~HyME35(6>C@iQk_Fo5f8NmnY+elM@{s9pZ6k zr@79Fkw_b{iJO^EaMf7drgu}ZHDeE5f6Z6_68-rz_kCV8KQ#5a9X3{CYm2_E3pAN=+6_dzIRyZV^D28WkxvPfl%x5p%w=p7-mzuSY)N+W+ z;aJFCuj4(b;^=HCFWaj#JIl-2(knW(($tjEexs*hNR$%lLH62Cc?{>b{#rWTC-=#D z4{uMaJXiZ;jR%zyxOzt*&saJidm?dwcKHs4RN?uigMxDM&Ga)L` z?Cmz9z&#UXIj=y7axlM)P{i8r4DE^jY8ujU6Y1JgiQ+T;lob`=vS%syb+iVYau9$6 z%1{2DnWffNfDez#fks@0!KPfMOl;$fCjI@C>VYT4Lf)Nnc^Aem7YX#ET zogk*J3|O4&2D;~Z0p&**=&yMT*3){yqeJaL>e+J;bg&iNc+?EOetQGH`8@}CEr{2S zcX`y-rHNoz%hLdikcd+*(4=Vxbx_+`{r=|s3R>g4gRIW)Bj&Kxwb*0_vS+jXnQI(t3we@}Ds3OR#D zHvN}J7+iQ+U!X(ef2q4&XA&Jdo|WGgGsq208_QPDeRgPh<3@P;!zKNZPgTi_Ree{?@nfAZ0O#p4sNY@e5wdey1#yyC=}0~J(S>dNUg~I{M$j+ znDY_dx8OVft$*dh`@@-K*rtD}N=L^Fm(q6V##cG$AmYnK> zmh1NxYh2Wh%%Lv~&$Ed1fjwhU(YTm9`*3W0Vw_7EN}auUe4#X0AH7h|Nx8XHKO?|C5#~ zicdeE%8`L<%+!BQ{M+CSkj_qc1(X#YIfuxAUn#s_obPeB-`5mc23{+>A-tY`ztANs(ftrg%)W+%{o-2zN%UVyj5 z0DNY2gD$mxkaGGhsBag7yWCIUt6c4+5 zjuKhhYR(-`^XvrY;XcTVb^JPyw~60$_4O^w|0|fS@&5Yhq;RWD-0hgb@rjnmW=_Wi zx}g;Py$O2AsounS0;wN@fy9RkR-)$v?Je<{mBNwoskxDn^Wu)eYhqz*hmo++*vX7; zdXVli(KzyKj-sj>Q(YF5=UzskWP7>pOQLF~d+SH&s8DOmc|SQ+(&`dwhBvF6*DNhj zz=>7L&Zdl<-&gpO%grwK5EqSq>B;DgE=^C#|F(JEW6sYc*TX^XYI$GFwmamy?pAjA zc9VWE_rAXyvf$glG{ci_7I89$CM#Y<|5?-`g9+o}+17CdH=2|V-ZSvrs&SDUbzRhZ z;`NbvhDg*cn*PFMW-3@Pl#oB{^9cLgor>E3IfRv5F-Gn8 z%g{9b$;ULz^#!cz5y0XqfUGC}nCJNySmNf#DC-p)EjceE$nt@^!oS|30X0i$C5Jy@ zguX(3ka!yze*Z6K-|3Da#S2m5Hke^!W%{2zYOt;kH!#*eAF-~~F%;4Jjv>3RBUuyC z82>>DSft4ZZ9PRmupI8__dEiXfyKZl?GdPuDFh`Cs(`?}78u-b2F62;f3t1t9(c~& zH1&6v(I93Fl)FC%avvT8`LtnhF?Rs?>h*#kxCa@g*8@h54+4cB)o|B%08m2*!OH^; z;P=~Z&^_G=Ty%NB5#H-tAvTbs5WN1o067QQ1~Gx`hv1onLU1>nA^RYb5Zo&z2p#e- z1osWkF+A@qAU+V>%>xjB2=1L01osSgRO=?^!BrMB$sx`4NYsCqPx5XJ9lM>`=u>|} zM#n65ALBmZV6XO_hrK8A4kQ(}UtDS2oST*qdChCHed2}mu;Q8&J&%Z-sQux0gU|=J z_U+r7Cx86mt^8xD{%Icqa*B5##W#cX``hLCY3#ySAZ91j<;TS3$HvIad!G5VbKiS+ zNw2OERkN~$U%ws&J+8dSS$B8wgD2_TJe@Kn*)3Jk)N&0S&1FhUHA(Q1lXawM0?Fdw zE_uX!m!A}K$tu#51(MPWKmo!wnn&CP7KbR0Xs1LQ}+tSKtqes}4uDak$HMngY1HQEVoy?+J<2b#c# z`aZCJ`UOz7eFNO#tbWSx1>E0%0j}!21RDcBfWw+SfVx`LPiX#d&B$ji3O z>tW%+M#fu=Tm!A$(Cjj!l)D|QWZQFBnVfUikGm!+v-H0-8}lo*y|umlLuW!AT|@o7 zz4b|ZWv`Ir)+`BMA|ba%ZvBBjB9YVdkjcqY(^i?j4MVm!1{$I{8S$B*eemF&-j?VT zp|z;1-qhMeanPD!HYm(K>kJ>c5=RwrW8TkoU7HfU){b*Uf}MEA#bu=znd+_O za7NBc#Zh~%F_dx2(ASPJH|_k?-@sJZ(FeP{TA@hPER1exc}IYqSH-`jygG+ z8PT0Aye)Q8t&9}piHqe)79<6+jyrG5;)<)|pWd>I3RZHL{u+1p4{4k2hMuM@kOo6A}M-;iBh!W;LBc2~)(URvoXj#i7F_szwB^Mk- z38|+6a_?UZxzvax?thOFK6UC7{tW~K;t$HomDCnMnHoqoU#7y#_SPN1MW1QY|h zf#B&RxE=Z#-1#~WdXGE~`(8PfO7eMVl4%uomC2F`I2{{#Ho)wTo2BFafH3{*dl5eMak=+meUjg- z@3w$)Zz}rRt@wk-jE%zg*$#i_7xPnDG1+V`_g)q|oSjW(XG^bOv&V#7F1vN9ps4w} z1GvJBUbibr#QZLA5yb2|Wl$>O*Hwj{%B;)$(>9R#hxd;1L;3LPyF|*;r7;u-ipJpY z>dCf%$;nf0vy=U?t)10t)+Q_L{h}7^+8M#H?}B0CZ?c)L1MIt zM9e;$Q`4eQg^SKAo~yK6jCN9-Xj)z_bZW=@xQQWQ%)~_V&*p^D)Is+Zky1`lQb3wc zXqJi0LbM!JPE#F5^8tEXb;AI#M~n5%`4#f{&-_+|Q!Z|LYL#{A>y&`ICWd6!fE z-{fpUMk(GZtNdrle)=@?pC#C5`7DggN_Z^@zwC~P0@{b9Px5|sTdfWNl9vfYQ?5hRTUcZvU z4^|&}&)qE}a;@2g_I(cvZe}`1a<5Rl=n|(QmivYHOy9 zD*V3Z?aSC*x&COZqKJE|?E0&i>;5Z+!MXjl z`_!$3ZeNk;V&6QFiU!8U60kjBv7}My$e=mzw~pSSvtGNsa%Q;xZ>9IaPLrFjMvjQ# zMo|h%6iLZSN*tv`_~T9`aTG~Nejbu4C_|A<4g$3|IzWXN!*t zhO<;rd$rpri)@b&PPn1Ox;(68A?&?cmI^-a;iC2pwipsyiI^|`S3q9ojrM!V(ayfq z5M)ud34pr<%5sD|#`A~4SmH7Pi28}y_xod?-A6Ej-lo4Zkg+Flj?q{J-u+hrn3Lsz z%`OFRj0C`^rU*=)>Hv>xpMu}vEdcnIg7XV`e|PvLyhXt4btkZ1)(>ow>p{!M0T5mz z1gsyQfZd&T(ED)+7_SwAFK4^K@}#H0dT}?fi+c+`r#1e4R@5r`DR^A*2H^KSo-uf> zs06|N!ZT|h1h4s65Imo7AMGI9Ah=h!W4NEVf4Dy@AQvFGPbVPG5ZpJsro?kj1;T{j zK1)ER!PSAG@eTUO4;w zfiua)9(>pP>?N*x;Z>%GRFcn!6{LokpGr=Ws2VvOv#&5SHzDszZp-_tXK&P5zX^zb zn$)Torms@|FJGyYM1%_qCGb{-(=U0Ytm2o^icD_Ie(t)6hp8-0M;cjDSxeP{D(kR- z;<$}6EBgG3pI-N8_S>Itp_!GlbSIV}!O6h^_E>ahdm3VBU}n(ZXKlmifyV>zA13fP z^X^zf!{z$3i|ZK{)*Nx+>aY@S#FHnf;q17mW9(E@5|zZET6B|$$@i6r&qIDyy z!&&OutfK6hK6ogkkI^UI%n%&#rrcfM??eh_oV zoma4nx@A>qZxmxKm2in9mn@f5CHi(};7%1spHrG{tCUWquAxvV$^NK=9219hpaESB+J8BO@Si>5M*sIh=bB$0@kOSK&GDXQw~ z>fBszE`dT`rbLjI-YnH0I^X~u8 zH!}PlcR2upJB!aI@tQK~gXP^z%h|DODnd$AQW6~8u6w*X7Ih}OCn}a278d5^DL-L< zI^qN4q^Ib6T;-lX?uIap^Z8C!?dSKDg|xXix%NBz+!NBxqRzT^*q0`tSJSkgAJ?M4 zKN9cg=bUgN)cP<>U#z^8zcfdUtIjJUAK15aUp_ZWOFa3y(PyOhWP_OAIc3r?`Kcg3 zEH=L&rl=?;mK|MFRC9&8-AX}XRi=jg%XK7WO(6>|7sg~yjE~o@5RP%jRde0AYOYiL zg0{G2;vX@Enie*ia=UCLytF-c$&-9bs$P1ZJ+2jORkfihA-E~e%G7S1=(W%wrKicd zU1Y)-OueyAX&|*fT_iGJverCopo1+WL^cbB##`x;&2c8umo9zUcTwlb`kh_w*Wq-lg(An$>Ax> z>ge;RJax5f`MuQZ`}R?nq{Zgva~~CyRo~rPHLJa47gc+S{tv2F1$CD;HD}5>H#cf* z!}~FZ9dzgHj$w1SpLr@h`A_+`2OLgNmCp9F3+yW>+qvq_6t238Q?|2sq%n5_o`j4P z>A`=bWTd4wi1*}EdTmto+?%T(Gapc@%c`@q9X3})B-G{pYUY0yEkZxHFUGp$Z=%Ti zm&otO9<=_peiWpyKnW_==x`X^Yjk7);`A+)oU4tJdD7VX%|sgO@K=P)R7bOP4xnJu z|D);5Vxm`jfEfbQf4O3JqpU*UwN+Jf8WkM9PGfEgz4TeaIu_a0jS<5!z zd?qwBwxS48NwU?wEh;g;bNn9PPxslQ$9?#x>GPV``@GNlyiOWUN)Q+@l$Q&>JdPt6 z7U6j1_Z2X&U_W4-RU-!d7ZRAozi^)GAf_>l2}rv404UADe5SAB7cZBB=V$JL-~*4q z-aiijN38<9`1BZ@{;vwCRn~#jo@e04#~Se7v}P{9K->ZMqn`rc&?yAl{(c2~HoOE6 z_jG|j_Adc9={cy~I0X8Ko&a5gR-jt-66mJB2cFMrLCU}j@XfgmC^G;!^bCNP8BHL5 z{WGu;zU!z?G=PvH7a&>?=OD#-FB-7I{QVU`nY}zIlZLy6pAWETc4bmsk@un zmgSl=yjgOTktMyDq?ywn`YOG@yECJ|r>(!b|4HaqgX&kk=ap~gR8f-FY^qX<(SS|D zm>6kRL`)1TmrXKNY;@_gb_=jJ^rka|nDqXE7qQ`;Pm9WG>%`uV2gH#o8m*p@OGihA zW?6;QA@$iPkH5+b-W^buy(&F#YW$pt)ZrscVRyhUxQM-KwnCG5s{03?O76SMs^fPH zw-yPtsbEF`QF+FRoKV+Z;;Ee|fNp6N@E9`JuS z?fTJO*G)&oIh>;DKL)i#*&$C^L7|}jpFBm(3_mg8|HFSEaGLo&+;IsxK)@Nst&hN9x-22997AMUkOZWr zZw6ZQE_~(tzXYUveFnv@dBm4}mvM%rETN^Af|D@cy{!2MG4p44@SX9})WLvL1nWA~ z+m9^Fyn2GbZvBTPoS9E0S*T;O2J@-O14pQXigdjA`5U~wubN;i>>_-$ONc?C3~nJ` zI+s)6CTN`VGIRz_!yS(L|J4ktt6Ra1{~iL_@n#Tvpa*o5g&^yD6R-$>4~9A40Xwf2 z_>H^-|2g%{Wr6rS9t967K7iB%BCtsQ9WXjD047-7VE&nx;CWpisChdG4(}QQhZaJ; z?d}B9LtTIj-*rg{@)~&v>T#g=A9>LV2wIylA^+1CstqX+q$OJj(vC3%X)qKb1wo#M zw8elRPei?xZIB3v4FqXa4RQ`roSfy6an8a+