From a2dc584ac84c036edce84dab4511e136825b2aa9 Mon Sep 17 00:00:00 2001 From: Tamim Ehsan Date: Fri, 16 Oct 2026 23:56:21 +0800 Subject: [PATCH 01/10] test: pin a pre-refactor IVF persistence fixture and generalize the historical-compat test to load it --- tests/CMakeLists.txt | 2 +- .../pre-ivf-per-cluster-storage/README.md | 17 +++++ .../ivf32_ip.expected | 17 +++++ .../ivf32_ip.index | Bin 0 -> 10750 bytes .../ivf32_l2.expected | 17 +++++ .../ivf32_l2.index | Bin 0 -> 10750 bytes .../ivf4_ip.expected | 17 +++++ .../pre-ivf-per-cluster-storage/ivf4_ip.index | Bin 0 -> 3622 bytes .../ivf4_l2.expected | 17 +++++ .../pre-ivf-per-cluster-storage/ivf4_l2.index | Bin 0 -> 3622 bytes .../pre-ivf-per-cluster-storage/manifest.json | 18 +++++ .../index/historical_persistence_test.cpp | 65 +++++++++++++----- 12 files changed, 151 insertions(+), 19 deletions(-) create mode 100644 tests/fixtures/persistence/pre-ivf-per-cluster-storage/README.md create mode 100644 tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_ip.expected create mode 100644 tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_ip.index create mode 100644 tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_l2.expected create mode 100644 tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_l2.index create mode 100644 tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf4_ip.expected create mode 100644 tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf4_ip.index create mode 100644 tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf4_l2.expected create mode 100644 tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf4_l2.index create mode 100644 tests/fixtures/persistence/pre-ivf-per-cluster-storage/manifest.json diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 84ab6e80..fe351aa6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -42,7 +42,7 @@ if(RABITQ_BUILD_INDEX) endif() set_source_files_properties(unit/rabitqlib/index/historical_persistence_test.cpp PROPERTIES COMPILE_DEFINITIONS - "RABITQ_TEST_FIXTURE_DIR=\"${CMAKE_CURRENT_SOURCE_DIR}/fixtures/persistence/v0.5.2\"") + "RABITQ_TEST_FIXTURE_ROOT=\"${CMAKE_CURRENT_SOURCE_DIR}/fixtures/persistence\"") rabitq_configure_target(rabitq_tests) # Link google test and rabitqlib headers diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/README.md b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/README.md new file mode 100644 index 00000000..4e4f36cf --- /dev/null +++ b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/README.md @@ -0,0 +1,17 @@ +# IVF snapshot immediately before the per-cluster-storage refactor + +These four files were written by `rabitqlib::ivf::IVF::save()` on `main` at commit +`64fc943e0e92fa7e18eec1477050b23342a976f8`, before `ivf.hpp`/`src/index/ivf.cpp` were changed +to give each cluster its own independently-growable storage instead of one combined buffer +across all clusters. Unlike `../v0.5.2/`, which proves format stability across a historical +release, this snapshot proves this specific refactor didn't change a single byte of the format +it reads or writes: it is pinned to the implementation immediately prior to that change, not a +distant past release. + +Generated with the same construction as `../generate.cpp`'s IVF cases (`kCount=33`, +`kDim=65`, `kQueries=3`, `kTopK=5`, the same LCG-seeded deterministic data, one cluster, point 0 +removed after construction), covering `ivf4`/`ivf32` x `l2`/`ip`. The one-off generator used to +produce them was not committed — it is a near-identical copy of `../generate.cpp` restricted to +the IVF cases and updated to this repository's current (post-0.7.0-refactor) include paths, run +once and discarded; `manifest.json` in this directory records the producer and SHA-256 hashes of +every file, the same way `../manifest.json` does for `v0.5.2`. diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_ip.expected b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_ip.expected new file mode 100644 index 00000000..a3382ec4 --- /dev/null +++ b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_ip.expected @@ -0,0 +1,17 @@ +33 65 3 5 +-1.2578125 -3.078125 -3.328125 0.67578125 3.171875 -0.2578125 -2.796875 0.91015625 -3.859375 0.328125 2.296875 -2.0859375 -0.66015625 -3.3046875 2.19140625 2.1484375 2.328125 3.7890625 3.3515625 0.08203125 1.41796875 1.43359375 -1.1484375 1.4375 2.55078125 2.0703125 -1.5859375 -2.8046875 -2.2890625 1.73828125 -2.41796875 -3.09765625 1.53515625 -3.6953125 -0.66796875 0.67578125 0.96484375 -2.9296875 2.41796875 1.25390625 -1.8984375 3.14453125 2.359375 0.65625 -2.52734375 -0.17578125 -0.80859375 -2.171875 -1.63671875 -0.421875 -3.08203125 -0.1328125 -1.94140625 2.734375 2.1875 -2.1015625 3.828125 3.82421875 -3.61328125 -0.9921875 -3.27734375 2.9765625 2.29296875 0.7421875 3.48828125 -2.58203125 1.0234375 -3.12109375 1.83203125 -2.29296875 1.05859375 2.15234375 -2.5390625 -2.3125 -2.7265625 -3.33203125 1.671875 3.6796875 3.53515625 -1.75 0.6328125 -3.1171875 -2.6875 -0.3671875 -2.640625 -2.52734375 -0.65625 1.89453125 -1.08984375 0.25390625 -3.65234375 1.08203125 -0.55078125 3.6171875 2.80078125 2.265625 2.80078125 0.98828125 -1.03515625 1.0234375 -0.04296875 2.95703125 1.8125 -1.6796875 2.75390625 -3.70703125 1.60546875 0.27734375 -3.2265625 -2.57421875 -0.875 1.40234375 -3.55078125 2.80859375 2.953125 3.37109375 -0.02734375 3.16796875 3.23828125 -0.21875 0.57421875 -2.82421875 3.80078125 1.66796875 -3.30859375 -1.21875 1.05078125 1.0546875 2.2890625 3.84765625 1.58984375 -0.7734375 -0.48046875 1.28125 -1.30859375 -1.27734375 2.67578125 1.19921875 2.3515625 -0.140625 -3.828125 -3.84375 1.0703125 -2.27734375 -2.05859375 3.59375 1.3359375 -3.44140625 -0.40234375 -0.26953125 1.671875 -3.24609375 -3.42578125 0.76953125 -0.67578125 -1.53125 -3.2578125 3.65234375 -2.9921875 3.4609375 0.25390625 1.5390625 -0.78125 2.109375 0.99609375 1.5078125 -0.33203125 -1.4296875 -0.01953125 -0.625 2.609375 0.796875 -1.1875 1.125 3.62890625 -3.23046875 -2.91015625 3.0859375 2.23828125 -3.6015625 -1.38671875 3.0703125 -2.2890625 -3.375 3.0390625 -2.02734375 -0.9140625 1 -0.0546875 0.66796875 -3.52734375 -2.8046875 0.93359375 -3.21875 -2.05859375 +17 -111.448395 +26 -98.9153748 +30 -97.6609802 +9 -70.2647552 +18 -59.205368 +10 -88.1517792 +4 -52.7075806 +24 -42.3019562 +29 -40.4504089 +23 -36.7008667 +8 -65.2822876 +31 -45.8321991 +19 -41.9718628 +26 -35.1358032 +14 -33.196228 diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_ip.index b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_ip.index new file mode 100644 index 0000000000000000000000000000000000000000..d596f2c7330d9be019e743c6f0d46c92e67b999f GIT binary patch literal 10750 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 OhytR4dO&@k0r0>0D)WE< literal 0 HcmV?d00001 diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_l2.expected b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_l2.expected new file mode 100644 index 00000000..1afdb5b4 --- /dev/null +++ b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_l2.expected @@ -0,0 +1,17 @@ +33 65 3 5 +-1.2578125 -3.078125 -3.328125 0.67578125 3.171875 -0.2578125 -2.796875 0.91015625 -3.859375 0.328125 2.296875 -2.0859375 -0.66015625 -3.3046875 2.19140625 2.1484375 2.328125 3.7890625 3.3515625 0.08203125 1.41796875 1.43359375 -1.1484375 1.4375 2.55078125 2.0703125 -1.5859375 -2.8046875 -2.2890625 1.73828125 -2.41796875 -3.09765625 1.53515625 -3.6953125 -0.66796875 0.67578125 0.96484375 -2.9296875 2.41796875 1.25390625 -1.8984375 3.14453125 2.359375 0.65625 -2.52734375 -0.17578125 -0.80859375 -2.171875 -1.63671875 -0.421875 -3.08203125 -0.1328125 -1.94140625 2.734375 2.1875 -2.1015625 3.828125 3.82421875 -3.61328125 -0.9921875 -3.27734375 2.9765625 2.29296875 0.7421875 3.48828125 -2.58203125 1.0234375 -3.12109375 1.83203125 -2.29296875 1.05859375 2.15234375 -2.5390625 -2.3125 -2.7265625 -3.33203125 1.671875 3.6796875 3.53515625 -1.75 0.6328125 -3.1171875 -2.6875 -0.3671875 -2.640625 -2.52734375 -0.65625 1.89453125 -1.08984375 0.25390625 -3.65234375 1.08203125 -0.55078125 3.6171875 2.80078125 2.265625 2.80078125 0.98828125 -1.03515625 1.0234375 -0.04296875 2.95703125 1.8125 -1.6796875 2.75390625 -3.70703125 1.60546875 0.27734375 -3.2265625 -2.57421875 -0.875 1.40234375 -3.55078125 2.80859375 2.953125 3.37109375 -0.02734375 3.16796875 3.23828125 -0.21875 0.57421875 -2.82421875 3.80078125 1.66796875 -3.30859375 -1.21875 1.05078125 1.0546875 2.2890625 3.84765625 1.58984375 -0.7734375 -0.48046875 1.28125 -1.30859375 -1.27734375 2.67578125 1.19921875 2.3515625 -0.140625 -3.828125 -3.84375 1.0703125 -2.27734375 -2.05859375 3.59375 1.3359375 -3.44140625 -0.40234375 -0.26953125 1.671875 -3.24609375 -3.42578125 0.76953125 -0.67578125 -1.53125 -3.2578125 3.65234375 -2.9921875 3.4609375 0.25390625 1.5390625 -0.78125 2.109375 0.99609375 1.5078125 -0.33203125 -1.4296875 -0.01953125 -0.625 2.609375 0.796875 -1.1875 1.125 3.62890625 -3.23046875 -2.91015625 3.0859375 2.23828125 -3.6015625 -1.38671875 3.0703125 -2.2890625 -3.375 3.0390625 -2.02734375 -0.9140625 1 -0.0546875 0.66796875 -3.52734375 -2.8046875 0.93359375 -3.21875 -2.05859375 +26 450.112122 +17 458.235657 +30 499.837524 +31 524.303589 +9 532.450073 +10 492.983521 +4 575.742737 +24 616.615479 +25 633.027588 +12 635.89978 +31 497.363464 +8 526.510376 +26 553.711975 +25 572.029541 +14 589.477295 diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_l2.index b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_l2.index new file mode 100644 index 0000000000000000000000000000000000000000..94e1ac8a09c3fe6a366b292beb0f22f3e8d3ed21 GIT binary patch literal 10750 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf4_ip.expected b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf4_ip.expected new file mode 100644 index 00000000..d3de9ffd --- /dev/null +++ b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf4_ip.expected @@ -0,0 +1,17 @@ +33 65 3 5 +-1.2578125 -3.078125 -3.328125 0.67578125 3.171875 -0.2578125 -2.796875 0.91015625 -3.859375 0.328125 2.296875 -2.0859375 -0.66015625 -3.3046875 2.19140625 2.1484375 2.328125 3.7890625 3.3515625 0.08203125 1.41796875 1.43359375 -1.1484375 1.4375 2.55078125 2.0703125 -1.5859375 -2.8046875 -2.2890625 1.73828125 -2.41796875 -3.09765625 1.53515625 -3.6953125 -0.66796875 0.67578125 0.96484375 -2.9296875 2.41796875 1.25390625 -1.8984375 3.14453125 2.359375 0.65625 -2.52734375 -0.17578125 -0.80859375 -2.171875 -1.63671875 -0.421875 -3.08203125 -0.1328125 -1.94140625 2.734375 2.1875 -2.1015625 3.828125 3.82421875 -3.61328125 -0.9921875 -3.27734375 2.9765625 2.29296875 0.7421875 3.48828125 -2.58203125 1.0234375 -3.12109375 1.83203125 -2.29296875 1.05859375 2.15234375 -2.5390625 -2.3125 -2.7265625 -3.33203125 1.671875 3.6796875 3.53515625 -1.75 0.6328125 -3.1171875 -2.6875 -0.3671875 -2.640625 -2.52734375 -0.65625 1.89453125 -1.08984375 0.25390625 -3.65234375 1.08203125 -0.55078125 3.6171875 2.80078125 2.265625 2.80078125 0.98828125 -1.03515625 1.0234375 -0.04296875 2.95703125 1.8125 -1.6796875 2.75390625 -3.70703125 1.60546875 0.27734375 -3.2265625 -2.57421875 -0.875 1.40234375 -3.55078125 2.80859375 2.953125 3.37109375 -0.02734375 3.16796875 3.23828125 -0.21875 0.57421875 -2.82421875 3.80078125 1.66796875 -3.30859375 -1.21875 1.05078125 1.0546875 2.2890625 3.84765625 1.58984375 -0.7734375 -0.48046875 1.28125 -1.30859375 -1.27734375 2.67578125 1.19921875 2.3515625 -0.140625 -3.828125 -3.84375 1.0703125 -2.27734375 -2.05859375 3.59375 1.3359375 -3.44140625 -0.40234375 -0.26953125 1.671875 -3.24609375 -3.42578125 0.76953125 -0.67578125 -1.53125 -3.2578125 3.65234375 -2.9921875 3.4609375 0.25390625 1.5390625 -0.78125 2.109375 0.99609375 1.5078125 -0.33203125 -1.4296875 -0.01953125 -0.625 2.609375 0.796875 -1.1875 1.125 3.62890625 -3.23046875 -2.91015625 3.0859375 2.23828125 -3.6015625 -1.38671875 3.0703125 -2.2890625 -3.375 3.0390625 -2.02734375 -0.9140625 1 -0.0546875 0.66796875 -3.52734375 -2.8046875 0.93359375 -3.21875 -2.05859375 +17 -105.395058 +30 -102.666962 +26 -98.7964249 +9 -71.5007706 +18 -58.2429581 +10 -81.1320343 +4 -51.4877663 +24 -39.1275826 +29 -38.2650833 +23 -33.3243561 +8 -69.4111938 +19 -45.8613091 +31 -44.8685837 +26 -35.4361267 +14 -34.0174561 diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf4_ip.index b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf4_ip.index new file mode 100644 index 0000000000000000000000000000000000000000..2ec697e0867b10624e99a0b6bf7910f7323906c1 GIT binary patch literal 3622 zcmchZ_fr#H7snGodJUZ*y&I|&6|%eZAQmhX1*It>3W`*bs;CG=dJ#c7hz}q|1Sz{4 zk)}dIm7;>8f{027Mf45x%FHwQ1D-qcnRCDQoSn0~Kiuy*Y;1DK%FIEV3G@Hu@;74X zQTFfN_+Mw6Tjni|-Kt=2j!k%7R(#1RMTRgLrRiLTA-jy~25s1gn`c+LJa)GoGEiH| z(aMSOmBT%hFZ6$+@f~Pj;el}vLSeO~DY)$m2)xeI1k`Q?6B`MRfZ~)BT#$?dd-hns zIfZL2kWx7?;aJrY>XQMBwd*3I$pru=NwTHdM;&~?$A$nd2ysY23kVCzhMN;ch`K}# z;JD{W`1j{Hm^2h^#G8JNIBLy<>=9CiX*@ngBHeYwdYUzCq}ork&?6wNe&g_TtRLXi zqmLZGm?3=|qHv9?5!}=~3o`|a0m)|{03*|xu!@cw9Bp|RmiCwibbs%MNtP@`C$BpA zs!|;>QGRKpHuXfXek%4XnaCNMLb z4`VbVnrOPB#Q0~?K>xWX@ZBLYu=lVuP#epM>{5|wiPT~6*!kFMfK`nXVXQ2B&2dfA_$n94d zaZj?+*y5y+vq|jEsC#4g)-ATz6#qHX)H?Sx##gVBv*PM2LS%lH-n(yEQnGjM1)f%H zZBThDw~;#PK5^0i#pu@?+S|R~wpx|symMAlor_O|YQKkG5>k2Y`B2B6(92^@jaYhA z`X=^i#zV5{BlAcJ0K0^H*)^&jrajrUoUyt{i7uAlB8paAk1$zY8oKez&>!z$0IEJ> zE}otJt=23lK#dVMcajUx;W>5zdotyGxl#YhJ)18aN_#)J4^T(0tz1LqQt`%e)G~BeeFCbXaUIH}wICAFP0+rCa!B|& zCj`A<1pUU{5` zrUa4E(ac12%sLsx374YbkqKxvTMD|3H3JRgOhaGIrlW7Ha?sU0DGDK-3K+>stkb@loTA!7IHgC6oudUl}>x_GA^EWc; zts8Kwj4{uGy~@IoQaw@H{(*Qm@e;oV z=j~d3XRiOLJ2O1F;7m6c za@=o?c~E#6NcsX>lqbqiObbc<-K7;)R$|1Iu;7^3+Xv6PymuW0NlHA(5t?UUTlk+4 zja>@H2Sogl@Ez@h(=u&NH%#~KK9nYY(n5$f@LVDqB>rYYTI0!K@sm7WjmahLai|RKh0<8A9dSA?wpfd$9Oefly8LA znr>V!;y=s6n$rilu>y+N*a|oa{8qrp2k;2(P*IwK*{#^i|_SmL4q+erW1G$0A{_ z?DfrERPpDT3Hd`9&0c$N%TF_;*J;U1KW$<=fNV7pWY3zxfD6r>x3ziaS97VZgSPc@ zpYeQMgi|I7eJAQ(X|4gy)|dtd(6tnY4D(VQQo2UiyKNeX&^bs@zci^v<5JRwOl__1dcmYv%cg1zLrD6g^R zq=Y?S{y#XRZHj07g}KR?rRFb1kCS@ZPs2#+%U-4yv+IH{9+JChS|94!60wb-{M{MY z=;2X6iG&rAp{fST+Cn)#kg(g25(dQScZUfvS%gN4ME1#$gcQM!DkX%KW!I3^$ma=G zLmQu@+j)0}uGp#BebjT$vNRmD8}sq6`Gb3lIiAz(coc`&lq6J++ssJcZnc3tCn>5= zn4$ti6(1O9kLSa*6PAm-vo=gBXJ3NzjX0zJ8NH8z2ENc$W8TJ7Z8JQ3!=V2cRW;N9ESS_R zh*+4#&Jzn1deY}l9MG! z8sR(>$GTo@Wlwb5jPa4z*qAYcAN+Ti!nszX0PmWQaBlw(7jK6^FP!9Dv3YBzO*S*BSBgApU>Q|kf2{pcR%fT{&bTI|PBi}z*-~GV+JJ?Ax_U`eL1BX|OQ^~g*>QBlJo;3f()uHvN2{h!xA^xR7 zj{JJW*a26zj;Ji0`BI@t<0V}$G*V#_8V%d!RPD=w@V*z=&#sYtdRxl$ zQ7uy3QpJ(3IKuOr)R6gm+7wC0+*WJlR7`bX^JS(D_(%R!$2#9=3Aw0B*>S`bm+dgs zzWA%pk1L2tADhc1_5R)aYnU(B!9o@>ge;LY(anDM3IQtNjPV`youVFgX1=xRJUys= zKPY_tzz5Oz`wE=-;90C7vS|#P^U9{@ZuF=AK%|&N(me`yZ uHa(Y*4nG|MIygFlbcE;#(-EN~N=J;2I2{Q(csi1Fw$qWKBTYw!&i?>4ofb0y literal 0 HcmV?d00001 diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf4_l2.expected b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf4_l2.expected new file mode 100644 index 00000000..b8cb8f01 --- /dev/null +++ b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf4_l2.expected @@ -0,0 +1,17 @@ +33 65 3 5 +-1.2578125 -3.078125 -3.328125 0.67578125 3.171875 -0.2578125 -2.796875 0.91015625 -3.859375 0.328125 2.296875 -2.0859375 -0.66015625 -3.3046875 2.19140625 2.1484375 2.328125 3.7890625 3.3515625 0.08203125 1.41796875 1.43359375 -1.1484375 1.4375 2.55078125 2.0703125 -1.5859375 -2.8046875 -2.2890625 1.73828125 -2.41796875 -3.09765625 1.53515625 -3.6953125 -0.66796875 0.67578125 0.96484375 -2.9296875 2.41796875 1.25390625 -1.8984375 3.14453125 2.359375 0.65625 -2.52734375 -0.17578125 -0.80859375 -2.171875 -1.63671875 -0.421875 -3.08203125 -0.1328125 -1.94140625 2.734375 2.1875 -2.1015625 3.828125 3.82421875 -3.61328125 -0.9921875 -3.27734375 2.9765625 2.29296875 0.7421875 3.48828125 -2.58203125 1.0234375 -3.12109375 1.83203125 -2.29296875 1.05859375 2.15234375 -2.5390625 -2.3125 -2.7265625 -3.33203125 1.671875 3.6796875 3.53515625 -1.75 0.6328125 -3.1171875 -2.6875 -0.3671875 -2.640625 -2.52734375 -0.65625 1.89453125 -1.08984375 0.25390625 -3.65234375 1.08203125 -0.55078125 3.6171875 2.80078125 2.265625 2.80078125 0.98828125 -1.03515625 1.0234375 -0.04296875 2.95703125 1.8125 -1.6796875 2.75390625 -3.70703125 1.60546875 0.27734375 -3.2265625 -2.57421875 -0.875 1.40234375 -3.55078125 2.80859375 2.953125 3.37109375 -0.02734375 3.16796875 3.23828125 -0.21875 0.57421875 -2.82421875 3.80078125 1.66796875 -3.30859375 -1.21875 1.05078125 1.0546875 2.2890625 3.84765625 1.58984375 -0.7734375 -0.48046875 1.28125 -1.30859375 -1.27734375 2.67578125 1.19921875 2.3515625 -0.140625 -3.828125 -3.84375 1.0703125 -2.27734375 -2.05859375 3.59375 1.3359375 -3.44140625 -0.40234375 -0.26953125 1.671875 -3.24609375 -3.42578125 0.76953125 -0.67578125 -1.53125 -3.2578125 3.65234375 -2.9921875 3.4609375 0.25390625 1.5390625 -0.78125 2.109375 0.99609375 1.5078125 -0.33203125 -1.4296875 -0.01953125 -0.625 2.609375 0.796875 -1.1875 1.125 3.62890625 -3.23046875 -2.91015625 3.0859375 2.23828125 -3.6015625 -1.38671875 3.0703125 -2.2890625 -3.375 3.0390625 -2.02734375 -0.9140625 1 -0.0546875 0.66796875 -3.52734375 -2.8046875 0.93359375 -3.21875 -2.05859375 +26 445.232147 +17 452.974304 +30 494.317627 +31 528.077759 +9 530.807129 +10 499.175171 +4 572.70874 +25 623.365967 +24 624.918762 +29 643.0896 +31 498.531799 +8 520.854736 +26 543.660034 +25 566.748413 +14 580.274048 diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf4_l2.index b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf4_l2.index new file mode 100644 index 0000000000000000000000000000000000000000..a2e2dfe1b69f3282f61fcfea5b55176a56b0cb41 GIT binary patch literal 3622 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/manifest.json b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/manifest.json new file mode 100644 index 00000000..946e9655 --- /dev/null +++ b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/manifest.json @@ -0,0 +1,18 @@ +{ + "source_tag": "main", + "source_commit": "64fc943e0e92fa7e18eec1477050b23342a976f8", + "writer": "one-off generator matching tests/fixtures/persistence/generate.cpp's IVF cases, run against the unmodified IVF class immediately before the per-cluster-storage refactor (not committed to the repo; see README.md in this directory)", + "producer": "Linux x86-64, g++, one thread, native optimization OFF", + "byte_order": "little", + "size_t_bytes": 8, + "sha256": { + "pre-ivf-per-cluster-storage/ivf32_ip.expected": "e0d953927d08b619c75b9424a032f136d32d3756d3fde4bf8c6abe0b7b743a28", + "pre-ivf-per-cluster-storage/ivf32_ip.index": "db6b66acf40caa4063c2d788bd37e4ae129865cfa6e6d32fd19afc846541ea2d", + "pre-ivf-per-cluster-storage/ivf32_l2.expected": "a9ba0430f4c8a517168aa1ff75bad72d7f6089161561412a75976fc970400f9c", + "pre-ivf-per-cluster-storage/ivf32_l2.index": "3bdfadfe289d6c0b474096fff990157ca76df0c8b27478ff6c32fcb1fc81a940", + "pre-ivf-per-cluster-storage/ivf4_ip.expected": "81a81577f76e75bb4faefbfe559176e5b0846dadd04332d6a9913eb13dfaaf40", + "pre-ivf-per-cluster-storage/ivf4_ip.index": "7fc3ab1b52848e5c9c014d62bf7fdedc731c2beed6e491edf58dd63efb462a3b", + "pre-ivf-per-cluster-storage/ivf4_l2.expected": "7bbd23134af0cdd4adc794df103bd1eb2aba0d362be3f12d3019e02a3d8d3165", + "pre-ivf-per-cluster-storage/ivf4_l2.index": "9966c3bddc306d2d2d062d4a696750adde3daa8e7c086add0fa2411669c488cf" + } +} diff --git a/tests/unit/rabitqlib/index/historical_persistence_test.cpp b/tests/unit/rabitqlib/index/historical_persistence_test.cpp index f9b27684..d8240a96 100644 --- a/tests/unit/rabitqlib/index/historical_persistence_test.cpp +++ b/tests/unit/rabitqlib/index/historical_persistence_test.cpp @@ -18,11 +18,23 @@ namespace rabitqlib { namespace { -class HistoricalPersistenceTest : public ::testing::TestWithParam {}; +// padded_dim is the historical/fixed padding rule the fixture set was written +// under (v0.5.2 always padded to a 64-coordinate multiple; sets generated under +// the current writer store and report their own actual padded_dim instead). +struct HistoricalFixtureCase { + std::string directory; // subdirectory under RABITQ_TEST_FIXTURE_ROOT + std::string name; + size_t padded_dim; +}; + +class HistoricalPersistenceTest + : public ::testing::TestWithParam {}; TEST_P(HistoricalPersistenceTest, LoadsSearchesResavesAndPreservesStateAfterFailedLoad) { - const std::filesystem::path directory(RABITQ_TEST_FIXTURE_DIR); - const auto name = GetParam(); + const std::filesystem::path directory = + std::filesystem::path(RABITQ_TEST_FIXTURE_ROOT) / GetParam().directory; + const auto name = GetParam().name; + const auto expected_padded_dim = GetParam().padded_dim; const auto path = (directory / (name + ".index")).string(); std::ifstream expected(directory / (name + ".expected")); ASSERT_TRUE(expected.is_open()); @@ -80,7 +92,7 @@ TEST_P(HistoricalPersistenceTest, LoadsSearchesResavesAndPreservesStateAfterFail if (name.find("ivf") == 0) { ivf::IVF index; exercise(index, [&] { - EXPECT_EQ(index.padded_dim(), 128U); + EXPECT_EQ(index.padded_dim(), expected_padded_dim); EXPECT_EQ(index.initializer_type(), ivf::InitializerType::Flat); index.search_batch(queries.data(), nq, k, 1, ids.data(), distances.data()); }); @@ -99,7 +111,7 @@ TEST_P(HistoricalPersistenceTest, LoadsSearchesResavesAndPreservesStateAfterFail } else { symqg::QuantizedGraph index; exercise(index, [&] { - EXPECT_EQ(index.padded_dim(), 128U); + EXPECT_EQ(index.padded_dim(), expected_padded_dim); index.set_ef(64); index.search_batch(queries.data(), nq, k, ids.data(), distances.data(), 1); }); @@ -110,20 +122,37 @@ INSTANTIATE_TEST_SUITE_P( V052, HistoricalPersistenceTest, ::testing::Values( - "ivf4_l2", - "ivf4_ip", - "ivf32_l2", - "ivf32_ip", - "hnsw4_l2", - "hnsw4_ip", - "qg0_l2", - "qg0_ip", - "qg4_l2", - "qg4_ip", - "qg8_l2", - "qg8_ip" + HistoricalFixtureCase{"v0.5.2", "ivf4_l2", 128}, + HistoricalFixtureCase{"v0.5.2", "ivf4_ip", 128}, + HistoricalFixtureCase{"v0.5.2", "ivf32_l2", 128}, + HistoricalFixtureCase{"v0.5.2", "ivf32_ip", 128}, + HistoricalFixtureCase{"v0.5.2", "hnsw4_l2", 128}, + HistoricalFixtureCase{"v0.5.2", "hnsw4_ip", 128}, + HistoricalFixtureCase{"v0.5.2", "qg0_l2", 128}, + HistoricalFixtureCase{"v0.5.2", "qg0_ip", 128}, + HistoricalFixtureCase{"v0.5.2", "qg4_l2", 128}, + HistoricalFixtureCase{"v0.5.2", "qg4_ip", 128}, + HistoricalFixtureCase{"v0.5.2", "qg8_l2", 128}, + HistoricalFixtureCase{"v0.5.2", "qg8_ip", 128} + ), + [](const auto& info) { return info.param.name; } +); + +// Pinned to the IVF implementation immediately before the per-cluster-storage +// refactor (see tests/fixtures/persistence/pre-ivf-per-cluster-storage/README.md), +// not a distant historical release: proves this specific change didn't move the +// on-disk format it reads or writes. kDim=65 rounds up to a 96 padded_dim under +// the current (32-multiple) padding rule, unlike v0.5.2's fixed 128. +INSTANTIATE_TEST_SUITE_P( + PreIvfPerClusterStorage, + HistoricalPersistenceTest, + ::testing::Values( + HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf4_l2", 96}, + HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf4_ip", 96}, + HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf32_l2", 96}, + HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf32_ip", 96} ), - [](const auto& info) { return info.param; } + [](const auto& info) { return info.param.name; } ); } // namespace } // namespace rabitqlib From f8187e2b3dec4bd0a9d90d6a75fd15c935211357 Mon Sep 17 00:00:00 2001 From: Tamim Ehsan Date: Sat, 17 Oct 2026 00:07:05 +0800 Subject: [PATCH 02/10] refactor(ivf): give each cluster its own independently-growable storage so add() only touches clusters that actually changed --- include/rabitqlib/index/ivf/ivf.hpp | 48 ++---- src/index/ivf.cpp | 245 ++++++++++++++-------------- 2 files changed, 142 insertions(+), 151 deletions(-) diff --git a/include/rabitqlib/index/ivf/ivf.hpp b/include/rabitqlib/index/ivf/ivf.hpp index b3362066..bf85e324 100644 --- a/include/rabitqlib/index/ivf/ivf.hpp +++ b/include/rabitqlib/index/ivf/ivf.hpp @@ -30,17 +30,25 @@ class Initializer; class IVF { private: + friend struct IvfTestAccess; + using ByteStorage = std::vector>; using FloatStorage = std::vector>; using IdStorage = std::vector>; + // One cluster's owned storage. Independent of every other cluster's, so growing + // one cluster never reallocates or copies another's. + struct ClusterStorage { + ByteStorage batch; // 1-bit code and factors + ByteStorage ex; // extra-bit codes and packed factors + FloatStorage raw; // original vectors for raw reranking + IdStorage ids; + }; + InitializerType initializer_type_ = InitializerType::Auto; - std::unique_ptr initializer_; // initializer for candidate clusters - ByteStorage batch_storage_; // 1-bit code and factors - ByteStorage ex_storage_; // extra-bit codes and packed factors - FloatStorage raw_storage_; // original vectors for raw reranking - IdStorage id_storage_; // point IDs organized by cluster + std::unique_ptr initializer_; // initializer for candidate clusters + std::vector cluster_storage_; // one entry per cluster size_t max_elements_ = 0; // Logical capacity; construct fills it, add grows it. size_t num_removed_ = 0; // Recovered from removal markers when loading. size_t dim_ = 0; @@ -70,37 +78,11 @@ class IVF { // True if `n` points cannot be addressed by the storage size computations [[nodiscard]] bool exceeds_storage(size_t n) const; - [[nodiscard]] size_t ids_bytes() const { return sizeof(PID) * max_elements_; } - - // get num of bytes used for 1-bit code and corresponding factors - [[nodiscard]] size_t batch_data_bytes(const std::vector& cluster_sizes) const; + // get num of bytes used for one cluster's 1-bit code and corresponding factors + [[nodiscard]] size_t cluster_batch_bytes(size_t cluster_size) const; [[nodiscard]] inline size_t rerank_vector_bytes() const; - [[nodiscard]] inline size_t ex_data_bytes() const; - - [[nodiscard]] char* batch_data() { - return reinterpret_cast(batch_storage_.data()); - } - - [[nodiscard]] const char* batch_data() const { - return reinterpret_cast(batch_storage_.data()); - } - - [[nodiscard]] char* ex_data() { - return raw_reranking_ ? reinterpret_cast(raw_storage_.data()) - : reinterpret_cast(ex_storage_.data()); - } - - [[nodiscard]] const char* ex_data() const { - return raw_reranking_ ? reinterpret_cast(raw_storage_.data()) - : reinterpret_cast(ex_storage_.data()); - } - - [[nodiscard]] PID* ids() { return id_storage_.data(); } - - [[nodiscard]] const PID* ids() const { return id_storage_.data(); } - void allocate_memory(const std::vector&); void init_clusters(const std::vector&); diff --git a/src/index/ivf.cpp b/src/index/ivf.cpp index 68267a47..c1f0f1a8 100644 --- a/src/index/ivf.cpp +++ b/src/index/ivf.cpp @@ -52,13 +52,9 @@ bool IVF::exceeds_storage(size_t n) const { n > max_size / batch_bytes || (rerank_bytes != 0 && n > max_size / rerank_bytes); } -size_t IVF::batch_data_bytes(const std::vector& cluster_sizes) const { - assert(cluster_sizes.size() == num_clusters_); - size_t total_blocks = 0; - for (auto size : cluster_sizes) { - total_blocks += div_round_up(size, fastscan::kBatchSize); - } - return total_blocks * BatchDataMap::data_bytes(padded_dim_); +size_t IVF::cluster_batch_bytes(size_t cluster_size) const { + return div_round_up(cluster_size, fastscan::kBatchSize) * + BatchDataMap::data_bytes(padded_dim_); } // Keep the original inlining intent inside this translation unit, especially the @@ -68,24 +64,16 @@ inline size_t IVF::rerank_vector_bytes() const { : ExDataMap::data_bytes(padded_dim_, ex_bits_); } -inline size_t IVF::ex_data_bytes() const { return rerank_vector_bytes() * max_elements_; } - void IVF::free_memory() { initializer_.reset(); - ByteStorage().swap(batch_storage_); - ByteStorage().swap(ex_storage_); - FloatStorage().swap(raw_storage_); - IdStorage().swap(id_storage_); + std::vector().swap(cluster_storage_); } void IVF::swap(IVF& other) noexcept { using std::swap; swap(initializer_type_, other.initializer_type_); swap(initializer_, other.initializer_); - swap(batch_storage_, other.batch_storage_); - swap(ex_storage_, other.ex_storage_); - swap(raw_storage_, other.raw_storage_); - swap(id_storage_, other.id_storage_); + swap(cluster_storage_, other.cluster_storage_); swap(max_elements_, other.max_elements_); swap(num_removed_, other.num_removed_); swap(dim_, other.dim_); @@ -250,42 +238,37 @@ void IVF::allocate_memory(const std::vector& cluster_sizes) { this->initializer_ = std::make_unique(padded_dim_, num_clusters_, metric_type_); } - batch_storage_ = ByteStorage(batch_data_bytes(cluster_sizes)); - if (rerank_vector_bytes() > 0) { - if (raw_reranking_) { - raw_storage_ = FloatStorage(max_elements_ * dim_); - } else { - ex_storage_ = ByteStorage(ex_data_bytes()); + const size_t rerank_bytes = rerank_vector_bytes(); + cluster_storage_.assign(num_clusters_, ClusterStorage{}); + for (size_t i = 0; i < num_clusters_; ++i) { + ClusterStorage& cs = cluster_storage_[i]; + cs.batch = ByteStorage(cluster_batch_bytes(cluster_sizes[i])); + if (rerank_bytes > 0) { + if (raw_reranking_) { + cs.raw = FloatStorage(cluster_sizes[i] * dim_); + } else { + cs.ex = ByteStorage(cluster_sizes[i] * rerank_bytes); + } } + cs.ids = IdStorage(cluster_sizes[i]); } - id_storage_ = IdStorage(max_elements_); this->ip_func_ = select_excode_ipfunc(ex_bits_); } /** - * @brief Initialize the cluster list by finding each cluster's storage offsets. + * @brief Initialize the cluster list from each cluster's own independent storage. */ void IVF::init_clusters(const std::vector& cluster_sizes) { this->clusters_.reserve(num_clusters_); - size_t added_vectors = 0; - size_t added_batches = 0; for (size_t i = 0; i < num_clusters_; ++i) { - // find data location for current cluster - size_t num = cluster_sizes[i]; - size_t num_batches = div_round_up(num, fastscan::kBatchSize); - - char* current_batch_data = - batch_data() + (BatchDataMap::data_bytes(padded_dim_) * added_batches); - char* current_ex_data = rerank_vector_bytes() > 0 - ? ex_data() + (added_vectors * rerank_vector_bytes()) - : nullptr; - PID* cluster_ids = ids() + added_vectors; - - this->clusters_.emplace_back(num, current_batch_data, current_ex_data, cluster_ids); - - added_vectors += num; - added_batches += num_batches; + ClusterStorage& cs = cluster_storage_[i]; + char* current_batch_data = reinterpret_cast(cs.batch.data()); + char* current_ex_data = raw_reranking_ ? reinterpret_cast(cs.raw.data()) + : reinterpret_cast(cs.ex.data()); + this->clusters_.emplace_back( + cluster_sizes[i], current_batch_data, current_ex_data, cs.ids.data() + ); } } @@ -344,8 +327,7 @@ void IVF::add( const float* data, size_t n, const PID* cluster_ids, bool faster, size_t num_threads ) { if (!ready_ || initializer_ == nullptr || rotator_ == nullptr || - clusters_.size() != num_clusters_ || batch_storage_.empty() || - id_storage_.size() != max_elements_) { + clusters_.size() != num_clusters_ || cluster_storage_.size() != num_clusters_) { throw std::logic_error("IVF index must be constructed or loaded before add"); } if (n == 0) { @@ -391,33 +373,12 @@ void IVF::add( rows_of[assigned[i]].push_back(static_cast(i)); } std::vector new_sizes(num_clusters_); + std::vector touched; for (size_t i = 0; i < num_clusters_; ++i) { new_sizes[i] = clusters_[i].num() + rows_of[i].size(); - } - - /* Build the grown storage on the side so the index is unchanged on failure */ - ByteStorage new_batch(batch_data_bytes(new_sizes)); - ByteStorage new_ex; - FloatStorage new_raw; - const size_t rerank_bytes = rerank_vector_bytes(); - if (raw_reranking_) { - new_raw = FloatStorage(new_num_points * dim_); - } else if (rerank_bytes > 0) { - new_ex = ByteStorage(new_num_points * rerank_bytes); - } - IdStorage new_ids(new_num_points); - char* new_rerank = raw_reranking_ ? reinterpret_cast(new_raw.data()) - : reinterpret_cast(new_ex.data()); - - std::vector batch_offset(num_clusters_); - std::vector vector_offset(num_clusters_); - size_t added_batches = 0; - size_t added_vectors = 0; - for (size_t i = 0; i < num_clusters_; ++i) { - batch_offset[i] = added_batches; - vector_offset[i] = added_vectors; - added_batches += div_round_up(new_sizes[i], fastscan::kBatchSize); - added_vectors += new_sizes[i]; + if (!rows_of[i].empty()) { + touched.push_back(i); + } } quant::RabitqConfig config; @@ -425,35 +386,53 @@ void IVF::add( config = quant::faster_config(padded_dim_, ex_bits_ + 1); } - const size_t batch_bytes = BatchDataMap::data_bytes(padded_dim_); + const size_t rerank_bytes = rerank_vector_bytes(); const PID first_pid = static_cast(max_elements_); + + /* Stage every touched cluster's grown storage on the side, so the index is + unchanged on failure and no untouched cluster is ever copied. */ + std::vector> staged(num_clusters_); + for (size_t i : touched) { + ClusterStorage cs; + cs.batch = ByteStorage(cluster_batch_bytes(new_sizes[i])); + if (raw_reranking_) { + cs.raw = FloatStorage(new_sizes[i] * dim_); + } else if (rerank_bytes > 0) { + cs.ex = ByteStorage(new_sizes[i] * rerank_bytes); + } + cs.ids = IdStorage(new_sizes[i]); + staged[i] = std::move(cs); + } + // An exception must not leave an OpenMP region, so keep the first one and rethrow it. index_detail::ParallelException failure; #pragma omp parallel for schedule(dynamic) num_threads(resolve_num_threads(num_threads)) - for (std::ptrdiff_t index = 0; index < static_cast(num_clusters_); + for (std::ptrdiff_t index = 0; index < static_cast(touched.size()); ++index) { - const size_t i = static_cast(index); + const size_t i = touched[static_cast(index)]; try { + ClusterStorage& cs = *staged[i]; extend_cluster( clusters_[i], rows_of[i], data, initializer_->centroid(static_cast(i)), first_pid, - reinterpret_cast(new_batch.data()) + (batch_offset[i] * batch_bytes), - rerank_bytes > 0 ? new_rerank + (vector_offset[i] * rerank_bytes) : nullptr, - new_ids.data() + vector_offset[i], + reinterpret_cast(cs.batch.data()), + rerank_bytes > 0 ? (raw_reranking_ ? reinterpret_cast(cs.raw.data()) + : reinterpret_cast(cs.ex.data())) + : nullptr, + cs.ids.data(), config ); } catch (...) { failure.capture(); } } failure.rethrow(); - /* Commit. Rebuilding the cluster list reuses its capacity, so it cannot throw. */ - batch_storage_.swap(new_batch); - ex_storage_.swap(new_ex); - raw_storage_.swap(new_raw); - id_storage_.swap(new_ids); + /* Commit. Untouched clusters are never staged, so they are unaffected by this. */ + for (size_t i : touched) { + cluster_storage_[i] = std::move(*staged[i]); + } max_elements_ = new_num_points; clusters_.clear(); init_clusters(new_sizes); @@ -477,19 +456,14 @@ void IVF::extend_cluster( PID* ids_dst, const quant::RabitqConfig& config ) const { + // Callers only extend touched clusters now; an untouched cluster's storage is + // never reallocated or copied at all, not even through this function. + assert(!rows.empty()); const size_t old_num = old_cluster.num(); const size_t batch_bytes = BatchDataMap::data_bytes(padded_dim_); const size_t rerank_bytes = rerank_vector_bytes(); const size_t cols = padded_dim_ / 8; - if (rows.empty()) { - const size_t old_batches = div_round_up(old_num, fastscan::kBatchSize); - std::copy_n(old_cluster.batch_data(), old_batches * batch_bytes, batch_dst); - std::copy_n(old_cluster.ex_data(), old_num * rerank_bytes, rerank_dst); - std::copy_n(old_cluster.ids(), old_num, ids_dst); - return; - } - const size_t full_batches = old_num / fastscan::kBatchSize; const size_t kept = old_num - (full_batches * fastscan::kBatchSize); const size_t tail_num = kept + rows.size(); @@ -564,8 +538,7 @@ void IVF::extend_cluster( size_t IVF::remove(const PID* ids_to_remove, size_t n) { if (!ready_ || initializer_ == nullptr || rotator_ == nullptr || - clusters_.size() != num_clusters_ || batch_storage_.empty() || - id_storage_.size() != max_elements_) { + clusters_.size() != num_clusters_ || cluster_storage_.size() != num_clusters_) { throw std::logic_error("IVF index must be constructed or loaded before remove"); } if (n == 0) { @@ -612,8 +585,7 @@ size_t IVF::remove(const PID* ids_to_remove, size_t n) { void IVF::save(const char* filename) const { if (!ready_ || initializer_ == nullptr || rotator_ == nullptr || - clusters_.size() != num_clusters_ || batch_storage_.empty() || - id_storage_.size() != max_elements_) { + clusters_.size() != num_clusters_ || cluster_storage_.size() != num_clusters_) { throw std::logic_error("Cannot save an unconstructed IVF index"); } if (filename == nullptr || filename[0] == '\0') { @@ -658,17 +630,38 @@ void IVF::save(const char* filename) const { /* Save rotator */ this->rotator_->save(output); - /* Save data */ + /* Save data. The reader expects three contiguous regions -- every cluster's batch + data, then every cluster's rerank data, then every cluster's IDs -- in cluster + order, exactly as one combined buffer laid out by cluster used to produce; each + region is now written from that cluster's own storage instead of a shared one, + but the bytes landing on disk are unchanged. */ this->initializer_->save(output, filename); - output.write( - batch_data(), static_cast(batch_data_bytes(cluster_sizes)) - ); - if (ex_data_bytes() != 0) { - output.write(ex_data(), static_cast(ex_data_bytes())); + for (size_t i = 0; i < num_clusters_; ++i) { + const ClusterStorage& cs = cluster_storage_[i]; + output.write( + reinterpret_cast(cs.batch.data()), + static_cast(cluster_batch_bytes(cluster_sizes[i])) + ); + } + const size_t rerank_bytes = rerank_vector_bytes(); + if (rerank_bytes != 0) { + for (size_t i = 0; i < num_clusters_; ++i) { + const ClusterStorage& cs = cluster_storage_[i]; + const char* rerank_ptr = raw_reranking_ + ? reinterpret_cast(cs.raw.data()) + : reinterpret_cast(cs.ex.data()); + output.write( + rerank_ptr, static_cast(cluster_sizes[i] * rerank_bytes) + ); + } + } + for (size_t i = 0; i < num_clusters_; ++i) { + const ClusterStorage& cs = cluster_storage_[i]; + output.write( + reinterpret_cast(cs.ids.data()), + static_cast(cluster_sizes[i] * sizeof(PID)) + ); } - output.write( - reinterpret_cast(ids()), static_cast(ids_bytes()) - ); output.close(); } @@ -813,24 +806,41 @@ void IVF::load(const char* filename) { /* Load rotator */ loaded.rotator_->load(input); - /* Load data */ + /* Load data. Three contiguous regions on disk -- every cluster's batch data, then + every cluster's rerank data, then every cluster's IDs -- read into each + cluster's own storage instead of one shared buffer; see save()'s matching + comment. */ loaded.allocate_memory(cluster_sizes); loaded.initializer_->load(input, filename); - input.read( - loaded.batch_data(), - static_cast(loaded.batch_data_bytes(cluster_sizes)) - ); - if (loaded.ex_data_bytes() != 0) { - input.read(loaded.ex_data(), static_cast(loaded.ex_data_bytes())); + for (size_t i = 0; i < loaded.num_clusters_; ++i) { + ClusterStorage& cs = loaded.cluster_storage_[i]; + input.read( + reinterpret_cast(cs.batch.data()), + static_cast(loaded.cluster_batch_bytes(cluster_sizes[i])) + ); } - input.read( - reinterpret_cast(loaded.ids()), - static_cast(loaded.ids_bytes()) - ); - for (size_t i = 0; i < loaded.max_elements_; ++i) { - if (loaded.ids()[i] >= loaded.max_elements_ || - loaded.ids()[i] >= buffer::kSearchBufferCheckedMask) { - throw std::runtime_error("Invalid point ID in IVF index file"); + const size_t rerank_bytes = loaded.rerank_vector_bytes(); + if (rerank_bytes != 0) { + for (size_t i = 0; i < loaded.num_clusters_; ++i) { + ClusterStorage& cs = loaded.cluster_storage_[i]; + char* rerank_ptr = loaded.raw_reranking_ + ? reinterpret_cast(cs.raw.data()) + : reinterpret_cast(cs.ex.data()); + input.read( + rerank_ptr, static_cast(cluster_sizes[i] * rerank_bytes) + ); + } + } + for (size_t i = 0; i < loaded.num_clusters_; ++i) { + ClusterStorage& cs = loaded.cluster_storage_[i]; + input.read( + reinterpret_cast(cs.ids.data()), + static_cast(cluster_sizes[i] * sizeof(PID)) + ); + for (PID id : cs.ids) { + if (id >= loaded.max_elements_ || id >= buffer::kSearchBufferCheckedMask) { + throw std::runtime_error("Invalid point ID in IVF index file"); + } } } @@ -993,8 +1003,7 @@ inline void IVF::validate_search( const float* query, size_t k, size_t nprobe, const PID* results ) const { if (!ready_ || initializer_ == nullptr || rotator_ == nullptr || - clusters_.size() != num_clusters_ || batch_storage_.empty() || - id_storage_.size() != max_elements_) { + clusters_.size() != num_clusters_ || cluster_storage_.size() != num_clusters_) { throw std::logic_error("IVF index must be constructed or loaded before search"); } if (query == nullptr) { From 3f7093ffd0f39ce8691c2ccc2ece2d67d2426e59 Mon Sep 17 00:00:00 2001 From: Tamim Ehsan Date: Sat, 17 Oct 2026 00:07:15 +0800 Subject: [PATCH 03/10] test(ivf): assert add() leaves untouched clusters' storage pointers unchanged --- tests/unit/rabitqlib/index/ivf_test.cpp | 51 +++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/unit/rabitqlib/index/ivf_test.cpp b/tests/unit/rabitqlib/index/ivf_test.cpp index 715660a0..27abb227 100644 --- a/tests/unit/rabitqlib/index/ivf_test.cpp +++ b/tests/unit/rabitqlib/index/ivf_test.cpp @@ -28,6 +28,13 @@ #include "rabitqlib/index/types.hpp" namespace rabitqlib::ivf { + +// Must live directly in rabitqlib::ivf, not in the anonymous namespace below: +// the `friend struct IvfTestAccess;` in ivf.hpp looks it up from there. +struct IvfTestAccess { + static Cluster cluster_view(const IVF& index, size_t i) { return index.clusters_[i]; } +}; + namespace { TEST(IvfSearchTest, BatchSearchMatchesIndividualQueries) { @@ -645,6 +652,50 @@ TEST(IvfAddTest, EveryWayOfAddingTheSamePointsGivesByteIdenticalIndexes) { std::remove(path.c_str()); } +TEST(IvfAddTest, AddingToOneClusterLeavesOtherClustersUntouched) { + // Cluster sizes 0, 1, 32, 97: an initially-empty cluster, one with a single + // point, one that exactly fills a FastScan batch, and one with a partial tail. + constexpr size_t kOld = 130; + const auto points = random_rows(kOld, 741); + const auto centroids = random_rows(kDynClusters, 742); + std::vector assigned(kOld, 3); + assigned[0] = 1; + std::fill(assigned.begin() + 1, assigned.begin() + 33, PID{2}); + + IVF index(kOld, kDynDim, kDynClusters, 4, METRIC_L2); + index.construct(points.data(), centroids.data(), assigned.data(), false, 1); + + std::vector before; + before.reserve(kDynClusters); + for (size_t i = 0; i < kDynClusters; ++i) { + before.push_back(IvfTestAccess::cluster_view(index, i)); + } + + // Route every new point to cluster 0 (the initially-empty one) explicitly. + constexpr size_t kNew = 5; + const auto extra = random_rows(kNew, 743); + const std::vector new_cluster_ids(kNew, 0); + index.add(extra.data(), kNew, new_cluster_ids.data(), false, 1); + + for (size_t i = 0; i < kDynClusters; ++i) { + SCOPED_TRACE(::testing::Message() << "cluster=" << i); + const Cluster after = IvfTestAccess::cluster_view(index, i); + if (i == 0) { + EXPECT_EQ(after.num(), before[i].num() + kNew); + EXPECT_NE(after.batch_data(), before[i].batch_data()); + EXPECT_NE(after.ex_data(), before[i].ex_data()); + EXPECT_NE(after.ids(), before[i].ids()); + } else { + // No reallocation and no copy: the untouched cluster's storage, not + // just its contents, is exactly what it was before add() ran. + EXPECT_EQ(after.num(), before[i].num()); + EXPECT_EQ(after.batch_data(), before[i].batch_data()); + EXPECT_EQ(after.ex_data(), before[i].ex_data()); + EXPECT_EQ(after.ids(), before[i].ids()); + } + } +} + TEST(IvfConstructionTest, RebuildMatchesIncrementalEncodingWithSkewedClustersAndTails) { // Cluster sizes 0, 1, 32, 97 cover empty rows and multiple physical tail batches. constexpr size_t kCount = 130; From ff51f60d11788d6824dcdf831f12c4fcae2b18fe Mon Sep 17 00:00:00 2001 From: Tamim Ehsan Date: Sat, 17 Oct 2026 00:15:44 +0800 Subject: [PATCH 04/10] bench(ivf): add a hotpath benchmark showing add() cost now scales with touched clusters, not index size --- benchmarks/hotpaths/CMakeLists.txt | 6 ++ benchmarks/hotpaths/ivf_add.cpp | 104 +++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 benchmarks/hotpaths/ivf_add.cpp diff --git a/benchmarks/hotpaths/CMakeLists.txt b/benchmarks/hotpaths/CMakeLists.txt index 9f356d99..afb26ba6 100644 --- a/benchmarks/hotpaths/CMakeLists.txt +++ b/benchmarks/hotpaths/CMakeLists.txt @@ -21,4 +21,10 @@ if(RABITQ_BUILD_INDEX) "${CMAKE_CURRENT_BINARY_DIR}/sidecar-smoke" 128 64 2) set_tests_properties(SidecarHotpathPrepare PROPERTIES FIXTURES_SETUP sidecar_hotpath) set_tests_properties(SidecarHotpathLoad PROPERTIES FIXTURES_REQUIRED sidecar_hotpath) + + rabitq_hotpath_target(ivf_add index) + add_test(NAME IvfAddHotpathSingle COMMAND rabitq_ivf_add_hotpaths + "${CMAKE_CURRENT_BINARY_DIR}/ivf-add-smoke-single" 64 8 256 16 1 single) + add_test(NAME IvfAddHotpathRouted COMMAND rabitq_ivf_add_hotpaths + "${CMAKE_CURRENT_BINARY_DIR}/ivf-add-smoke-routed" 64 8 256 16 1 routed) endif() diff --git a/benchmarks/hotpaths/ivf_add.cpp b/benchmarks/hotpaths/ivf_add.cpp new file mode 100644 index 00000000..eb179ffe --- /dev/null +++ b/benchmarks/hotpaths/ivf_add.cpp @@ -0,0 +1,104 @@ +// Does IVF::add() cost scale with the whole index, or with the points actually +// added? Builds a base index once, saves it, then times one add() call from a +// freshly loaded (so deterministic) copy per repetition -- never reusing a +// grown index across repetitions, so every repetition starts from the same +// base_n-point state and the hash of the result is comparable across them. +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.hpp" +#include "rabitqlib/core/metric.hpp" +#include "rabitqlib/index/ivf/ivf.hpp" +#include "rabitqlib/index/types.hpp" + +namespace { +void run(int argc, char** argv) { + if (argc != 8) { + throw std::invalid_argument( + "Usage: rabitq_ivf_add_hotpaths path dim clusters base_n add_n reps " + "single|routed" + ); + } + const std::string path(argv[1]); + const size_t dim = bench::number(argv[2]); + const size_t clusters = bench::number(argv[3]); + const size_t base_n = bench::number(argv[4]); + const size_t add_n = bench::number(argv[5]); + const size_t reps = bench::number(argv[6]); + const std::string mode(argv[7]); + if (mode != "single" && mode != "routed") { + throw std::invalid_argument("Mode must be single or routed"); + } + if (clusters < 1 || base_n < clusters) { + throw std::invalid_argument("base_n must be at least clusters"); + } + + const auto base_data = bench::values(bench::product(base_n, dim), 42); + const auto add_data = bench::values(bench::product(add_n, dim), 91); + const auto centroids = bench::values(bench::product(clusters, dim), 7); + // Round-robin: arbitrary but deterministic, and every cluster gets some + // points. Quality of the clustering is irrelevant to a storage-cost benchmark. + std::vector base_assigned(base_n); + for (size_t i = 0; i < base_n; ++i) { + base_assigned[i] = static_cast(i % clusters); + } + // "single": every new point goes to cluster 0 explicitly, the case that + // isolates one touched cluster. "routed": cluster_ids is null, so add() + // does its own nearest-centroid routing, the representative case where + // points land across many clusters. + const std::vector single_ids(add_n, rabitqlib::PID{0}); + const rabitqlib::PID* add_cluster_ids = mode == "single" ? single_ids.data() : nullptr; + + { + rabitqlib::ivf::IVF base(base_n, dim, clusters, 4, rabitqlib::METRIC_L2); + base.construct(base_data.data(), centroids.data(), base_assigned.data(), false, 1); + base.save(path.c_str()); + } + + bench::metadata(dim, base_n, 1); + const char* name = mode == "single" ? "add_single_cluster" : "add_routed"; + uint64_t expected = 0; + for (size_t rep = 0; rep <= reps; ++rep) { + rabitqlib::ivf::IVF index; + index.load(path.c_str()); + + const auto start = bench::Clock::now(); + index.add(add_data.data(), add_n, add_cluster_ids, false, 1); + const double seconds = + std::chrono::duration(bench::Clock::now() - start).count(); + + uint64_t h = 14695981039346656037ULL; + const size_t total = index.max_elements(); + h = bench::hash(&total, sizeof(total), h); + std::vector ids(10); + std::vector distances(10); + index.search(base_data.data(), ids.size(), clusters, ids.data(), distances.data()); + h = bench::hash(ids.data(), ids.size() * sizeof(rabitqlib::PID), h); + h = bench::hash(distances.data(), distances.size() * sizeof(float), h); + + if (rep == 0) { + expected = h; + } else { + if (h != expected) { + throw std::runtime_error("add() result changed between repetitions"); + } + bench::row(name, rep - 1, add_n, seconds, h); + } + } +} +} // namespace + +int main(int argc, char** argv) { + try { + run(argc, argv); + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } +} From f4b2d7c4022d14ff6f223f52ddfdf5ccb1039bde Mon Sep 17 00:00:00 2001 From: Tamim Ehsan Date: Sat, 17 Oct 2026 11:43:07 +0800 Subject: [PATCH 05/10] perf(ivf): reuse a cluster's existing capacity on add() instead of reallocating every call --- src/index/ivf.cpp | 104 ++++++++++++++++++++---- tests/unit/rabitqlib/index/ivf_test.cpp | 31 +++++++ 2 files changed, 121 insertions(+), 14 deletions(-) diff --git a/src/index/ivf.cpp b/src/index/ivf.cpp index c1f0f1a8..21ee9ef4 100644 --- a/src/index/ivf.cpp +++ b/src/index/ivf.cpp @@ -389,18 +389,60 @@ void IVF::add( const size_t rerank_bytes = rerank_vector_bytes(); const PID first_pid = static_cast(max_elements_); - /* Stage every touched cluster's grown storage on the side, so the index is - unchanged on failure and no untouched cluster is ever copied. */ + // True if this cluster's own storage already has enough spare capacity for + // new_sizes[i] -- in which case extend_cluster can write directly into that + // spare region (no allocation, no copy of the unchanged prefix) instead of + // building a separate staged buffer. Growing within capacity never + // reallocates and never moves the old data std::vector::capacity() already + // confirmed is there, so clusters_[i] (read by extend_cluster as the "old" + // view) stays valid throughout; nothing becomes observable until the commit + // loop below bumps each vector's size, which, since capacity already + // covers it, cannot reallocate or throw either. + const auto fits_in_place = [&](size_t i) { + const ClusterStorage& cs = cluster_storage_[i]; + if (cs.batch.capacity() < cluster_batch_bytes(new_sizes[i]) || + cs.ids.capacity() < new_sizes[i]) { + return false; + } + if (raw_reranking_) { + return cs.raw.capacity() >= new_sizes[i] * dim_; + } + if (rerank_bytes > 0) { + return cs.ex.capacity() >= new_sizes[i] * rerank_bytes; + } + return true; + }; + + /* Clusters that outgrow their current capacity are staged on the side, so + the index is unchanged on failure. Growing the staged buffer past what + this call strictly needs means a later add() to the same cluster is + more likely to fit in place instead of staging again. Clusters that + already have room, and untouched clusters, are never staged or copied + at all. */ + std::vector in_place(num_clusters_, false); std::vector> staged(num_clusters_); for (size_t i : touched) { + if (fits_in_place(i)) { + in_place[i] = true; + continue; + } + const ClusterStorage& old_cs = cluster_storage_[i]; ClusterStorage cs; - cs.batch = ByteStorage(cluster_batch_bytes(new_sizes[i])); + const size_t want_batch = cluster_batch_bytes(new_sizes[i]); + cs.batch.reserve(std::max(want_batch, old_cs.batch.capacity() * 2)); + cs.batch.resize(want_batch); + const size_t want_ids = new_sizes[i]; + cs.ids.reserve(std::max(want_ids, old_cs.ids.capacity() * 2)); + cs.ids.resize(want_ids); if (raw_reranking_) { - cs.raw = FloatStorage(new_sizes[i] * dim_); + const size_t want_raw = new_sizes[i] * dim_; + cs.raw.reserve(std::max(want_raw, old_cs.raw.capacity() * 2)); + cs.raw.resize(want_raw); } else if (rerank_bytes > 0) { - cs.ex = ByteStorage(new_sizes[i] * rerank_bytes); + const size_t want_ex = new_sizes[i] * rerank_bytes; + cs.ex.reserve(std::max(want_ex, old_cs.ex.capacity() * 2)); + cs.ex.resize(want_ex); } - cs.ids = IdStorage(new_sizes[i]); staged[i] = std::move(cs); } @@ -411,27 +453,61 @@ void IVF::add( ++index) { const size_t i = touched[static_cast(index)]; try { - ClusterStorage& cs = *staged[i]; + char* batch_ptr; + char* rerank_ptr; + PID* ids_ptr; + if (in_place[i]) { + // Grow this cluster's own storage before writing into it. resize() + // growing value-initializes (zero-fills) only the newly added + // elements -- the existing prefix is untouched -- and, since + // capacity was already confirmed sufficient, cannot reallocate or + // throw. Doing this before extend_cluster writes is essential: + // doing it after would re-zero exactly what was just written. + ClusterStorage& cs = cluster_storage_[i]; + cs.batch.resize(cluster_batch_bytes(new_sizes[i])); + cs.ids.resize(new_sizes[i]); + if (raw_reranking_) { + cs.raw.resize(new_sizes[i] * dim_); + } else if (rerank_bytes > 0) { + cs.ex.resize(new_sizes[i] * rerank_bytes); + } + batch_ptr = reinterpret_cast(cs.batch.data()); + rerank_ptr = rerank_bytes > 0 + ? (raw_reranking_ ? reinterpret_cast(cs.raw.data()) + : reinterpret_cast(cs.ex.data())) + : nullptr; + ids_ptr = cs.ids.data(); + } else { + ClusterStorage& cs = *staged[i]; + batch_ptr = reinterpret_cast(cs.batch.data()); + rerank_ptr = rerank_bytes > 0 + ? (raw_reranking_ ? reinterpret_cast(cs.raw.data()) + : reinterpret_cast(cs.ex.data())) + : nullptr; + ids_ptr = cs.ids.data(); + } extend_cluster( clusters_[i], rows_of[i], data, initializer_->centroid(static_cast(i)), first_pid, - reinterpret_cast(cs.batch.data()), - rerank_bytes > 0 ? (raw_reranking_ ? reinterpret_cast(cs.raw.data()) - : reinterpret_cast(cs.ex.data())) - : nullptr, - cs.ids.data(), + batch_ptr, + rerank_ptr, + ids_ptr, config ); } catch (...) { failure.capture(); } } failure.rethrow(); - /* Commit. Untouched clusters are never staged, so they are unaffected by this. */ + /* Commit. Untouched clusters are never staged or resized, so they are + unaffected by this. In-place clusters were already grown and written + above; only staged (reallocated) clusters still need moving in. */ for (size_t i : touched) { - cluster_storage_[i] = std::move(*staged[i]); + if (!in_place[i]) { + cluster_storage_[i] = std::move(*staged[i]); + } } max_elements_ = new_num_points; clusters_.clear(); diff --git a/tests/unit/rabitqlib/index/ivf_test.cpp b/tests/unit/rabitqlib/index/ivf_test.cpp index 27abb227..25a252fa 100644 --- a/tests/unit/rabitqlib/index/ivf_test.cpp +++ b/tests/unit/rabitqlib/index/ivf_test.cpp @@ -696,6 +696,37 @@ TEST(IvfAddTest, AddingToOneClusterLeavesOtherClustersUntouched) { } } +TEST(IvfAddTest, SecondSmallAddToTheSameClusterReusesCapacityInPlace) { + // The first add to a freshly-constructed cluster must reallocate (capacity + // starts exact-fit), but it over-allocates for future growth; a second small + // add to the same cluster should then need no allocation at all. + constexpr size_t kOld = 40; + const auto points = random_rows(kOld + 2, 851); + const auto centroids = random_rows(kDynClusters, 852); + const std::vector assigned(kOld, 0); + + IVF index(kOld, kDynDim, kDynClusters, 4, METRIC_L2); + index.construct(points.data(), centroids.data(), assigned.data(), false, 1); + + const Cluster before_first = IvfTestAccess::cluster_view(index, 0); + const PID route_to_zero = 0; + index.add(points.data() + (kOld * kDynDim), 1, &route_to_zero, false, 1); + const Cluster after_first = IvfTestAccess::cluster_view(index, 0); + EXPECT_NE(after_first.batch_data(), before_first.batch_data()); + EXPECT_EQ(after_first.num(), kOld + 1); + + index.add(points.data() + ((kOld + 1) * kDynDim), 1, &route_to_zero, false, 1); + const Cluster after_second = IvfTestAccess::cluster_view(index, 0); + EXPECT_EQ(after_second.num(), kOld + 2); + EXPECT_EQ(after_second.batch_data(), after_first.batch_data()); + EXPECT_EQ(after_second.ex_data(), after_first.ex_data()); + EXPECT_EQ(after_second.ids(), after_first.ids()); + // Byte-identical correctness of repeated small adds (not just this pointer + // behavior) is already covered by + // IvfAddTest.EveryWayOfAddingTheSamePointsGivesByteIdenticalIndexes's + // "chunked" case, which exercises this same in-place-reuse path. +} + TEST(IvfConstructionTest, RebuildMatchesIncrementalEncodingWithSkewedClustersAndTails) { // Cluster sizes 0, 1, 32, 97 cover empty rows and multiple physical tail batches. constexpr size_t kCount = 130; From f2c615534fa5e2e1c4699f4f64a14d4d1d45d880 Mon Sep 17 00:00:00 2001 From: Tamim Ehsan Date: Sun, 18 Oct 2026 21:22:56 +0800 Subject: [PATCH 06/10] test(ivf): regenerate pre-refactor fixtures with uneven clusters and commit their generator --- .../CMakeLists.txt | 13 ++++ .../pre-ivf-per-cluster-storage/README.md | 50 +++++++++---- .../pre-ivf-per-cluster-storage/generate.cpp | 69 ++++++++++++++++++ .../ivf32_ip.expected | 34 ++++----- .../ivf32_ip.index | Bin 10750 -> 40606 bytes .../ivf32_l2.expected | 34 ++++----- .../ivf32_l2.index | Bin 10750 -> 40606 bytes .../ivf4_ip.expected | 34 ++++----- .../pre-ivf-per-cluster-storage/ivf4_ip.index | Bin 3622 -> 12526 bytes .../ivf4_l2.expected | 34 ++++----- .../pre-ivf-per-cluster-storage/ivf4_l2.index | Bin 3622 -> 12526 bytes .../pre-ivf-per-cluster-storage/manifest.json | 20 ++--- .../index/historical_persistence_test.cpp | 45 ++++++++---- 13 files changed, 224 insertions(+), 109 deletions(-) create mode 100644 tests/fixtures/persistence/pre-ivf-per-cluster-storage/CMakeLists.txt create mode 100644 tests/fixtures/persistence/pre-ivf-per-cluster-storage/generate.cpp diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/CMakeLists.txt b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/CMakeLists.txt new file mode 100644 index 00000000..e5838291 --- /dev/null +++ b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.20) +project(rabitq_pre_ivf_per_cluster_storage_fixtures LANGUAGES CXX) + +if(NOT IS_DIRECTORY "${RABITQ_HISTORICAL_SOURCE}") + message(FATAL_ERROR "Set RABITQ_HISTORICAL_SOURCE to the pinned pre-refactor 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/pre-ivf-per-cluster-storage/README.md b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/README.md index 4e4f36cf..3a544c29 100644 --- a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/README.md +++ b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/README.md @@ -1,17 +1,37 @@ -# IVF snapshot immediately before the per-cluster-storage refactor +# IVF fixtures from before per-cluster storage -These four files were written by `rabitqlib::ivf::IVF::save()` on `main` at commit -`64fc943e0e92fa7e18eec1477050b23342a976f8`, before `ivf.hpp`/`src/index/ivf.cpp` were changed -to give each cluster its own independently-growable storage instead of one combined buffer -across all clusters. Unlike `../v0.5.2/`, which proves format stability across a historical -release, this snapshot proves this specific refactor didn't change a single byte of the format -it reads or writes: it is pinned to the implementation immediately prior to that change, not a -distant past release. +These files were written by `main` at commit +`64fc943e0e92fa7e18eec1477050b23342a976f8`, the last commit where IVF kept every +cluster in one shared buffer, 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. -Generated with the same construction as `../generate.cpp`'s IVF cases (`kCount=33`, -`kDim=65`, `kQueries=3`, `kTopK=5`, the same LCG-seeded deterministic data, one cluster, point 0 -removed after construction), covering `ivf4`/`ivf32` x `l2`/`ip`. The one-off generator used to -produce them was not committed — it is a near-identical copy of `../generate.cpp` restricted to -the IVF cases and updated to this repository's current (post-0.7.0-refactor) include paths, run -once and discarded; `manifest.json` in this directory records the producer and SHA-256 hashes of -every file, the same way `../manifest.json` does for `v0.5.2`. +The four cases cover four-bit and raw-vector (`nbits=32`) IVF for L2 and inner +product. Each index holds 130 points in 65 original dimensions (96 padded) across +four clusters of 0, 1, 32 and 97 points, so the file has an empty cluster, a +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. + +## Deliberate regeneration + +Build the generator only against an isolated copy of the pinned commit: + +```bash +mkdir -p build/fixtures/pre-ivf-per-cluster-storage +git archive 64fc943 | tar -x -C build/fixtures/pre-ivf-per-cluster-storage +cmake -S tests/fixtures/persistence/pre-ivf-per-cluster-storage \ + -B build/fixtures/pre-ivf-generator \ + -DRABITQ_HISTORICAL_SOURCE="$PWD/build/fixtures/pre-ivf-per-cluster-storage" \ + -DCMAKE_BUILD_TYPE=Release +cmake --build build/fixtures/pre-ivf-generator --target generate_fixtures +``` + +Run `generate_fixtures` with a new output directory. Rotation is seeded from +`random_device`, so the bytes differ on every run; each index and its `.expected` +file form a fixed pair. Record new hashes in the manifest before adopting them. diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/generate.cpp b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/generate.cpp new file mode 100644 index 00000000..1b82de02 --- /dev/null +++ b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/generate.cpp @@ -0,0 +1,69 @@ +// Build only against the pinned pre-refactor source, never the current checkout. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "rabitqlib/core/metric.hpp" +#include "rabitqlib/index/ivf/ivf.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; + for (auto metric : {METRIC_L2, METRIC_IP}) { + for (const std::string kind : {"ivf4", "ivf32"}) { + const std::string name = kind + (metric == METRIC_L2 ? "_l2" : "_ip"); + const auto path = (directory / (name + ".index")).string(); + std::vector ids(kQueries * kTopK); + std::vector distances(ids.size()); + ivf::IVF index(kCount, kDim, kClusters, kind == "ivf32" ? 32 : 4, metric); + 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/pre-ivf-per-cluster-storage/ivf32_ip.expected b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_ip.expected index a3382ec4..750cfd52 100644 --- a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_ip.expected +++ b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_ip.expected @@ -1,17 +1,17 @@ -33 65 3 5 --1.2578125 -3.078125 -3.328125 0.67578125 3.171875 -0.2578125 -2.796875 0.91015625 -3.859375 0.328125 2.296875 -2.0859375 -0.66015625 -3.3046875 2.19140625 2.1484375 2.328125 3.7890625 3.3515625 0.08203125 1.41796875 1.43359375 -1.1484375 1.4375 2.55078125 2.0703125 -1.5859375 -2.8046875 -2.2890625 1.73828125 -2.41796875 -3.09765625 1.53515625 -3.6953125 -0.66796875 0.67578125 0.96484375 -2.9296875 2.41796875 1.25390625 -1.8984375 3.14453125 2.359375 0.65625 -2.52734375 -0.17578125 -0.80859375 -2.171875 -1.63671875 -0.421875 -3.08203125 -0.1328125 -1.94140625 2.734375 2.1875 -2.1015625 3.828125 3.82421875 -3.61328125 -0.9921875 -3.27734375 2.9765625 2.29296875 0.7421875 3.48828125 -2.58203125 1.0234375 -3.12109375 1.83203125 -2.29296875 1.05859375 2.15234375 -2.5390625 -2.3125 -2.7265625 -3.33203125 1.671875 3.6796875 3.53515625 -1.75 0.6328125 -3.1171875 -2.6875 -0.3671875 -2.640625 -2.52734375 -0.65625 1.89453125 -1.08984375 0.25390625 -3.65234375 1.08203125 -0.55078125 3.6171875 2.80078125 2.265625 2.80078125 0.98828125 -1.03515625 1.0234375 -0.04296875 2.95703125 1.8125 -1.6796875 2.75390625 -3.70703125 1.60546875 0.27734375 -3.2265625 -2.57421875 -0.875 1.40234375 -3.55078125 2.80859375 2.953125 3.37109375 -0.02734375 3.16796875 3.23828125 -0.21875 0.57421875 -2.82421875 3.80078125 1.66796875 -3.30859375 -1.21875 1.05078125 1.0546875 2.2890625 3.84765625 1.58984375 -0.7734375 -0.48046875 1.28125 -1.30859375 -1.27734375 2.67578125 1.19921875 2.3515625 -0.140625 -3.828125 -3.84375 1.0703125 -2.27734375 -2.05859375 3.59375 1.3359375 -3.44140625 -0.40234375 -0.26953125 1.671875 -3.24609375 -3.42578125 0.76953125 -0.67578125 -1.53125 -3.2578125 3.65234375 -2.9921875 3.4609375 0.25390625 1.5390625 -0.78125 2.109375 0.99609375 1.5078125 -0.33203125 -1.4296875 -0.01953125 -0.625 2.609375 0.796875 -1.1875 1.125 3.62890625 -3.23046875 -2.91015625 3.0859375 2.23828125 -3.6015625 -1.38671875 3.0703125 -2.2890625 -3.375 3.0390625 -2.02734375 -0.9140625 1 -0.0546875 0.66796875 -3.52734375 -2.8046875 0.93359375 -3.21875 -2.05859375 -17 -111.448395 -26 -98.9153748 -30 -97.6609802 -9 -70.2647552 -18 -59.205368 -10 -88.1517792 -4 -52.7075806 -24 -42.3019562 -29 -40.4504089 -23 -36.7008667 -8 -65.2822876 -31 -45.8321991 -19 -41.9718628 -26 -35.1358032 -14 -33.196228 +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/pre-ivf-per-cluster-storage/ivf32_ip.index b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_ip.index index d596f2c7330d9be019e743c6f0d46c92e67b999f..185e737d127da41da6be8b3003b5b34ca35adf09 100644 GIT binary patch literal 40606 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 T1k;4jgwll3gws%HB53{}W=d^P delta 2837 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 E2Y8=cY5)KL diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/manifest.json b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/manifest.json index 946e9655..078241d8 100644 --- a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/manifest.json +++ b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/manifest.json @@ -1,18 +1,18 @@ { "source_tag": "main", "source_commit": "64fc943e0e92fa7e18eec1477050b23342a976f8", - "writer": "one-off generator matching tests/fixtures/persistence/generate.cpp's IVF cases, run against the unmodified IVF class immediately before the per-cluster-storage refactor (not committed to the repo; see README.md in this directory)", - "producer": "Linux x86-64, g++, one thread, native optimization OFF", + "writer": "tests/fixtures/persistence/pre-ivf-per-cluster-storage/generate.cpp", + "producer": "Linux x86-64, g++ 14.3.0, Release, one thread, native optimization OFF", "byte_order": "little", "size_t_bytes": 8, "sha256": { - "pre-ivf-per-cluster-storage/ivf32_ip.expected": "e0d953927d08b619c75b9424a032f136d32d3756d3fde4bf8c6abe0b7b743a28", - "pre-ivf-per-cluster-storage/ivf32_ip.index": "db6b66acf40caa4063c2d788bd37e4ae129865cfa6e6d32fd19afc846541ea2d", - "pre-ivf-per-cluster-storage/ivf32_l2.expected": "a9ba0430f4c8a517168aa1ff75bad72d7f6089161561412a75976fc970400f9c", - "pre-ivf-per-cluster-storage/ivf32_l2.index": "3bdfadfe289d6c0b474096fff990157ca76df0c8b27478ff6c32fcb1fc81a940", - "pre-ivf-per-cluster-storage/ivf4_ip.expected": "81a81577f76e75bb4faefbfe559176e5b0846dadd04332d6a9913eb13dfaaf40", - "pre-ivf-per-cluster-storage/ivf4_ip.index": "7fc3ab1b52848e5c9c014d62bf7fdedc731c2beed6e491edf58dd63efb462a3b", - "pre-ivf-per-cluster-storage/ivf4_l2.expected": "7bbd23134af0cdd4adc794df103bd1eb2aba0d362be3f12d3019e02a3d8d3165", - "pre-ivf-per-cluster-storage/ivf4_l2.index": "9966c3bddc306d2d2d062d4a696750adde3daa8e7c086add0fa2411669c488cf" + "pre-ivf-per-cluster-storage/ivf32_ip.expected": "3ecb6604dce2108556e5e48130d4e9902d7ef7f0ef81aba39a3caa40b62230fe", + "pre-ivf-per-cluster-storage/ivf32_ip.index": "e7c2850ce9b09b4a660e85b3aafe03854d206cb2659801e5ef4752f295415805", + "pre-ivf-per-cluster-storage/ivf32_l2.expected": "7843685eadb19c6023df353aa6602e56f5042e4d0c6c2b1ac6019949b7459734", + "pre-ivf-per-cluster-storage/ivf32_l2.index": "4b0f98a898edf9cf157d226c51ba74a313d5fb5c055dc5663e14ce3a631f121b", + "pre-ivf-per-cluster-storage/ivf4_ip.expected": "2107393b9f53a79d5b07b29771d069b7bedffb1608f105913ff95899dc27f0e0", + "pre-ivf-per-cluster-storage/ivf4_ip.index": "928f4440a375f2cbdb5d7f524b3b52b33e6b9dc176dd7b86dbbaef7a6aa091c6", + "pre-ivf-per-cluster-storage/ivf4_l2.expected": "7de95886d52dfbb5300890f920256fd7927eae2a4528223753e6f44f76a3e20d", + "pre-ivf-per-cluster-storage/ivf4_l2.index": "eb73f9066fef993a5f7e833641adb8bddb1e95cdc04addb2261a8e10bec0c41f" } } diff --git a/tests/unit/rabitqlib/index/historical_persistence_test.cpp b/tests/unit/rabitqlib/index/historical_persistence_test.cpp index d8240a96..69ab4e13 100644 --- a/tests/unit/rabitqlib/index/historical_persistence_test.cpp +++ b/tests/unit/rabitqlib/index/historical_persistence_test.cpp @@ -5,6 +5,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -18,17 +21,26 @@ namespace rabitqlib { namespace { -// padded_dim is the historical/fixed padding rule the fixture set was written -// under (v0.5.2 always padded to a 64-coordinate multiple; sets generated under -// the current writer store and report their own actual padded_dim instead). +std::string read_bytes(const std::filesystem::path& path) { + std::ifstream input(path, std::ios::binary); + return {std::istreambuf_iterator(input), std::istreambuf_iterator()}; +} + +// padded_dim follows the padding rule of the writer that produced the fixture set. struct HistoricalFixtureCase { std::string directory; // subdirectory under RABITQ_TEST_FIXTURE_ROOT std::string name; size_t padded_dim; + size_t count = 33; + size_t ivf_nprobe = 1; + bool resave_identical = false; // the current writer must reproduce the file }; -class HistoricalPersistenceTest - : public ::testing::TestWithParam {}; +void PrintTo(const HistoricalFixtureCase& value, std::ostream* output) { + *output << value.directory << '/' << value.name; +} + +class HistoricalPersistenceTest : public ::testing::TestWithParam {}; TEST_P(HistoricalPersistenceTest, LoadsSearchesResavesAndPreservesStateAfterFailedLoad) { const std::filesystem::path directory = @@ -40,7 +52,7 @@ TEST_P(HistoricalPersistenceTest, LoadsSearchesResavesAndPreservesStateAfterFail ASSERT_TRUE(expected.is_open()); size_t count = 0, dim = 0, nq = 0, k = 0; expected >> count >> dim >> nq >> k; - ASSERT_EQ(count, 33U); + ASSERT_EQ(count, GetParam().count); ASSERT_EQ(dim, 65U); ASSERT_EQ(nq, 3U); ASSERT_EQ(k, 5U); @@ -78,6 +90,9 @@ TEST_P(HistoricalPersistenceTest, LoadsSearchesResavesAndPreservesStateAfterFail search(); compare(); index.save(output.string().c_str()); + if (GetParam().resave_identical) { + EXPECT_TRUE(read_bytes(output) == read_bytes(path)); + } index.load(output.string().c_str()); check_counts(); search(); @@ -94,7 +109,9 @@ TEST_P(HistoricalPersistenceTest, LoadsSearchesResavesAndPreservesStateAfterFail exercise(index, [&] { EXPECT_EQ(index.padded_dim(), expected_padded_dim); EXPECT_EQ(index.initializer_type(), ivf::InitializerType::Flat); - index.search_batch(queries.data(), nq, k, 1, ids.data(), distances.data()); + index.search_batch( + queries.data(), nq, k, GetParam().ivf_nprobe, ids.data(), distances.data() + ); }); } else if (name.find("hnsw") == 0) { hnsw::HierarchicalNSW index; @@ -138,19 +155,15 @@ INSTANTIATE_TEST_SUITE_P( [](const auto& info) { return info.param.name; } ); -// Pinned to the IVF implementation immediately before the per-cluster-storage -// refactor (see tests/fixtures/persistence/pre-ivf-per-cluster-storage/README.md), -// not a distant historical release: proves this specific change didn't move the -// on-disk format it reads or writes. kDim=65 rounds up to a 96 padded_dim under -// the current (32-multiple) padding rule, unlike v0.5.2's fixed 128. +// Four uneven IVF clusters written before per-cluster storage; dim 65 pads to 96. INSTANTIATE_TEST_SUITE_P( PreIvfPerClusterStorage, HistoricalPersistenceTest, ::testing::Values( - HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf4_l2", 96}, - HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf4_ip", 96}, - HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf32_l2", 96}, - HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf32_ip", 96} + HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf4_l2", 96, 130, 4, true}, + HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf4_ip", 96, 130, 4, true}, + HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf32_l2", 96, 130, 4, true}, + HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf32_ip", 96, 130, 4, true} ), [](const auto& info) { return info.param.name; } ); From 319cce6342f53e49aee427c55d5318aab6a996e5 Mon Sep 17 00:00:00 2001 From: Tamim Ehsan Date: Sun, 18 Oct 2026 21:22:56 +0800 Subject: [PATCH 07/10] fix(ivf): keep failed adds atomic, grow clusters exactly, and add reserve() --- benchmarks/hotpaths/ivf_add.cpp | 14 +- include/rabitqlib/index/ivf/ivf.hpp | 86 +++++-- src/index/ivf.cpp | 327 ++++++++++++------------ tests/unit/rabitqlib/index/ivf_test.cpp | 241 ++++++++++++++--- 4 files changed, 450 insertions(+), 218 deletions(-) diff --git a/benchmarks/hotpaths/ivf_add.cpp b/benchmarks/hotpaths/ivf_add.cpp index eb179ffe..7ecbe11e 100644 --- a/benchmarks/hotpaths/ivf_add.cpp +++ b/benchmarks/hotpaths/ivf_add.cpp @@ -1,8 +1,4 @@ -// Does IVF::add() cost scale with the whole index, or with the points actually -// added? Builds a base index once, saves it, then times one add() call from a -// freshly loaded (so deterministic) copy per repetition -- never reusing a -// grown index across repetitions, so every repetition starts from the same -// base_n-point state and the hash of the result is comparable across them. +// Times one IVF::add() per repetition, each on a freshly loaded copy of the base index. #include #include #include @@ -42,16 +38,12 @@ void run(int argc, char** argv) { const auto base_data = bench::values(bench::product(base_n, dim), 42); const auto add_data = bench::values(bench::product(add_n, dim), 91); const auto centroids = bench::values(bench::product(clusters, dim), 7); - // Round-robin: arbitrary but deterministic, and every cluster gets some - // points. Quality of the clustering is irrelevant to a storage-cost benchmark. + // Round-robin assignment; clustering quality is irrelevant to storage cost. std::vector base_assigned(base_n); for (size_t i = 0; i < base_n; ++i) { base_assigned[i] = static_cast(i % clusters); } - // "single": every new point goes to cluster 0 explicitly, the case that - // isolates one touched cluster. "routed": cluster_ids is null, so add() - // does its own nearest-centroid routing, the representative case where - // points land across many clusters. + // "single" sends every new point to cluster 0; "routed" lets add() pick the nearest. const std::vector single_ids(add_n, rabitqlib::PID{0}); const rabitqlib::PID* add_cluster_ids = mode == "single" ? single_ids.data() : nullptr; diff --git a/include/rabitqlib/index/ivf/ivf.hpp b/include/rabitqlib/index/ivf/ivf.hpp index bf85e324..08b53085 100644 --- a/include/rabitqlib/index/ivf/ivf.hpp +++ b/include/rabitqlib/index/ivf/ivf.hpp @@ -37,18 +37,45 @@ class IVF { using FloatStorage = std::vector>; using IdStorage = std::vector>; - // One cluster's owned storage. Independent of every other cluster's, so growing - // one cluster never reallocates or copies another's. - struct ClusterStorage { - ByteStorage batch; // 1-bit code and factors - ByteStorage ex; // extra-bit codes and packed factors - FloatStorage raw; // original vectors for raw reranking - IdStorage ids; + // One cluster's own storage, independent of every other cluster's. + class ClusterStorage { + public: + explicit ClusterStorage(bool raw_reranking) : raw_reranking_(raw_reranking) {} + + ByteStorage batch_storage; // 1-bit code and factors + ByteStorage ex_storage; // extra-bit codes and packed factors + FloatStorage raw_storage; // original vectors for raw reranking + IdStorage id_storage; // point IDs of this cluster + + [[nodiscard]] char* batch_data() { + return reinterpret_cast(batch_storage.data()); + } + + [[nodiscard]] const char* batch_data() const { + return reinterpret_cast(batch_storage.data()); + } + + [[nodiscard]] char* ex_data() { + return raw_reranking_ ? reinterpret_cast(raw_storage.data()) + : reinterpret_cast(ex_storage.data()); + } + + [[nodiscard]] const char* ex_data() const { + return raw_reranking_ ? reinterpret_cast(raw_storage.data()) + : reinterpret_cast(ex_storage.data()); + } + + [[nodiscard]] PID* ids() { return id_storage.data(); } + + [[nodiscard]] const PID* ids() const { return id_storage.data(); } + + private: + bool raw_reranking_; // raw vectors replace extra-bit codes }; InitializerType initializer_type_ = InitializerType::Auto; - std::unique_ptr initializer_; // initializer for candidate clusters - std::vector cluster_storage_; // one entry per cluster + std::unique_ptr initializer_; // initializer for candidate clusters + std::vector cluster_storage_; // one entry per cluster size_t max_elements_ = 0; // Logical capacity; construct fills it, add grows it. size_t num_removed_ = 0; // Recovered from removal markers when loading. size_t dim_ = 0; @@ -72,17 +99,30 @@ class IVF { quantize_cluster(Cluster&, const std::vector&, const float*, const float*, float*, float*, const quant::RabitqConfig&); void - extend_cluster(const Cluster&, const std::vector&, const float*, const float*, PID, char*, char*, PID*, const quant::RabitqConfig&) + extend_cluster(const Cluster&, const std::vector&, const float*, const float*, PID, char*, char*, PID*, char*, const quant::RabitqConfig&) const; // True if `n` points cannot be addressed by the storage size computations [[nodiscard]] bool exceeds_storage(size_t n) const; - // get num of bytes used for one cluster's 1-bit code and corresponding factors - [[nodiscard]] size_t cluster_batch_bytes(size_t cluster_size) const; + [[nodiscard]] static size_t ids_bytes(size_t cluster_size) { + return sizeof(PID) * cluster_size; + } + + // get num of bytes used for 1-bit code and corresponding factors + [[nodiscard]] size_t batch_data_bytes(size_t cluster_size) const; [[nodiscard]] inline size_t rerank_vector_bytes() const; + [[nodiscard]] inline size_t ex_data_bytes(size_t cluster_size) const; + + // Size every buffer of a cluster's storage for `n` points + void resize_storage(ClusterStorage& cs, size_t n) const; + + void reserve_storage(ClusterStorage& cs, size_t n) const; + + [[nodiscard]] bool has_capacity(const ClusterStorage& cs, size_t n) const; + void allocate_memory(const std::vector&); void init_clusters(const std::vector&); @@ -148,9 +188,11 @@ class IVF { * data the centroids were trained on. Point `i` of `data` receives PID * `num_stored_points() + i`. The index is unchanged if this throws. * - * The grown index is built next to the old one, so a call needs memory for both and - * takes time proportional to the whole index, not just to `n`: prefer a few large - * calls to many small ones. `construct` expects `max_elements()` rows afterwards. + * Only clusters that receive points are touched. Each one is rebuilt next to its old + * storage, so a call needs extra memory for, and takes time proportional to, those + * clusters plus `n`; prefer a few large calls to many small ones. A cluster with + * enough capacity from `reserve` grows in place instead. `construct` expects + * `max_elements()` rows afterwards. * Not safe to call while another thread searches, adds or removes. * * @param data New points (n * dim) @@ -169,6 +211,20 @@ class IVF { size_t num_threads = std::numeric_limits::max() ); + /** + * @brief Preallocate room for a cluster to hold `num_points` points. + * + * Later `add` calls grow the cluster in place, without copying it, until it holds more + * than `num_points`. Does nothing if the cluster already has that capacity. Reserved + * capacity is not saved: a loaded index has none. The index is unchanged if this + * throws. Not safe to call while another thread searches, adds or removes. + * + * @param cluster_id Cluster to grow + * @param num_points Total number of points the cluster can hold, including its + * current ones + */ + void reserve(PID cluster_id, size_t num_points); + /** * @brief Exclude points from all later search results. * diff --git a/src/index/ivf.cpp b/src/index/ivf.cpp index 21ee9ef4..f0564836 100644 --- a/src/index/ivf.cpp +++ b/src/index/ivf.cpp @@ -52,7 +52,7 @@ bool IVF::exceeds_storage(size_t n) const { n > max_size / batch_bytes || (rerank_bytes != 0 && n > max_size / rerank_bytes); } -size_t IVF::cluster_batch_bytes(size_t cluster_size) const { +size_t IVF::batch_data_bytes(size_t cluster_size) const { return div_round_up(cluster_size, fastscan::kBatchSize) * BatchDataMap::data_bytes(padded_dim_); } @@ -64,6 +64,45 @@ inline size_t IVF::rerank_vector_bytes() const { : ExDataMap::data_bytes(padded_dim_, ex_bits_); } +inline size_t IVF::ex_data_bytes(size_t cluster_size) const { + return rerank_vector_bytes() * cluster_size; +} + +void IVF::resize_storage(ClusterStorage& cs, size_t n) const { + cs.batch_storage.resize(batch_data_bytes(n)); + if (rerank_vector_bytes() > 0) { + if (raw_reranking_) { + cs.raw_storage.resize(n * dim_); + } else { + cs.ex_storage.resize(ex_data_bytes(n)); + } + } + cs.id_storage.resize(n); +} + +void IVF::reserve_storage(ClusterStorage& cs, size_t n) const { + cs.batch_storage.reserve(batch_data_bytes(n)); + if (rerank_vector_bytes() > 0) { + if (raw_reranking_) { + cs.raw_storage.reserve(n * dim_); + } else { + cs.ex_storage.reserve(ex_data_bytes(n)); + } + } + cs.id_storage.reserve(n); +} + +bool IVF::has_capacity(const ClusterStorage& cs, size_t n) const { + if (cs.batch_storage.capacity() < batch_data_bytes(n) || cs.id_storage.capacity() < n) { + return false; + } + if (rerank_vector_bytes() == 0) { + return true; + } + return raw_reranking_ ? cs.raw_storage.capacity() >= n * dim_ + : cs.ex_storage.capacity() >= ex_data_bytes(n); +} + void IVF::free_memory() { initializer_.reset(); std::vector().swap(cluster_storage_); @@ -238,19 +277,9 @@ void IVF::allocate_memory(const std::vector& cluster_sizes) { this->initializer_ = std::make_unique(padded_dim_, num_clusters_, metric_type_); } - const size_t rerank_bytes = rerank_vector_bytes(); - cluster_storage_.assign(num_clusters_, ClusterStorage{}); + cluster_storage_.assign(num_clusters_, ClusterStorage(raw_reranking_)); for (size_t i = 0; i < num_clusters_; ++i) { - ClusterStorage& cs = cluster_storage_[i]; - cs.batch = ByteStorage(cluster_batch_bytes(cluster_sizes[i])); - if (rerank_bytes > 0) { - if (raw_reranking_) { - cs.raw = FloatStorage(cluster_sizes[i] * dim_); - } else { - cs.ex = ByteStorage(cluster_sizes[i] * rerank_bytes); - } - } - cs.ids = IdStorage(cluster_sizes[i]); + resize_storage(cluster_storage_[i], cluster_sizes[i]); } this->ip_func_ = select_excode_ipfunc(ex_bits_); @@ -263,11 +292,9 @@ void IVF::init_clusters(const std::vector& cluster_sizes) { this->clusters_.reserve(num_clusters_); for (size_t i = 0; i < num_clusters_; ++i) { ClusterStorage& cs = cluster_storage_[i]; - char* current_batch_data = reinterpret_cast(cs.batch.data()); - char* current_ex_data = raw_reranking_ ? reinterpret_cast(cs.raw.data()) - : reinterpret_cast(cs.ex.data()); + char* current_ex_data = rerank_vector_bytes() > 0 ? cs.ex_data() : nullptr; this->clusters_.emplace_back( - cluster_sizes[i], current_batch_data, current_ex_data, cs.ids.data() + cluster_sizes[i], cs.batch_data(), current_ex_data, cs.ids() ); } } @@ -387,63 +414,27 @@ void IVF::add( } const size_t rerank_bytes = rerank_vector_bytes(); + const size_t batch_bytes = BatchDataMap::data_bytes(padded_dim_); const PID first_pid = static_cast(max_elements_); - // True if this cluster's own storage already has enough spare capacity for - // new_sizes[i] -- in which case extend_cluster can write directly into that - // spare region (no allocation, no copy of the unchanged prefix) instead of - // building a separate staged buffer. Growing within capacity never - // reallocates and never moves the old data std::vector::capacity() already - // confirmed is there, so clusters_[i] (read by extend_cluster as the "old" - // view) stays valid throughout; nothing becomes observable until the commit - // loop below bumps each vector's size, which, since capacity already - // covers it, cannot reallocate or throw either. - const auto fits_in_place = [&](size_t i) { - const ClusterStorage& cs = cluster_storage_[i]; - if (cs.batch.capacity() < cluster_batch_bytes(new_sizes[i]) || - cs.ids.capacity() < new_sizes[i]) { - return false; - } - if (raw_reranking_) { - return cs.raw.capacity() >= new_sizes[i] * dim_; - } - if (rerank_bytes > 0) { - return cs.ex.capacity() >= new_sizes[i] * rerank_bytes; - } - return true; - }; - - /* Clusters that outgrow their current capacity are staged on the side, so - the index is unchanged on failure. Growing the staged buffer past what - this call strictly needs means a later add() to the same cluster is - more likely to fit in place instead of staging again. Clusters that - already have room, and untouched clusters, are never staged or copied - at all. */ - std::vector in_place(num_clusters_, false); - std::vector> staged(num_clusters_); - for (size_t i : touched) { - if (fits_in_place(i)) { - in_place[i] = true; - continue; + /* Rebuild clusters without reserved capacity on the side; the rest grow in place */ + std::vector> staged(touched.size()); + std::vector tails(touched.size()); // repacked tails of in-place clusters + for (size_t t = 0; t < touched.size(); ++t) { + const size_t i = touched[t]; + if (has_capacity(cluster_storage_[i], new_sizes[i])) { + const size_t full_batches = clusters_[i].num() / fastscan::kBatchSize; + tails[t].resize(batch_data_bytes(new_sizes[i]) - (full_batches * batch_bytes)); + } else { + staged[t].emplace(raw_reranking_); + resize_storage(*staged[t], new_sizes[i]); } - const ClusterStorage& old_cs = cluster_storage_[i]; - ClusterStorage cs; - const size_t want_batch = cluster_batch_bytes(new_sizes[i]); - cs.batch.reserve(std::max(want_batch, old_cs.batch.capacity() * 2)); - cs.batch.resize(want_batch); - const size_t want_ids = new_sizes[i]; - cs.ids.reserve(std::max(want_ids, old_cs.ids.capacity() * 2)); - cs.ids.resize(want_ids); - if (raw_reranking_) { - const size_t want_raw = new_sizes[i] * dim_; - cs.raw.reserve(std::max(want_raw, old_cs.raw.capacity() * 2)); - cs.raw.resize(want_raw); - } else if (rerank_bytes > 0) { - const size_t want_ex = new_sizes[i] * rerank_bytes; - cs.ex.reserve(std::max(want_ex, old_cs.ex.capacity() * 2)); - cs.ex.resize(want_ex); + } + // Growing within capacity cannot reallocate or throw, so old cluster views stay valid + for (size_t t = 0; t < touched.size(); ++t) { + if (!staged[t]) { + resize_storage(cluster_storage_[touched[t]], new_sizes[touched[t]]); } - staged[i] = std::move(cs); } // An exception must not leave an OpenMP region, so keep the first one and rethrow it. @@ -451,62 +442,49 @@ void IVF::add( #pragma omp parallel for schedule(dynamic) num_threads(resolve_num_threads(num_threads)) for (std::ptrdiff_t index = 0; index < static_cast(touched.size()); ++index) { - const size_t i = touched[static_cast(index)]; + const auto t = static_cast(index); + const size_t i = touched[t]; try { - char* batch_ptr; - char* rerank_ptr; - PID* ids_ptr; - if (in_place[i]) { - // Grow this cluster's own storage before writing into it. resize() - // growing value-initializes (zero-fills) only the newly added - // elements -- the existing prefix is untouched -- and, since - // capacity was already confirmed sufficient, cannot reallocate or - // throw. Doing this before extend_cluster writes is essential: - // doing it after would re-zero exactly what was just written. - ClusterStorage& cs = cluster_storage_[i]; - cs.batch.resize(cluster_batch_bytes(new_sizes[i])); - cs.ids.resize(new_sizes[i]); - if (raw_reranking_) { - cs.raw.resize(new_sizes[i] * dim_); - } else if (rerank_bytes > 0) { - cs.ex.resize(new_sizes[i] * rerank_bytes); - } - batch_ptr = reinterpret_cast(cs.batch.data()); - rerank_ptr = rerank_bytes > 0 - ? (raw_reranking_ ? reinterpret_cast(cs.raw.data()) - : reinterpret_cast(cs.ex.data())) - : nullptr; - ids_ptr = cs.ids.data(); - } else { - ClusterStorage& cs = *staged[i]; - batch_ptr = reinterpret_cast(cs.batch.data()); - rerank_ptr = rerank_bytes > 0 - ? (raw_reranking_ ? reinterpret_cast(cs.raw.data()) - : reinterpret_cast(cs.ex.data())) - : nullptr; - ids_ptr = cs.ids.data(); - } + ClusterStorage& cs = staged[t] ? *staged[t] : cluster_storage_[i]; + const size_t full_batches = clusters_[i].num() / fastscan::kBatchSize; + char* tail = staged[t] ? cs.batch_data() + (full_batches * batch_bytes) + : reinterpret_cast(tails[t].data()); extend_cluster( clusters_[i], rows_of[i], data, initializer_->centroid(static_cast(i)), first_pid, - batch_ptr, - rerank_ptr, - ids_ptr, + cs.batch_data(), + rerank_bytes > 0 ? cs.ex_data() : nullptr, + cs.ids(), + tail, config ); } catch (...) { failure.capture(); } } - failure.rethrow(); + if (failure.failed()) { + // In-place clusters were only written past their old size; shrinking drops that + for (size_t t = 0; t < touched.size(); ++t) { + if (!staged[t]) { + resize_storage(cluster_storage_[touched[t]], clusters_[touched[t]].num()); + } + } + failure.rethrow(); + } - /* Commit. Untouched clusters are never staged or resized, so they are - unaffected by this. In-place clusters were already grown and written - above; only staged (reallocated) clusters still need moving in. */ - for (size_t i : touched) { - if (!in_place[i]) { - cluster_storage_[i] = std::move(*staged[i]); + /* Commit. Nothing below can throw. */ + for (size_t t = 0; t < touched.size(); ++t) { + const size_t i = touched[t]; + if (staged[t]) { + cluster_storage_[i] = std::move(*staged[t]); + } else { + const size_t full_batches = clusters_[i].num() / fastscan::kBatchSize; + std::copy_n( + tails[t].data(), + tails[t].size(), + cluster_storage_[i].batch_storage.data() + (full_batches * batch_bytes) + ); } } max_elements_ = new_num_points; @@ -517,9 +495,10 @@ void IVF::add( /** * @brief Copy one cluster into its grown storage and append new points to it. * - * Full batches, rerank data and IDs are copied unchanged. The old partial tail batch is - * unpacked and repacked together with the new points, so the result is byte-identical to - * quantizing the same points in the same order in one construct call. + * Full batches, rerank data and IDs are copied unchanged, unless they are already in place. + * The old partial tail batch is unpacked and repacked together with the new points into + * `tail_dst`, so the result is byte-identical to quantizing the same points in the same + * order in one construct call. */ void IVF::extend_cluster( const Cluster& old_cluster, @@ -530,11 +509,10 @@ void IVF::extend_cluster( char* batch_dst, char* rerank_dst, PID* ids_dst, + char* tail_dst, const quant::RabitqConfig& config ) const { - // Callers only extend touched clusters now; an untouched cluster's storage is - // never reallocated or copied at all, not even through this function. - assert(!rows.empty()); + assert(!rows.empty()); // add() only extends clusters that receive new points const size_t old_num = old_cluster.num(); const size_t batch_bytes = BatchDataMap::data_bytes(padded_dim_); const size_t rerank_bytes = rerank_vector_bytes(); @@ -544,9 +522,15 @@ void IVF::extend_cluster( const size_t kept = old_num - (full_batches * fastscan::kBatchSize); const size_t tail_num = kept + rows.size(); - std::copy_n(old_cluster.batch_data(), full_batches * batch_bytes, batch_dst); - std::copy_n(old_cluster.ex_data(), old_num * rerank_bytes, rerank_dst); - std::copy_n(old_cluster.ids(), old_num, ids_dst); + if (batch_dst != old_cluster.batch_data()) { + std::copy_n(old_cluster.batch_data(), full_batches * batch_bytes, batch_dst); + } + if (rerank_dst != old_cluster.ex_data()) { + std::copy_n(old_cluster.ex_data(), old_num * rerank_bytes, rerank_dst); + } + if (ids_dst != old_cluster.ids()) { + std::copy_n(old_cluster.ids(), old_num, ids_dst); + } /* Codes and factors of every point in the tail, old points first */ std::vector codes(tail_num * cols); @@ -596,7 +580,7 @@ void IVF::extend_cluster( } /* Repack the tail into batches */ - char* batch = batch_dst + (full_batches * batch_bytes); + char* batch = tail_dst; for (size_t start = 0; start < tail_num; start += fastscan::kBatchSize) { const size_t count = std::min(fastscan::kBatchSize, tail_num - start); BatchDataMap cur_batch(batch, padded_dim_); @@ -612,6 +596,46 @@ void IVF::extend_cluster( } } +void IVF::reserve(PID cluster_id, size_t num_points) { + if (!ready_ || clusters_.size() != num_clusters_ || + cluster_storage_.size() != num_clusters_) { + throw std::logic_error("IVF index must be constructed or loaded before reserve"); + } + if (cluster_id >= num_clusters_) { + throw std::invalid_argument("Cluster ID is out of range"); + } + if (num_points > buffer::kSearchBufferMaxPointCount) { + throw std::invalid_argument("IVF point count exceeds the supported ID range"); + } + if (exceeds_storage(num_points)) { + throw std::invalid_argument("IVF configuration exceeds addressable storage"); + } + ClusterStorage& cs = cluster_storage_[cluster_id]; + if (has_capacity(cs, num_points)) { + return; + } + + const Cluster& old_cluster = clusters_[cluster_id]; + const size_t num = old_cluster.num(); + ClusterStorage grown(raw_reranking_); + reserve_storage(grown, num_points); + resize_storage(grown, num); + std::copy_n(old_cluster.batch_data(), batch_data_bytes(num), grown.batch_data()); + if (rerank_vector_bytes() > 0) { + std::copy_n(old_cluster.ex_data(), ex_data_bytes(num), grown.ex_data()); + } + std::copy_n(old_cluster.ids(), num, grown.ids()); + std::vector cluster_sizes(num_clusters_); + for (size_t i = 0; i < num_clusters_; ++i) { + cluster_sizes[i] = clusters_[i].num(); + } + + /* Commit. Rebuilding the cluster list reuses its capacity, so it cannot throw. */ + cs = std::move(grown); + clusters_.clear(); + init_clusters(cluster_sizes); +} + size_t IVF::remove(const PID* ids_to_remove, size_t n) { if (!ready_ || initializer_ == nullptr || rotator_ == nullptr || clusters_.size() != num_clusters_ || cluster_storage_.size() != num_clusters_) { @@ -706,36 +730,26 @@ void IVF::save(const char* filename) const { /* Save rotator */ this->rotator_->save(output); - /* Save data. The reader expects three contiguous regions -- every cluster's batch - data, then every cluster's rerank data, then every cluster's IDs -- in cluster - order, exactly as one combined buffer laid out by cluster used to produce; each - region is now written from that cluster's own storage instead of a shared one, - but the bytes landing on disk are unchanged. */ + /* Save data: all batch data, then all ex data, then all IDs, each in cluster order */ this->initializer_->save(output, filename); for (size_t i = 0; i < num_clusters_; ++i) { - const ClusterStorage& cs = cluster_storage_[i]; output.write( - reinterpret_cast(cs.batch.data()), - static_cast(cluster_batch_bytes(cluster_sizes[i])) + cluster_storage_[i].batch_data(), + static_cast(batch_data_bytes(cluster_sizes[i])) ); } - const size_t rerank_bytes = rerank_vector_bytes(); - if (rerank_bytes != 0) { + if (rerank_vector_bytes() != 0) { for (size_t i = 0; i < num_clusters_; ++i) { - const ClusterStorage& cs = cluster_storage_[i]; - const char* rerank_ptr = raw_reranking_ - ? reinterpret_cast(cs.raw.data()) - : reinterpret_cast(cs.ex.data()); output.write( - rerank_ptr, static_cast(cluster_sizes[i] * rerank_bytes) + cluster_storage_[i].ex_data(), + static_cast(ex_data_bytes(cluster_sizes[i])) ); } } for (size_t i = 0; i < num_clusters_; ++i) { - const ClusterStorage& cs = cluster_storage_[i]; output.write( - reinterpret_cast(cs.ids.data()), - static_cast(cluster_sizes[i] * sizeof(PID)) + reinterpret_cast(cluster_storage_[i].ids()), + static_cast(ids_bytes(cluster_sizes[i])) ); } @@ -882,38 +896,31 @@ void IVF::load(const char* filename) { /* Load rotator */ loaded.rotator_->load(input); - /* Load data. Three contiguous regions on disk -- every cluster's batch data, then - every cluster's rerank data, then every cluster's IDs -- read into each - cluster's own storage instead of one shared buffer; see save()'s matching - comment. */ + /* Load data: all batch data, then all ex data, then all IDs, each in cluster order */ loaded.allocate_memory(cluster_sizes); loaded.initializer_->load(input, filename); for (size_t i = 0; i < loaded.num_clusters_; ++i) { - ClusterStorage& cs = loaded.cluster_storage_[i]; input.read( - reinterpret_cast(cs.batch.data()), - static_cast(loaded.cluster_batch_bytes(cluster_sizes[i])) + loaded.cluster_storage_[i].batch_data(), + static_cast(loaded.batch_data_bytes(cluster_sizes[i])) ); } - const size_t rerank_bytes = loaded.rerank_vector_bytes(); - if (rerank_bytes != 0) { + if (loaded.rerank_vector_bytes() != 0) { for (size_t i = 0; i < loaded.num_clusters_; ++i) { - ClusterStorage& cs = loaded.cluster_storage_[i]; - char* rerank_ptr = loaded.raw_reranking_ - ? reinterpret_cast(cs.raw.data()) - : reinterpret_cast(cs.ex.data()); input.read( - rerank_ptr, static_cast(cluster_sizes[i] * rerank_bytes) + loaded.cluster_storage_[i].ex_data(), + static_cast(loaded.ex_data_bytes(cluster_sizes[i])) ); } } for (size_t i = 0; i < loaded.num_clusters_; ++i) { - ClusterStorage& cs = loaded.cluster_storage_[i]; input.read( - reinterpret_cast(cs.ids.data()), - static_cast(cluster_sizes[i] * sizeof(PID)) + reinterpret_cast(loaded.cluster_storage_[i].ids()), + static_cast(ids_bytes(cluster_sizes[i])) ); - for (PID id : cs.ids) { + } + 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) { throw std::runtime_error("Invalid point ID in IVF index file"); } diff --git a/tests/unit/rabitqlib/index/ivf_test.cpp b/tests/unit/rabitqlib/index/ivf_test.cpp index 25a252fa..da258836 100644 --- a/tests/unit/rabitqlib/index/ivf_test.cpp +++ b/tests/unit/rabitqlib/index/ivf_test.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -29,10 +30,19 @@ namespace rabitqlib::ivf { -// Must live directly in rabitqlib::ivf, not in the anonymous namespace below: -// the `friend struct IvfTestAccess;` in ivf.hpp looks it up from there. +// Outside the anonymous namespace so it matches the friend declaration in ivf.hpp. struct IvfTestAccess { static Cluster cluster_view(const IVF& index, size_t i) { return index.clusters_[i]; } + + static bool has_spare_capacity(const IVF& index, size_t i) { + const auto& cs = index.cluster_storage_[i]; + return cs.batch_storage.capacity() > cs.batch_storage.size() || + cs.ex_storage.capacity() > cs.ex_storage.size() || + cs.raw_storage.capacity() > cs.raw_storage.size() || + cs.id_storage.capacity() > cs.id_storage.size(); + } + + static std::unique_ptr>& rotator(IVF& index) { return index.rotator_; } }; namespace { @@ -642,6 +652,30 @@ TEST(IvfAddTest, EveryWayOfAddingTheSamePointsGivesByteIdenticalIndexes) { ASSERT_EQ(done, kNew); EXPECT_TRUE(saved_bytes(chunked, path) == expected) << "chunked"; + // Even clusters are reserved and grow in place; odd ones are rebuilt each time. + IVF reserved; + reserved.load(base_path.c_str()); + std::vector final_sizes(kDynClusters, 0); + for (PID cluster : assigned) { + ++final_sizes[cluster]; + } + for (size_t i = 0; i < kDynClusters; i += 2) { + reserved.reserve(static_cast(i), final_sizes[i]); + } + const Cluster reserved_view = IvfTestAccess::cluster_view(reserved, 0); + done = 0; + for (size_t chunk : std::array{1, 31, 32, 7, 1, kNew - 72}) { + reserved.add( + extra + (done * kDynDim), chunk, assigned.data() + kOld + done, false, 1 + ); + done += chunk; + } + EXPECT_TRUE(saved_bytes(reserved, path) == expected) << "reserved"; + EXPECT_EQ( + IvfTestAccess::cluster_view(reserved, 0).batch_data(), + reserved_view.batch_data() + ); + IVF threaded; threaded.load(base_path.c_str()); threaded.add(extra, kNew, nullptr, false, 4); @@ -653,8 +687,7 @@ TEST(IvfAddTest, EveryWayOfAddingTheSamePointsGivesByteIdenticalIndexes) { } TEST(IvfAddTest, AddingToOneClusterLeavesOtherClustersUntouched) { - // Cluster sizes 0, 1, 32, 97: an initially-empty cluster, one with a single - // point, one that exactly fills a FastScan batch, and one with a partial tail. + // Cluster sizes 0, 1, 32, 97 cover empty, single, full-batch and tail clusters. constexpr size_t kOld = 130; const auto points = random_rows(kOld, 741); const auto centroids = random_rows(kDynClusters, 742); @@ -682,12 +715,8 @@ TEST(IvfAddTest, AddingToOneClusterLeavesOtherClustersUntouched) { const Cluster after = IvfTestAccess::cluster_view(index, i); if (i == 0) { EXPECT_EQ(after.num(), before[i].num() + kNew); - EXPECT_NE(after.batch_data(), before[i].batch_data()); - EXPECT_NE(after.ex_data(), before[i].ex_data()); - EXPECT_NE(after.ids(), before[i].ids()); } else { - // No reallocation and no copy: the untouched cluster's storage, not - // just its contents, is exactly what it was before add() ran. + // Untouched clusters keep their storage: no reallocation and no copy. EXPECT_EQ(after.num(), before[i].num()); EXPECT_EQ(after.batch_data(), before[i].batch_data()); EXPECT_EQ(after.ex_data(), before[i].ex_data()); @@ -696,35 +725,183 @@ TEST(IvfAddTest, AddingToOneClusterLeavesOtherClustersUntouched) { } } -TEST(IvfAddTest, SecondSmallAddToTheSameClusterReusesCapacityInPlace) { - // The first add to a freshly-constructed cluster must reallocate (capacity - // starts exact-fit), but it over-allocates for future growth; a second small - // add to the same cluster should then need no allocation at all. +TEST(IvfAddTest, UnreservedClustersGrowToExactlyTheirNewSize) { constexpr size_t kOld = 40; - const auto points = random_rows(kOld + 2, 851); + const auto points = random_rows(kOld + 1, 851); const auto centroids = random_rows(kDynClusters, 852); const std::vector assigned(kOld, 0); - IVF index(kOld, kDynDim, kDynClusters, 4, METRIC_L2); - index.construct(points.data(), centroids.data(), assigned.data(), false, 1); + for (size_t bits : {1UL, 4UL, 32UL}) { + SCOPED_TRACE(::testing::Message() << "bits=" << bits); + IVF index(kOld, kDynDim, kDynClusters, bits, METRIC_L2); + index.construct(points.data(), centroids.data(), assigned.data(), false, 1); + const PID route_to_zero = 0; + index.add(points.data() + (kOld * kDynDim), 1, &route_to_zero, false, 1); + EXPECT_EQ(IvfTestAccess::cluster_view(index, 0).num(), kOld + 1); + EXPECT_FALSE(IvfTestAccess::has_spare_capacity(index, 0)); + } +} - const Cluster before_first = IvfTestAccess::cluster_view(index, 0); +TEST(IvfReserveTest, ReservedClusterGrowsInPlaceUntilItsCapacityIsUsed) { + constexpr size_t kOld = 40; + constexpr size_t kReserved = kOld + 2; + const auto points = random_rows(kReserved + 1, 861); + const auto centroids = random_rows(kDynClusters, 862); + const std::vector assigned(kOld, 0); const PID route_to_zero = 0; - index.add(points.data() + (kOld * kDynDim), 1, &route_to_zero, false, 1); - const Cluster after_first = IvfTestAccess::cluster_view(index, 0); - EXPECT_NE(after_first.batch_data(), before_first.batch_data()); - EXPECT_EQ(after_first.num(), kOld + 1); - - index.add(points.data() + ((kOld + 1) * kDynDim), 1, &route_to_zero, false, 1); - const Cluster after_second = IvfTestAccess::cluster_view(index, 0); - EXPECT_EQ(after_second.num(), kOld + 2); - EXPECT_EQ(after_second.batch_data(), after_first.batch_data()); - EXPECT_EQ(after_second.ex_data(), after_first.ex_data()); - EXPECT_EQ(after_second.ids(), after_first.ids()); - // Byte-identical correctness of repeated small adds (not just this pointer - // behavior) is already covered by - // IvfAddTest.EveryWayOfAddingTheSamePointsGivesByteIdenticalIndexes's - // "chunked" case, which exercises this same in-place-reuse path. + + for (size_t bits : {1UL, 4UL, 32UL}) { + SCOPED_TRACE(::testing::Message() << "bits=" << bits); + IVF index(kOld, kDynDim, kDynClusters, bits, METRIC_L2); + index.construct(points.data(), centroids.data(), assigned.data(), false, 1); + index.reserve(0, kReserved); + const Cluster reserved = IvfTestAccess::cluster_view(index, 0); + EXPECT_EQ(reserved.num(), kOld); + + for (size_t row = kOld; row < kReserved; ++row) { + index.add(points.data() + (row * kDynDim), 1, &route_to_zero, false, 1); + const Cluster grown = IvfTestAccess::cluster_view(index, 0); + EXPECT_EQ(grown.num(), row + 1); + EXPECT_EQ(grown.batch_data(), reserved.batch_data()); + EXPECT_EQ(grown.ex_data(), reserved.ex_data()); + EXPECT_EQ(grown.ids(), reserved.ids()); + } + + // The old storage is alive while its replacement is allocated, so they differ. + index.add(points.data() + (kReserved * kDynDim), 1, &route_to_zero, false, 1); + EXPECT_NE( + IvfTestAccess::cluster_view(index, 0).batch_data(), reserved.batch_data() + ); + EXPECT_FALSE(IvfTestAccess::has_spare_capacity(index, 0)); + } +} + +TEST(IvfReserveTest, RejectsBadInputAndLeavesSavedBytesUnchanged) { + constexpr size_t kOld = 40; + const auto points = random_rows(kOld, 871); + const auto centroids = random_rows(kDynClusters, 872); + const std::vector assigned(kOld, 0); + const std::string path = ::testing::TempDir() + "rabitq_ivf_reserve.index"; + const auto expect_message = [](const auto& call, const char* message) { + try { + call(); + ADD_FAILURE() << "expected: " << message; + } catch (const std::exception& error) { EXPECT_STREQ(error.what(), message); } + }; + + IVF unbuilt(kOld, kDynDim, kDynClusters, 4); + EXPECT_THROW(unbuilt.reserve(0, 1), std::logic_error); + expect_message( + [&] { unbuilt.reserve(0, 1); }, + "IVF index must be constructed or loaded before reserve" + ); + + IVF index(kOld, kDynDim, kDynClusters, 4); + index.construct(points.data(), centroids.data(), assigned.data(), false, 1); + const std::string expected = saved_bytes(index, path); + const Cluster constructed = IvfTestAccess::cluster_view(index, 0); + + EXPECT_THROW(index.reserve(static_cast(kDynClusters), 1), std::invalid_argument); + expect_message( + [&] { index.reserve(static_cast(kDynClusters), 1); }, + "Cluster ID is out of range" + ); + EXPECT_THROW( + index.reserve(0, buffer::kSearchBufferMaxPointCount + 1), std::invalid_argument + ); + expect_message( + [&] { index.reserve(0, buffer::kSearchBufferMaxPointCount + 1); }, + "IVF point count exceeds the supported ID range" + ); + + // Asking for less than the cluster already holds does nothing. + index.reserve(0, kOld / 2); + EXPECT_EQ(IvfTestAccess::cluster_view(index, 0).batch_data(), constructed.batch_data()); + EXPECT_FALSE(IvfTestAccess::has_spare_capacity(index, 0)); + + // Spare capacity changes neither the file nor what a loaded index holds. + index.reserve(0, kOld * 2); + index.reserve(1, kOld); + EXPECT_TRUE(IvfTestAccess::has_spare_capacity(index, 0)); + EXPECT_TRUE(saved_bytes(index, path) == expected); + IVF loaded; + loaded.load(path.c_str()); + for (size_t i = 0; i < kDynClusters; ++i) { + EXPECT_FALSE(IvfTestAccess::has_spare_capacity(loaded, i)); + } + std::remove(path.c_str()); +} + +// Delegates to a real rotator, but the call numbered `fail_at` throws. +class FailingRotator : public Rotator { + public: + FailingRotator(std::unique_ptr> inner, size_t fail_at) + : Rotator(0, inner->size()), inner_(std::move(inner)), fail_at_(fail_at) {} + + void rotate(const float* src, float* dst) const override { + if (calls_++ == fail_at_) { + throw std::runtime_error("injected rotation failure"); + } + inner_->rotate(src, dst); + } + void load(std::ifstream& input) override { inner_->load(input); } + void save(std::ofstream& output) const override { inner_->save(output); } + void load(const char* data) override { inner_->load(data); } + void save(char* data) const override { inner_->save(data); } + [[nodiscard]] size_t dump_bytes() const override { return inner_->dump_bytes(); } + + std::unique_ptr> release() { return std::move(inner_); } + + private: + std::unique_ptr> inner_; + size_t fail_at_; + mutable size_t calls_ = 0; +}; + +TEST(IvfAddTest, FailedAddLeavesClustersGrowingInPlaceUnchanged) { + // Cluster 0 has a partial tail batch and grows in place; cluster 1 fails afterwards. + constexpr size_t kOld = 70; + constexpr size_t kInPlace = 3; + const auto points = random_rows(kOld + 6, 881); + const auto centroids = random_rows(kDynClusters, 882); + std::vector assigned(kOld, 1); + std::fill(assigned.begin(), assigned.begin() + 40, PID{0}); + const std::array new_clusters{0, 0, 0, 1, 1, 1}; + const float* extra = points.data() + (kOld * kDynDim); + const std::string base_path = ::testing::TempDir() + "rabitq_ivf_add_fail_base.index"; + const std::string path = ::testing::TempDir() + "rabitq_ivf_add_fail.index"; + + for (size_t bits : {1UL, 4UL, 32UL}) { + SCOPED_TRACE(::testing::Message() << "bits=" << bits); + IVF index(kOld, kDynDim, kDynClusters, bits, METRIC_L2); + index.construct(points.data(), centroids.data(), assigned.data(), false, 1); + index.save(base_path.c_str()); + index.reserve(0, 64); + const std::string expected = saved_bytes(index, path); + const Cluster before = IvfTestAccess::cluster_view(index, 0); + + auto& rotator = IvfTestAccess::rotator(index); + rotator = std::make_unique(std::move(rotator), kInPlace); + EXPECT_THROW( + index.add(extra, new_clusters.size(), new_clusters.data(), false, 1), + std::runtime_error + ); + rotator = static_cast(*rotator).release(); + + EXPECT_EQ(index.max_elements(), kOld); + EXPECT_EQ(IvfTestAccess::cluster_view(index, 0).num(), before.num()); + EXPECT_EQ(IvfTestAccess::cluster_view(index, 0).batch_data(), before.batch_data()); + EXPECT_TRUE(saved_bytes(index, path) == expected); + + // Retrying matches an index that never saw the failure. + index.add(extra, new_clusters.size(), new_clusters.data(), false, 1); + IVF reference; + reference.load(base_path.c_str()); + reference.add(extra, new_clusters.size(), new_clusters.data(), false, 1); + EXPECT_TRUE(saved_bytes(index, path) == saved_bytes(reference, path)); + } + std::remove(base_path.c_str()); + std::remove(path.c_str()); } TEST(IvfConstructionTest, RebuildMatchesIncrementalEncodingWithSkewedClustersAndTails) { From 0b4116af8c39ae97457d799662f8412440036d58 Mon Sep 17 00:00:00 2001 From: Tamim Ehsan Date: Sun, 18 Oct 2026 21:22:56 +0800 Subject: [PATCH 08/10] feat(python): expose IvfIndex.reserve --- python_bindings/ivf_bindings.cpp | 28 ++++++++++++++++++++++++- tests/python/test_ivf.py | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/python_bindings/ivf_bindings.cpp b/python_bindings/ivf_bindings.cpp index 2ee059d0..a2eef5f2 100644 --- a/python_bindings/ivf_bindings.cpp +++ b/python_bindings/ivf_bindings.cpp @@ -164,6 +164,23 @@ class IvfIndex { return ids; } + void reserve(int64_t cluster_id, int64_t num_points) { + const auto operation = access_.write(); + if (!built_) { + throw std::runtime_error("IvfIndex must be built or loaded before reserve"); + } + if (cluster_id < 0 || static_cast(cluster_id) >= index_->num_clusters()) { + throw std::invalid_argument("cluster_id must be in [0, num_clusters)"); + } + if (num_points < 0) { + throw std::invalid_argument("num_points must be non-negative"); + } + py::gil_scoped_release release; + index_->reserve( + static_cast(cluster_id), static_cast(num_points) + ); + } + size_t remove(py::handle ids) { const auto operation = access_.write(); auto ids_array = ensure_1d_integer_array(ids, "ids"); @@ -347,7 +364,16 @@ void register_ivf(py::module_& m) { "Append vectors without the original data and return their ids. Vectors " "are quantized against the existing centroids, which do not move. Without " "cluster_ids each vector goes to its nearest centroid. Each call copies " - "the whole index, so add many vectors per call rather than one at a time." + "every cluster it adds to, unless reserve left room for the new vectors, " + "so add many vectors per call rather than one at a time." + ) + .def( + "reserve", + &IvfIndex::reserve, + py::arg("cluster_id"), + py::arg("num_points"), + "Preallocate room for a cluster to hold num_points vectors, so later adds " + "to it do not copy it. Reserved room is not saved." ) .def( "remove", diff --git a/tests/python/test_ivf.py b/tests/python/test_ivf.py index 163768c2..8261ba12 100644 --- a/tests/python/test_ivf.py +++ b/tests/python/test_ivf.py @@ -602,6 +602,41 @@ def test_add_chunks_match_a_single_add(tmp_path): assert a.read_bytes() == b.read_bytes() +@pytest.mark.parametrize("nbits", [1, 4, 32]) +def test_reserved_adds_match_a_single_add(tmp_path, nbits): + idx, old, new, assigned = _dynamic_index(nbits=nbits) + base = tmp_path / "base.index" + idx.save(str(base)) + whole = IvfIndex.load(str(base)) + whole.add(new, assigned) + parts = IvfIndex.load(str(base)) + for cluster in range(parts.num_clusters): + parts.reserve(cluster, len(old) + len(new)) + for chunk in np.array_split(np.arange(len(new)), [1, 33, 34, 90]): + parts.add(new[chunk], assigned[chunk]) + a, b = tmp_path / "a.index", tmp_path / "b.index" + whole.save(str(a)) + parts.save(str(b)) + assert a.read_bytes() == b.read_bytes() + + +def test_reserve_rejects_invalid_arguments(): + idx, old, _, _ = _dynamic_index() + for bad in (-1, 3, 2**32): + with pytest.raises(ValueError, match="cluster_id"): + idx.reserve(bad, 10) + with pytest.raises(ValueError, match="num_points"): + idx.reserve(0, -1) + with pytest.raises(ValueError, match="supported ID range"): + idx.reserve(0, 2**40) + idx.reserve(0, 0) + assert idx.max_elements == len(old) + + unbuilt = IvfIndex(65, 4, 1, 4) + with pytest.raises(RuntimeError, match="built or loaded"): + unbuilt.reserve(0, 1) + + def test_add_accepts_fast_quantization_and_other_dtypes(): idx, _, new, _ = _dynamic_index() idx.add(new[:10].astype(np.float64), fast_quantization=True) From 12690aaa6aaf5deacfa5c48cc6e8d227aa40d8da Mon Sep 17 00:00:00 2001 From: Tamim Ehsan Date: Sun, 18 Oct 2026 21:22:56 +0800 Subject: [PATCH 09/10] docs(ivf): describe per-cluster add cost and reserve --- docs/docs/index/ivf.md | 25 +++++++++++++++++++------ docs/docs/index/usage.md | 2 +- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/docs/docs/index/ivf.md b/docs/docs/index/ivf.md index 2c1a614b..a545f286 100644 --- a/docs/docs/index/ivf.md +++ b/docs/docs/index/ivf.md @@ -278,6 +278,8 @@ void IVF::add( ); size_t IVF::remove(const PID* ids_to_remove, size_t n); + +void IVF::reserve(PID cluster_id, size_t num_points); ``` - **data**, **n**: `n` new vectors, quantized like `construct` does. Vector `i` @@ -290,6 +292,9 @@ size_t IVF::remove(const PID* ids_to_remove, size_t n); - **ids_to_remove**: PIDs to remove. Every PID must be below `num_stored_points()`; nothing is removed if one is not. `remove` returns how many were newly removed and can be repeated safely. +- **cluster_id**, **num_points**: `reserve` preallocates room for cluster `cluster_id` + to hold `num_points` vectors in total, counting the ones it already holds. It does + nothing if the cluster already has that room. See [Cost of `add`](#cost-of-add). In Python: @@ -298,6 +303,7 @@ new_ids = index.add(vectors, num_threads=1, fast_quantization=False) # To choose clusters explicitly, use this instead of the call above: # new_ids = index.add(vectors, cluster_ids=labels) removed = index.remove(new_ids[:10]) +index.reserve(cluster_id, num_points) ``` Python `vectors` has shape `(n, index.dim)` and is converted to contiguous float32. @@ -314,14 +320,21 @@ access; see the ### Cost of `add` -`add` builds the grown index next to the old one and swaps it in, so the index is -unchanged if it throws. Every call copies the whole storage, so: +Each cluster has its own storage, and `add` only touches the clusters that receive +new vectors. Each of those is rebuilt next to its old storage and swapped in, so the +index is unchanged if `add` throws. As a result: + +- A call needs extra memory for the clusters it touches while it runs. +- Each call costs time proportional to the sizes of the touched clusters, on top of a + cost per new vector. Routed vectors usually touch most clusters, so a call can + still copy most of the index. **Add many vectors per call rather than one at a + time.** -- It needs memory for both copies while it runs. -- Each call has a fixed cost that grows with the size of the whole index, on top of a - cost per new vector. **Add many vectors per call rather than one at a time.** +A rebuilt cluster gets exactly the room it needs. To grow a cluster without +copying it, call `reserve` first: later `add` calls then write into the reserved +room in place until the cluster outgrows it. The reserved room is memory the index +holds but does not use yet. It is not saved, so a loaded index has none. -There is no spare capacity: every `add` moves the data. Measure update latency and peak memory with your index size, bit width and batch size; serialized file size alone does not determine peak process memory. diff --git a/docs/docs/index/usage.md b/docs/docs/index/usage.md index c9062254..c334ef64 100644 --- a/docs/docs/index/usage.md +++ b/docs/docs/index/usage.md @@ -102,7 +102,7 @@ construction, updates, and file I/O. The following contract applies to | Operation on the same index | Concurrent access | | --- | --- | | `search`, `search_batch`, `save`, and property reads | May run together | -| `build`, `add`, `remove`, and `resize`, where available | Require exclusive access | +| `build`, `add`, `remove`, `reserve`, and `resize`, where available | Require exclusive access | A conflicting call immediately raises `RuntimeError("Index is busy: conflicting operation in progress")`. From 6712f462aa1a67dc99112a82a51a85da944b132a Mon Sep 17 00:00:00 2001 From: gouyt13clear Date: Mon, 19 Oct 2026 15:39:30 +0800 Subject: [PATCH 10/10] test(ivf): fix persistence test collisions and trim fixtures Include the fixture directory in temporary filenames to prevent collisions during parallel runs. Keep only the new quantized and raw L2 fixtures, preserving all existing v0.5.2 compatibility cases. --- .../pre-ivf-per-cluster-storage/README.md | 4 +- .../pre-ivf-per-cluster-storage/generate.cpp | 46 +++++++++--------- .../ivf32_ip.expected | 17 ------- .../ivf32_ip.index | Bin 40606 -> 0 bytes .../ivf4_ip.expected | 17 ------- .../pre-ivf-per-cluster-storage/ivf4_ip.index | Bin 12526 -> 0 bytes .../pre-ivf-per-cluster-storage/manifest.json | 4 -- .../index/historical_persistence_test.cpp | 8 ++- 8 files changed, 27 insertions(+), 69 deletions(-) delete mode 100644 tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_ip.expected delete mode 100644 tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_ip.index delete mode 100644 tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf4_ip.expected delete mode 100644 tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf4_ip.index 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 3a544c29..b270766b 100644 --- a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/README.md +++ b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/README.md @@ -6,8 +6,8 @@ cluster in one shared buffer, 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. -The four cases cover four-bit and raw-vector (`nbits=32`) IVF for L2 and inner -product. Each index holds 130 points in 65 original dimensions (96 padded) across +The two cases cover four-bit and raw-vector (`nbits=32`) IVF for L2. +Each index holds 130 points in 65 original dimensions (96 padded) across four clusters of 0, 1, 32 and 97 points, so the file has an empty cluster, a 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 diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/generate.cpp b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/generate.cpp index 1b82de02..e27357c9 100644 --- a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/generate.cpp +++ b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/generate.cpp @@ -40,30 +40,28 @@ int main(int argc, char** argv) { clusters[0] = 1; for (size_t i = 1; i < 33; ++i) clusters[i] = 2; - for (auto metric : {METRIC_L2, METRIC_IP}) { - for (const std::string kind : {"ivf4", "ivf32"}) { - const std::string name = kind + (metric == METRIC_L2 ? "_l2" : "_ip"); - const auto path = (directory / (name + ".index")).string(); - std::vector ids(kQueries * kTopK); - std::vector distances(ids.size()); - ivf::IVF index(kCount, kDim, kClusters, kind == "ivf32" ? 32 : 4, metric); - 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(); - } + for (const std::string kind : {"ivf4", "ivf32"}) { + const std::string name = kind + "_l2"; + const auto path = (directory / (name + ".index")).string(); + std::vector ids(kQueries * kTopK); + std::vector distances(ids.size()); + ivf::IVF index(kCount, kDim, kClusters, kind == "ivf32" ? 32 : 4, METRIC_L2); + 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/pre-ivf-per-cluster-storage/ivf32_ip.expected b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_ip.expected deleted file mode 100644 index 750cfd52..00000000 --- a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_ip.expected +++ /dev/null @@ -1,17 +0,0 @@ -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/pre-ivf-per-cluster-storage/ivf32_ip.index b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_ip.index deleted file mode 100644 index 185e737d127da41da6be8b3003b5b34ca35adf09..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40606 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

VrWmnUozldz`Uj- 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 R*TUbxb#OiWEtK#D_V*huc z{Zq1#<%NIh)PG9$e@eE0N|wJn{h#IE3gw@+>px}WH2V~Gj+_EZmOqR4k#DhC@EX+>_#989r*ES>K{au|^ z+4(3s@+!hA7qyuu3viUK3n_Q1apD6(=Y_feFm^z!`hgbOVun-fK0#vaiH?Pt1R$Od z>&T4~fLYjtPwE{8*wO2clI`sgA$CcJ>2w=1#lqmEEk7c1E9lJne4P1G5rNt)9CPlY z=WLWm*jzdtvu>@&xE|4oF$_nr5~7uNA($$G#V@V`Ab$@c%Lh2&!2*e&JMrW$E5K|n zg1mnI2uz=X$-O*?`1yqnk`LjO$T%FNh~XfSf-`yZa7xJsNZ8Kl5LF#IIfAJ;(6z;R zH?jl38iF%L262M%QHL06qSLy`^RYjx0}TCfT6!Bq_aq_mx5k+sO*nCFT!-LJr{}0H z;ozqtj=mg3*yA9Gea@f*&Vzv9xQv5Ret@yL;Amn!0#zJ<(EkWA6JDI~t%R797#%wT zb+CQLkYJ%h@clbPdv8Nf(E*u-vIuN_4~dp_GS7*+EVfCqSKDIClGrHZigvC$_LbqE`k|#H9fddb$B48#0whD?-5VUE3_ikIqv)8h zDvq5aukqDrh_OkLeTL(h0-H7=pMs-za)4+ZhS(Z2$dpLNE#w3caM^`pzD7F0Tno=B zn8cYg_wna38i=sHslyz*fD^(AbQNj|nf#1a(23j+bb@t>UcAN>FiWq(=dq@MS7|>$24~@4D6q23+Bu1pp^S zaUwjMURsrjvqW=G=&O_!^GYi_j6~4kvxtP|gNAPASj9X$=E7@#Z3(S91kYc(?=P zJa~oExSztDnrgiG?heEhEy95?7qlxX0?6+KUd+dam|8C&dJqgLb$@W+Aq3IDRvgW3 z#ue;$0jx5%KId2oPE*W<76;h@b14$f*>wdH;#LrpEaK?WL!8jFB*(yGI%O>l5lLqt z<{+YjoMs@=DubiYES#dwhA6*`5GI&fk8OKXk41IRY1CT~t9_(Rvo6Ld#1JIr8FXe< z22PtGziU$KbeIt@A*fTtLDx2zGuscbyJ6axiUNHyNgdI=FGK8^rw(x}i;m35bB)`F zV}19uC^}itqFo2%j6TPaus5WAO@leUe<02GK2EfB>ktxm>DZi@4o&GA#5TOqL0j1n zac3CE+GXj)D>@|d$>Rik(#DL-w6XINbVB19#17=qi3M^j8IkvWvJAjF^61EEGg|Y{ zbK~D))xVX0%H`Xax4DgDJA z?-Td`wVmVN4gZw?KE?lCz5M+DUH@YyD%-(SLu`|2^W5Bfj8& zTmOCS2M4>CjlMo^4GdrT9y;VJo{}sgWMAIJ$0pLnHoq=_RsB{v-+Z?q2>Z-_A=wATMBUn*D_e^$S+=Udi|yrij#ApK)=KU}}? z3GF^VWo#$U%9D%9N5}FU*?M0tch#1HmG+iBLc*3-LPEP%^mJKS=3|OgT$~gNFW*L9 zz6hz3y3D%#>h!q#_iHk;((>Y>Z5KaGoVxL~$8};TYIeN$R{GziCbKR*{Zc(y%N&YM zFNls#B+{ZY;?olHVvQ0Hyt#c!7cJ`3zkGTBAV5`B`G;hcz?3Gw6u470XsJCuIw6|- z!N*n6{dsB4@$FYKO2lKT(?vChlIv`)XDKo*E>Q{5wGEVMi3U)pKqam%G6?k$D#b&v zffkd&Am%Kn#Kl>_tSn=Amtwl;q&36)=@8YT>awo)*a(#wbe?MQCWi_C zW?hAfL@G)e1E*0(W+yUE%MMqG1c##CU~PnDwBu%55n5*f2^| zdx;Wx2auxtEMhb2LJuWJkzZ>&vM7Cn%B%ZOr1}&J;(3ig=WA3``4px7>_ZY)+t8!1 z$7mjOqW5ckz&TS-@HXu*sPLnLgZEE^(lrslfQSG^Y%$<+pB`wQ3K=sL4u<7A>5OFsIIPQuAL(u`C_;3KIFg*u_&W9d&zE4d^Y!>gX2)@{(E;`_ta_J@=6K5NUTHVVp0K32Xy$^skRls0ZuR*ZFL zE4ja6OKd^Q{o3Nz;=BgwTdXn|rG)JbB~MvHfr}hM>4Tgx(;_LKGRc9Jokqk&N9V>S zXE^58r1Fimhp?q~f!ev6AUEQkB&oLfY)?#LJ1Ei2H7yDy<5{b)-W-ap@;Z6BObI9~(rFWIRM z9ytMnPE^i9QJ%PYGVXQ6 zYwOR-_w8Rld@Wjx_plNZn4KyS?)>A?7OJjIJ<#K1lG5H7Z|&jy#H`WD=zPyW`} z39&>WXxG@wVAe%abEGdLY=0|ao>peiT8*eQdr1bfIE!JofkRheeKQr4(QEj%vV@A6 z1v5a@HU{s5C@S+YE4BJ!2DLbCD|Pb3ItF%`pUNzaVqoDIBj@cGK>HrT__()=k=7MQ zwb)`uMR!(!$%`#$0JS2nYh}nJ^f|JeZAW1hy{Jj`G19DQKpKvX2)=AY)&ebPt<*T0 z$t^_l&ni)F7yxdW)YIv+&8MX;!G0``3 z)_1lxYZr935i;w0wJ9ukl@L??wrM)Xn&FPlA;I~~)_3A3Y8lIHHRCVhaN{*~U zeWS&sW~4j`QTJ36xDfkqi+QxQdaDa}|!4uT%+l4lj=AdKm>X4L^6Ikh_0|E+;fg>>%fOGN? zcq|eIf%#-5hNDrjPfDaJ(Cq^HM}k&pfjr0PsteFIw?Cxa*N z^!49)n)~^xs0#b}xclt(_a|=zE7h{NJg$P~0!?+znp{o=joN6LxK*`mrTG+o7EEp{ zt27^fO?Cu-`hYzTIg-=14BA8}^P5r43bEGJ!-Ol+>Wj0kPHs-okr zQNyNMdqV2m!t2uT!F}m|4CVtdDRvsLkcFs7o$_8+Gv9`JY-6?nA<6}|V`JTcM#XK3UHBiofvYiw$v zO6#1nP4KV{&taRdLk7-v&-7>7UoZV`Zy$W!In>x*cXGrjG_=!Zcnj76Y;O zfPm}o8Q5++WOpHpF}abQVRx0c!F#%o@my4yYS(#(ft5a`%CV(W)gl_GSWP|yOQYi$ zZ2<9d2r;nsegsq=GMK~MR7`=4g$>F7bdIv=3eK$8b&)DZx7#11vHB_$ajy=A_K%=o zqdLU4vIrI2>_BXX$?x*}Er>^tf#e>vA!hL%l(yy>ihcYN9Z4)jFK<6URl?1PaP31^ zBubD}_85BR@DyE$YeBElJJILfGW3N3k+oet+IHqCs+I3VGm}k-bA*Au3KXGOO(#%5 zJq-Q?r~>}SZa~o>5*VCy0##ndfbY!-!1mh(RF`^zeRLP_L&OX0{h|-1DjmU$)oE~~ zUk|W)QGxJP51{re3}j>&ft0RP@V(vv%-^yGruApRQp_G;!|w<_KX3+0>&^n^K`-DL zdH`@(S%RO}&A{^bb9n@^Jf2$~|1IBtdAz$kK3?7**Rl?l$D_;pTfYDD{+4xzEsuwn zzkYc9AUUAH`n?E$?j+*EuF6S~q?Xdn;lc^_Z|~>1LQ6Yx?=@2NW~UPe zecCcz#UCkb8WRl6n*Sggtc;!U!6n;Vph54(>sw_nc)WP-C$#Amjg{?CU9M;;7w^== zHM*iVR^Vn@57uL57Rc_B67<2IP;94?a=>xFT%AEO43nr<(;&o`ksgS$n|gAGk{nQesNad3_E z7zC4s1vO`5)w8zj`{A%rhRvs(e>8efa;*=~+|KfQx!3KcrWc8LK_uQzz=7_^>f#4- z6nJ;8vvOS>F=}EKc-bg6hB6&0^+otvOs0W`O9>{H4D-I4K)&nYqZ;R~wbeYH*eChY z+}@>E@7VRsem*f;*<0kZWWCkF9|+nFKy2VOMTen z@1T~Ja-K-t0*qhphc8kC{k)@Z4LTp>>%L`*uG*`dRz6f1w@Od-%bo1S_3n*g6~h?z zR!51|0_N@&rESrAj5RdxQa%!uSDu=9{zwd0VT;#`l*2!NFMSQEk8#7IFX?>IafGj& zZJiE^RV#87`jzip`A2nH{zGB7d#n$gtsqvt_Je#^$=W_t-GFohoS?>b>dpSwIjdrf z%sc7p{#;BsdJi4@T zpxbm?$a}Afq?JNjwh5g}R@pFveSZh(rvWrY=MUY>URoL3GkSS;$<>_YgW`_z!UC2d z6ORqOk=5JVbn^~icdU{BtQ$1;5^GpkjIzR+1Ih#&3@NLLEELI zj{My~Fb>J*6Y$MTbtccwx{j6VtId?I8YcxwdW=U3N6Bk&c&_y1>G8R@O=KlTaR*kf zHQ>%&s{30Hs zFTuJuyjN}Z&Qi;MZ$U(_pUv??4u+2JRlOq3t-Fs9@Ft76Zc#X-pD(B0a z>!=-VwH5)GnGkBckpN!~lqU?!&$)%3Hy@tQ&^Z4|$2#oUbWLS(r$$`o$Kn@XeRuj` zYBl(gvpAgH6R6y$^flp&e?;#|N<_51`kG_yWlJe{`8KWCGB`EZy0H2bz>|=g#xXc0 zv+Y}JM^R^}C3+!QqZH(0!F|~DD|PFP*y!W@(N(Ki0)lL@-=FD*-&3KOs(R7g-<7el z1rLWby5kRRoR)HbxM8TE;F9WAvD4;ZaTRj9*m@aMRAT}Sj@vwlFT9qww&0m4Z`Ikg zbpkZEi9_SLYB$br$fLjGvSbhRAx`Z>sxDUW(DcEVg}xCFFB`S&dD!me<~ifC(Lebf zE`8zRnkS2m_s0u%cw^lTIHDN9D^Ih#+zN}XrJC8$&^?*`+^1B_W@jD>Y)DyJsULGp z&r_!N2od57Toh$tpz)X)cg1L!l*#-VnKNHT3{>3bhUlhCt94pB>7BICx$U*V7`qPc zqpl41x2V`ujfd~8^IEOsvPL=ag!7Kxodqid8sl!Yo6hV=mSdyd1GIP~v6?8jQtJA^fK4RayRfPEWVp}_d^*U& zx|;c7OTy15yU)5A#}y{O4(6hTPdC(6AOTCHzyAPy6tdm1rntm&>voB+yf@2k#XEx2qHAw&m&sX=J zXXo2FH0yd0;|fCM!B=5&jb&HvT;h#wg-3oG@Ucm^j_KyGG)usj$GVaWcX_df$FZps zTgGuYO&b_E7Z&`%;7E{tPRf>#K2Ch;`exS^c*47)UwKL7y6Fbx#Z~G4<`x=z65i*`2NzfmLtSHtiO0{v!+tpANX*Ml* z>8ge1&R!S3F72CksXZHNn%(6>v2_Z-aE}pe*vEaz1@C>@c3V&EZdb(6yzF@~>xAvD zt`ZT{*PDVzgU$T{3G*gkJZuFOl+(9e?GkETu-bFBd6njqz?6=jQpun=l|{K9EiS&I znPXvbm?6NQ#md8C`Rd42K_7|o_1|8Gib_3j2>8Npf3!sK^_S~9J6GsDWF#bsV@=QT z_MoF=KD2S2yNcAl&}0iO=la9P2c`G|3kNDx{MP9AU6uD2+nwBy zgO|kDCojBBi4AfWDy{>jylPTAQ?^7(VC%He&nYul=a9ToPkpDn+J4&A>r=k$eD!|* zl(OtAK3!WSEk9mt^WUL+0K1$D_XMj!Ub{FG=C8!(Jqi!k5wwo0EW!)1H@((2t_^8^ zE@N%4*m5O?72Css2YifyEUq7Zq;*stLLMvCYeMWSrPx!rxYI|x!-%^c0m>a~uoo+} z3HIZ_&if!#w9yI6sLF4pV}@TVR$b={kBKuaFdQzgE;CUTqNc9Bjjb*v?4$5=)v~aQ zvFBLldTVvxgQesA>oSE*=BJI1e6$lwy+1VXXH^ok@}u=nkl+TkFCKzIyFD|B&q^Uz zc@w_h2%pw{yV*uAE&l7fPL{JkG6J7C9r^1x(GUjuR)cBcs~CC2kGgQCIHm~-%>kXm%?*7b=v4EM>D{zADWE5=mbohS80 zH~db&-i9lO*6fkPirC@r-~6!adgJqEjYrY9XY^02QC56x@AyoSGP;zz?zhMHKmAQL zWqF1EnB52Z@3S<>vZ74(Gh?DTmA*3F^8UOJ7vHq~hvS+zLd{y<_v!2izwYqTg|HR@ zT3LHxID6Y|hpg0T_R6~=Gy8U*ksGMnaLX~;+N<={n_Cm_~ixCg7C`Is;F_u`7R0 zkN+Oge~TrI`p3v^YPLHrCjP_oD2GG2F)u3;9X|Xf8av?v4=6f85HSAyibUtaZNqyn z^HN@}0P}i?Kdy=K>j1A(bk~g)JRqKt(^CfG-s-~13`^n3hIWw$^H}(-dbT5Vg`XJ; zGUJO#`C{^su&roJ%-x2~e4$T&Gk_aC9p>b``Dbco7F191uUeA2C>?vo{Qh@%^OVJT zsmMafl#Ucov<{pzc7lqD>k~dqX3WJd{%$fZ{~R!`s&Y6tJ(_l5V0`14uJ8=^#^N7X z!~>kWTm=qF z!&3)&YTq~dH+5AHzjgX0aHP~i`gcc4`VR{^omCGMeye_NCMvan242AV=PjRzR* zsP%oZXNOR;_h<_59`slY%?TSp3&Lq>Yuyv8h&2B}9fP)ydY47wG$u2drJ{Lu@%n?= zF5e6u(pbts9yb*>h_dx^EGkD?_bbh=d7ENLRIv|ewj@8+-BEG8bozrPy?06*`_TZC zhmXMNwew~R0a@RLI9iP_-cdDut<9{PRvQ;Mo^b!xnMbelD-3oGU>n}ztRqgaQHy2s zBik1;DA*uik0>}{b9?&e8CoD)#j(t2a{q^J&7k8g(ytL(bAHGec+eS<$`t)cm==A6n^ z6X^#d?u%Wv|0HacCFP>Dht?5^Ak7Z)rx^uSmNT^mr%ZVj85(PAJ9L}v0~%jmuD|*G zcWuJM1;4;6*vSCU6}JWIeNVi*;p+#-@7BrYS7(C3&0h~aDs>*Mk~;9T_e{-Y!SuCv z7qQW9c%NenEG>PpyC+~3^;TeaxuWvv%NZwT_C1P|%}^6|<5JN!7LBw_O9LOOf$IlZ zSX^!>WmaPV;+lVP%WxmxrJj+RrkB+w^Q5ji+-aCEOptrm1#Ffd)m(~&$tP6;uXi|! zhvo?W@>Zhm&=^{}-m@qOri=JWMAQ$Bc7wUo*kTg>&agU+e;0a@o^hY!M9!3(32Wr> z0e-i0_|F_17N+mYQQ`>|4XPv1wbGe4}Zpp4&7+2nL@9|SgR zKs_)VJ`b450d5oiK{M7>?)#s(v)$vWyLI=P=x*K%p=veGe4M6guZYwez)DXQqNB5W zW^d*<>J_|a&l=nF(C*S`#eoCOYw5*rO^P^@H16P2{e;vEE6OFLoU>qSH(aEdt=e|as`U9I1HJg>HQe7N43FIt|Kff| z-SXi1i+dYyU>L8u>#g-r;QFeZSX(c)AfDQr=l{qHnjAR3zD=c$bw_YtOo-OgQwKSB zm}1+D!9ZLRG?Y-(d>T8xb&@L7Z|l)K$#z3Q&CqPPB$2o9M9GPJOHu^3i+GTMyAHWQ zi@qozA)Rz*G!okv&F2ywzWwnzMVp7iiF{AK8}l{A3=%@w6e1PK*gL>kF4o^s+jdzv zM$y$1Z!GmlC2L$czIf>F)%b??yaQRcKWW^iV`vXO;K~koq5tBOJKg7b=P=32$>7gJ zSjY{zxbIV9k{F6%RY|Fr`ZKHTT_;A>7Nfzgm(lKeA1)h*6R3>IKpUF{@8@ATgpu%ncBs#MHVW~mQou(w5R;QUN6$Q z)i=T%p)1Zj@&g$#?>kJfFIE{h3tH?L;7dN%LG6BoR$a?G$u9MZH4H=t?8G95je;30ncQw_< z?5X?iQBhq{xlJ##JIC2BGAjLoz9BZ~4N-tBT+J@kviZ!^R^7blJGE<%+!pxyi|^Kc zz38V&AJjP$uBEbmtga;zWq_-uBIJE;qxP%pp+K29;z3 zI9}~iTe{yO%)uf)6Z0v2W0A7z=~pU`%K0j*pL~|$H}*iqqk!(_3Ujor`oFua5qH#* z7Vvho-7I{|_UVg?_CVfolRewc2P&tEaJ(bhJW-viIy{canwjY&CUxZ%xAc_>4f6ep z+7>tSX(l8q?4<+#RyP@Ryx8uw^wLrc_72eu!IcAi$j`feX)n399oc_~4|-={_9)0K8x>^RPKZsa6;`Gxn+ucP+R!Y+c@ zOPgV@^vR?5Gmq|mMb#HMqE;yh&%jj9t5z=c%wYx!&(Vh^+aj%0 zjvd<2pC;1QD=O9(sZHMeWL{HZDq%J~ zzVI7&3T|8FQEg`%(f#%88SIl4GCiUL`4f)Y4u8`)Zu0P4p+cEn{G7_tBUvt&YqOj) z>Jn*t4fiMF!Pw~^(6q?_Dqk^>?FRQx{)`ck^IF3p8*t&+anX#4vpcQ}B*kI(N)|`& z)FXj%FdeoZ=8wOc+we=%2TgMKd#SXY9ect)@1-zW)^*ZnVe6^(t-cC%M}XdTAS=58 znhTTDJO5m2h)CfX6LSPH#)_TN=Z9-FW`8kTgCuC%=0cfG1a1W8G->do_FR^2Y<1zM zj61gC;EEsDx_LYI+F047Lw9k_cfEUN!qo{PYzWq68$zortqJM2dtnlkFYl=dHypM+ zb4&M-;0%vmk!PE?{ZNGbtELMCjT=ZDIs}z@H1fFC^zc7Jr|r)8M)%plsk(4Q6M?-+ zUt8%LU9v@d%7rn7r1}>9FsPW>EPtdn!zFd;(*5muFKne=bl6&!23_;Xs^Szk2yIZ` zaoH2S1fU{73L1Tqwh@PN?>C2*-EnO6O=X=_;GKFgsJ*l{K_^ZymQTjE3eq0m2BymU zV3c8x-Be(4{AB(LKhDOw0~|`0y{vXPJA1=1@9V$E)qepE3F1luo*R1r?swRQH(h&k zuQ}?p*Qzki%9?WCwBQ~`pZYbg4g@y8zU9nm!Ah}t1jf<{s36F;wbJGqVft`_jARG+ z0~F;gludgO;3G0dU{(k@my}%o&b9kH zKko0Gx4(IXzqx|H`IEmnfxmf;zqx?F`H8NgGKrq)nvFBw3OiX$xs9NuIQgw4Jnrw3DPj zQY0ynlu5ftDx}>cRgxM>ouon1Bx#YfNjfB)1W11q8@ePtl0M0RWJoe18Iw#%dq}1v zGm<%JFKHjif@DeBPqHFelWa%_NVX(9l0C_R$1(JeD=Sjh&5K<_KAcc|s E2XWskA^-pY diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/manifest.json b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/manifest.json index 078241d8..9b91675f 100644 --- a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/manifest.json +++ b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/manifest.json @@ -6,12 +6,8 @@ "byte_order": "little", "size_t_bytes": 8, "sha256": { - "pre-ivf-per-cluster-storage/ivf32_ip.expected": "3ecb6604dce2108556e5e48130d4e9902d7ef7f0ef81aba39a3caa40b62230fe", - "pre-ivf-per-cluster-storage/ivf32_ip.index": "e7c2850ce9b09b4a660e85b3aafe03854d206cb2659801e5ef4752f295415805", "pre-ivf-per-cluster-storage/ivf32_l2.expected": "7843685eadb19c6023df353aa6602e56f5042e4d0c6c2b1ac6019949b7459734", "pre-ivf-per-cluster-storage/ivf32_l2.index": "4b0f98a898edf9cf157d226c51ba74a313d5fb5c055dc5663e14ce3a631f121b", - "pre-ivf-per-cluster-storage/ivf4_ip.expected": "2107393b9f53a79d5b07b29771d069b7bedffb1608f105913ff95899dc27f0e0", - "pre-ivf-per-cluster-storage/ivf4_ip.index": "928f4440a375f2cbdb5d7f524b3b52b33e6b9dc176dd7b86dbbaef7a6aa091c6", "pre-ivf-per-cluster-storage/ivf4_l2.expected": "7de95886d52dfbb5300890f920256fd7927eae2a4528223753e6f44f76a3e20d", "pre-ivf-per-cluster-storage/ivf4_l2.index": "eb73f9066fef993a5f7e833641adb8bddb1e95cdc04addb2261a8e10bec0c41f" } diff --git a/tests/unit/rabitqlib/index/historical_persistence_test.cpp b/tests/unit/rabitqlib/index/historical_persistence_test.cpp index 69ab4e13..645f0214 100644 --- a/tests/unit/rabitqlib/index/historical_persistence_test.cpp +++ b/tests/unit/rabitqlib/index/historical_persistence_test.cpp @@ -63,8 +63,8 @@ TEST_P(HistoricalPersistenceTest, LoadsSearchesResavesAndPreservesStateAfterFail for (size_t i = 0; i < ids.size(); ++i) expected >> reference_ids[i] >> reference_distances[i]; ASSERT_TRUE(expected.good()); - const auto output = - std::filesystem::temp_directory_path() / ("rabitq_historical_" + name); + const auto output = std::filesystem::temp_directory_path() / + ("rabitq_historical_" + GetParam().directory + "_" + name); const auto compare = [&] { EXPECT_EQ(ids, reference_ids); for (size_t i = 0; i < ids.size(); ++i) { @@ -161,9 +161,7 @@ INSTANTIATE_TEST_SUITE_P( HistoricalPersistenceTest, ::testing::Values( HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf4_l2", 96, 130, 4, true}, - HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf4_ip", 96, 130, 4, true}, - HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf32_l2", 96, 130, 4, true}, - HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf32_ip", 96, 130, 4, true} + HistoricalFixtureCase{"pre-ivf-per-cluster-storage", "ivf32_l2", 96, 130, 4, true} ), [](const auto& info) { return info.param.name; } );

VrWmnUozldz`Uj- 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 diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_l2.expected b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_l2.expected index 1afdb5b4..499fa616 100644 --- a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_l2.expected +++ b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_l2.expected @@ -1,17 +1,17 @@ -33 65 3 5 --1.2578125 -3.078125 -3.328125 0.67578125 3.171875 -0.2578125 -2.796875 0.91015625 -3.859375 0.328125 2.296875 -2.0859375 -0.66015625 -3.3046875 2.19140625 2.1484375 2.328125 3.7890625 3.3515625 0.08203125 1.41796875 1.43359375 -1.1484375 1.4375 2.55078125 2.0703125 -1.5859375 -2.8046875 -2.2890625 1.73828125 -2.41796875 -3.09765625 1.53515625 -3.6953125 -0.66796875 0.67578125 0.96484375 -2.9296875 2.41796875 1.25390625 -1.8984375 3.14453125 2.359375 0.65625 -2.52734375 -0.17578125 -0.80859375 -2.171875 -1.63671875 -0.421875 -3.08203125 -0.1328125 -1.94140625 2.734375 2.1875 -2.1015625 3.828125 3.82421875 -3.61328125 -0.9921875 -3.27734375 2.9765625 2.29296875 0.7421875 3.48828125 -2.58203125 1.0234375 -3.12109375 1.83203125 -2.29296875 1.05859375 2.15234375 -2.5390625 -2.3125 -2.7265625 -3.33203125 1.671875 3.6796875 3.53515625 -1.75 0.6328125 -3.1171875 -2.6875 -0.3671875 -2.640625 -2.52734375 -0.65625 1.89453125 -1.08984375 0.25390625 -3.65234375 1.08203125 -0.55078125 3.6171875 2.80078125 2.265625 2.80078125 0.98828125 -1.03515625 1.0234375 -0.04296875 2.95703125 1.8125 -1.6796875 2.75390625 -3.70703125 1.60546875 0.27734375 -3.2265625 -2.57421875 -0.875 1.40234375 -3.55078125 2.80859375 2.953125 3.37109375 -0.02734375 3.16796875 3.23828125 -0.21875 0.57421875 -2.82421875 3.80078125 1.66796875 -3.30859375 -1.21875 1.05078125 1.0546875 2.2890625 3.84765625 1.58984375 -0.7734375 -0.48046875 1.28125 -1.30859375 -1.27734375 2.67578125 1.19921875 2.3515625 -0.140625 -3.828125 -3.84375 1.0703125 -2.27734375 -2.05859375 3.59375 1.3359375 -3.44140625 -0.40234375 -0.26953125 1.671875 -3.24609375 -3.42578125 0.76953125 -0.67578125 -1.53125 -3.2578125 3.65234375 -2.9921875 3.4609375 0.25390625 1.5390625 -0.78125 2.109375 0.99609375 1.5078125 -0.33203125 -1.4296875 -0.01953125 -0.625 2.609375 0.796875 -1.1875 1.125 3.62890625 -3.23046875 -2.91015625 3.0859375 2.23828125 -3.6015625 -1.38671875 3.0703125 -2.2890625 -3.375 3.0390625 -2.02734375 -0.9140625 1 -0.0546875 0.66796875 -3.52734375 -2.8046875 0.93359375 -3.21875 -2.05859375 -26 450.112122 -17 458.235657 -30 499.837524 -31 524.303589 -9 532.450073 -10 492.983521 -4 575.742737 -24 616.615479 -25 633.027588 -12 635.89978 -31 497.363464 -8 526.510376 -26 553.711975 -25 572.029541 -14 589.477295 +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 +92 420.222473 +46 471.803406 +128 482.1716 +43 495.518188 +3 497.874329 +31 412.941284 +112 416.260925 +129 460.315308 +71 464.002625 +128 464.167297 +32 423.822571 +3 449.275635 +87 462.434082 +12 477.827637 +48 480.80365 diff --git a/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_l2.index b/tests/fixtures/persistence/pre-ivf-per-cluster-storage/ivf32_l2.index index 94e1ac8a09c3fe6a366b292beb0f22f3e8d3ed21..aa50d1f3a82e2dae785b776d06405c5a0c6f8777 100644 GIT binary patch literal 40606 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 m+h9BFfGc4q?1J5J6j20 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 E2XWskA^-pY delta 2834 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