tieredq 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. tieredq-0.1.0/.gitignore +14 -0
  2. tieredq-0.1.0/CMakeLists.txt +89 -0
  3. tieredq-0.1.0/LICENSE +21 -0
  4. tieredq-0.1.0/NOTICE +14 -0
  5. tieredq-0.1.0/PKG-INFO +268 -0
  6. tieredq-0.1.0/README.md +245 -0
  7. tieredq-0.1.0/bindings/arrays.h +62 -0
  8. tieredq-0.1.0/bindings/index.cpp +212 -0
  9. tieredq-0.1.0/bindings/ivf.cpp +143 -0
  10. tieredq-0.1.0/bindings/module.cpp +8 -0
  11. tieredq-0.1.0/bindings/nested.cpp +259 -0
  12. tieredq-0.1.0/bindings/search.h +21 -0
  13. tieredq-0.1.0/bindings/training.h +63 -0
  14. tieredq-0.1.0/cmake/Python.cmake +14 -0
  15. tieredq-0.1.0/cmake/TieredQConfig.cmake.in +8 -0
  16. tieredq-0.1.0/docs/benchmark-report.md +472 -0
  17. tieredq-0.1.0/docs/benchmarks/fast-only-storage-results.json +1 -0
  18. tieredq-0.1.0/docs/benchmarks/glove200-fast-scan-results.json +183 -0
  19. tieredq-0.1.0/docs/benchmarks/ivf-glove100-fast-scan-results.json +938 -0
  20. tieredq-0.1.0/docs/benchmarks/linear-fast-scan-results.json +1004 -0
  21. tieredq-0.1.0/docs/benchmarks/rabitq-glove200-reference-results.json +647 -0
  22. tieredq-0.1.0/docs/fast-only-mode-validation.md +30 -0
  23. tieredq-0.1.0/docs/glove200-fast-scan.md +137 -0
  24. tieredq-0.1.0/docs/high-bit-recall.md +271 -0
  25. tieredq-0.1.0/docs/ivf-fast-scan-optimization-analysis.md +89 -0
  26. tieredq-0.1.0/docs/ivf-glove100-fast-scan.md +45 -0
  27. tieredq-0.1.0/docs/linear-fast-scan.md +54 -0
  28. tieredq-0.1.0/docs/qps-first-principles.md +437 -0
  29. tieredq-0.1.0/docs/rabitq-glove200-reference.md +51 -0
  30. tieredq-0.1.0/docs/releasing.md +88 -0
  31. tieredq-0.1.0/docs/tieredq-vs-rabitq-benchmark.md +268 -0
  32. tieredq-0.1.0/examples/fast_scan_benchmark.cpp +116 -0
  33. tieredq-0.1.0/examples/scan_benchmark.cpp +106 -0
  34. tieredq-0.1.0/examples/search.cpp +24 -0
  35. tieredq-0.1.0/include/tieredq/detail/tieredq_scan_plan.h +12 -0
  36. tieredq-0.1.0/include/tieredq/tieredq.h +370 -0
  37. tieredq-0.1.0/include/tieredq/tieredq_blocks.h +66 -0
  38. tieredq-0.1.0/include/tieredq/tieredq_nested.h +138 -0
  39. tieredq-0.1.0/include/tieredq/tieredq_nested_flat.h +32 -0
  40. tieredq-0.1.0/include/tieredq/tieredq_refinement.h +91 -0
  41. tieredq-0.1.0/integration.md +196 -0
  42. tieredq-0.1.0/licenses/gcc-gpl3.txt +674 -0
  43. tieredq-0.1.0/licenses/gcc-lgpl21.txt +517 -0
  44. tieredq-0.1.0/licenses/gcc-runtime-exception.txt +72 -0
  45. tieredq-0.1.0/licenses/llvm-openmp.txt +361 -0
  46. tieredq-0.1.0/licenses/nanobind.txt +26 -0
  47. tieredq-0.1.0/licenses/openblas.txt +29 -0
  48. tieredq-0.1.0/licenses/robin-map.txt +21 -0
  49. tieredq-0.1.0/publish-pypi.sh +6 -0
  50. tieredq-0.1.0/pyproject.toml +58 -0
  51. tieredq-0.1.0/python/tieredq/__init__.py +4 -0
  52. tieredq-0.1.0/scripts/release.py +227 -0
  53. tieredq-0.1.0/src/tieredq.cpp +1113 -0
  54. tieredq-0.1.0/src/tieredq_additive_books.h +35 -0
  55. tieredq-0.1.0/src/tieredq_basis_panels.h +174 -0
  56. tieredq-0.1.0/src/tieredq_block_array.h +44 -0
  57. tieredq-0.1.0/src/tieredq_blocks.cpp +203 -0
  58. tieredq-0.1.0/src/tieredq_encode.h +139 -0
  59. tieredq-0.1.0/src/tieredq_encode_float.h +136 -0
  60. tieredq-0.1.0/src/tieredq_encode_grid.h +250 -0
  61. tieredq-0.1.0/src/tieredq_encode_matrix.h +127 -0
  62. tieredq-0.1.0/src/tieredq_fast_dispatch.h +29 -0
  63. tieredq-0.1.0/src/tieredq_fast_fine.h +131 -0
  64. tieredq-0.1.0/src/tieredq_fast_rows.h +191 -0
  65. tieredq-0.1.0/src/tieredq_fast_scan.h +139 -0
  66. tieredq-0.1.0/src/tieredq_fast_storage.h +157 -0
  67. tieredq-0.1.0/src/tieredq_index.cpp +444 -0
  68. tieredq-0.1.0/src/tieredq_index_io.cpp +69 -0
  69. tieredq-0.1.0/src/tieredq_index_read.h +32 -0
  70. tieredq-0.1.0/src/tieredq_io.h +167 -0
  71. tieredq-0.1.0/src/tieredq_ivf.cpp +405 -0
  72. tieredq-0.1.0/src/tieredq_ivf_coarse.h +77 -0
  73. tieredq-0.1.0/src/tieredq_ivf_rows.cpp +25 -0
  74. tieredq-0.1.0/src/tieredq_layout.h +94 -0
  75. tieredq-0.1.0/src/tieredq_lookup_layout.h +116 -0
  76. tieredq-0.1.0/src/tieredq_lookup_range.h +89 -0
  77. tieredq-0.1.0/src/tieredq_lookup_range_avx2.h +61 -0
  78. tieredq-0.1.0/src/tieredq_lookup_range_avx512.h +89 -0
  79. tieredq-0.1.0/src/tieredq_lookup_range_neon.h +74 -0
  80. tieredq-0.1.0/src/tieredq_metric.h +76 -0
  81. tieredq-0.1.0/src/tieredq_nested.cpp +405 -0
  82. tieredq-0.1.0/src/tieredq_nested_flat.cpp +41 -0
  83. tieredq-0.1.0/src/tieredq_profile.h +64 -0
  84. tieredq-0.1.0/src/tieredq_query_projection_x86.h +182 -0
  85. tieredq-0.1.0/src/tieredq_range_checkpoint.h +72 -0
  86. tieredq-0.1.0/src/tieredq_range_checkpoint_avx512.h +92 -0
  87. tieredq-0.1.0/src/tieredq_refinement.cpp +692 -0
  88. tieredq-0.1.0/src/tieredq_scan_avx512.h +141 -0
  89. tieredq-0.1.0/src/tieredq_scan_codes.h +79 -0
  90. tieredq-0.1.0/src/tieredq_scan_codes_neon.h +89 -0
  91. tieredq-0.1.0/src/tieredq_scan_codes_x86.h +351 -0
  92. tieredq-0.1.0/src/tieredq_scan_fast_scalar.h +73 -0
  93. tieredq-0.1.0/src/tieredq_scan_groups.h +74 -0
  94. tieredq-0.1.0/src/tieredq_scan_int8.h +182 -0
  95. tieredq-0.1.0/src/tieredq_scan_int8_avx2.h +121 -0
  96. tieredq-0.1.0/src/tieredq_scan_int8_avx512.h +82 -0
  97. tieredq-0.1.0/src/tieredq_scan_int8_neon.h +120 -0
  98. tieredq-0.1.0/src/tieredq_scan_lookup_avx512.h +100 -0
  99. tieredq-0.1.0/src/tieredq_scan_lookup_neon.h +113 -0
  100. tieredq-0.1.0/src/tieredq_scan_lookup_x86.h +309 -0
  101. tieredq-0.1.0/src/tieredq_scan_neon.h +220 -0
  102. tieredq-0.1.0/src/tieredq_scan_query_x86.h +58 -0
  103. tieredq-0.1.0/src/tieredq_search.h +111 -0
  104. tieredq-0.1.0/src/tieredq_search_fast.h +80 -0
  105. tieredq-0.1.0/src/tieredq_train_allocation.h +155 -0
  106. tieredq-0.1.0/src/tieredq_train_codebook.h +51 -0
  107. tieredq-0.1.0/src/tieredq_train_codebook_data.h +160 -0
  108. tieredq-0.1.0/src/tieredq_train_covariance.h +71 -0
  109. tieredq-0.1.0/src/tieredq_train_fine.h +56 -0
  110. tieredq-0.1.0/src/tieredq_train_kmeans.h +58 -0
  111. tieredq-0.1.0/src/tieredq_train_linalg.h +117 -0
  112. tieredq-0.1.0/src/tieredq_train_residuals.h +40 -0
  113. tieredq-0.1.0/src/tieredq_train_rows.h +47 -0
  114. tieredq-0.1.0/src/tieredq_train_stats.h +106 -0
  115. tieredq-0.1.0/src/training_rng.h +37 -0
  116. tieredq-0.1.0/tests/fixtures/tieredq_codec.codec +0 -0
  117. tieredq-0.1.0/tests/fixtures/tieredq_codec.md +5 -0
  118. tieredq-0.1.0/tests/python/test_api.py +215 -0
  119. tieredq-0.1.0/tests/python/test_fast_mode.py +270 -0
  120. tieredq-0.1.0/tests/python/test_fast_scan.py +138 -0
  121. tieredq-0.1.0/tests/python/test_fine_tiers.py +52 -0
  122. tieredq-0.1.0/tests/python/test_index_modes.py +42 -0
  123. tieredq-0.1.0/tests/python/test_ivf_nested.py +210 -0
  124. tieredq-0.1.0/tests/python/test_small_training.py +45 -0
  125. tieredq-0.1.0/tests/test_tieredq.cpp +363 -0
  126. tieredq-0.1.0/tests/test_tieredq_additive.cpp +318 -0
  127. tieredq-0.1.0/tests/test_tieredq_blocks.cpp +114 -0
  128. tieredq-0.1.0/tests/test_tieredq_encoder.cpp +101 -0
  129. tieredq-0.1.0/tests/test_tieredq_fast_fine.cpp +144 -0
  130. tieredq-0.1.0/tests/test_tieredq_fast_mode.cpp +129 -0
  131. tieredq-0.1.0/tests/test_tieredq_ivf.cpp +216 -0
  132. tieredq-0.1.0/tests/test_tieredq_lookup.cpp +366 -0
  133. tieredq-0.1.0/tests/test_tieredq_metric.cpp +212 -0
  134. tieredq-0.1.0/tests/test_tieredq_model.cpp +513 -0
  135. tieredq-0.1.0/tests/test_tieredq_neon.cpp +86 -0
  136. tieredq-0.1.0/tests/test_tieredq_nested.cpp +231 -0
  137. tieredq-0.1.0/tests/test_tieredq_nested_flat.cpp +95 -0
  138. tieredq-0.1.0/tests/test_tieredq_projection.cpp +107 -0
  139. tieredq-0.1.0/tests/test_tieredq_range_checkpoint.cpp +121 -0
  140. tieredq-0.1.0/tests/test_tieredq_refinement.cpp +181 -0
  141. tieredq-0.1.0/tests/test_tieredq_search.cpp +344 -0
  142. tieredq-0.1.0/tests/test_training_rng.cpp +47 -0
@@ -0,0 +1,14 @@
1
+ build/
2
+ dist/
3
+ wheelhouse/
4
+ example.tq
5
+ .venv/
6
+ __pycache__/
7
+ .pytest_cache/
8
+ *.egg-info/
9
+
10
+ # Local pruning research and provenance; production range pruning lives in src/.
11
+ /benchmark/experiments/error_bound_pruning/
12
+ /benchmark/experiments/lowbit_rabitq/
13
+ /benchmark/results/pruning-decision-20261026/
14
+ /benchmark/results/pruning-scanner-ablation-20261026/
@@ -0,0 +1,89 @@
1
+ # Standalone TieredQ codec and exhaustive in-memory index.
2
+ cmake_minimum_required(VERSION 3.18)
3
+ project(TieredQ VERSION 0.1.0 LANGUAGES CXX)
4
+ include(GNUInstallDirs)
5
+ include(CMakePackageConfigHelpers)
6
+ set(TIEREDQ_STANDALONE OFF)
7
+ if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
8
+ set(TIEREDQ_STANDALONE ON)
9
+ endif()
10
+ option(TIEREDQ_TESTS "Build TieredQ tests" ${TIEREDQ_STANDALONE})
11
+ option(TIEREDQ_EXAMPLES "Build standalone example/benchmark" ${TIEREDQ_STANDALONE})
12
+ if(APPLE)
13
+ execute_process(COMMAND brew --prefix libomp OUTPUT_VARIABLE LIBOMP_PREFIX OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET)
14
+ if(LIBOMP_PREFIX)
15
+ list(APPEND CMAKE_PREFIX_PATH "${LIBOMP_PREFIX}")
16
+ endif()
17
+ endif()
18
+ find_package(OpenMP REQUIRED)
19
+ # Training's covariance (syrk) and eigendecomposition (syevd) run about 10x faster on an optimized BLAS/LAPACK than on
20
+ # the reference one, so OpenBLAS, then sequential MKL, are preferred unless BLA_VENDOR names one.
21
+ if(NOT DEFINED BLA_VENDOR)
22
+ foreach(vendor OpenBLAS Intel10_64lp_seq)
23
+ set(BLA_VENDOR ${vendor})
24
+ find_package(LAPACK QUIET)
25
+ if(LAPACK_FOUND)
26
+ break()
27
+ endif()
28
+ endforeach()
29
+ if(NOT LAPACK_FOUND)
30
+ unset(BLA_VENDOR)
31
+ endif()
32
+ endif()
33
+ find_package(LAPACK REQUIRED)
34
+ set(TIEREDQ_BLA_VENDOR "${BLA_VENDOR}")
35
+ message(STATUS "TieredQ LAPACK: ${BLA_VENDOR} ${LAPACK_LIBRARIES}")
36
+ add_library(dv_tieredq STATIC src/tieredq.cpp src/tieredq_index.cpp src/tieredq_index_io.cpp src/tieredq_ivf.cpp src/tieredq_refinement.cpp src/tieredq_ivf_rows.cpp src/tieredq_nested.cpp src/tieredq_nested_flat.cpp src/tieredq_blocks.cpp)
37
+ add_library(TieredQ::TieredQ ALIAS dv_tieredq)
38
+ set_target_properties(dv_tieredq PROPERTIES POSITION_INDEPENDENT_CODE ON EXPORT_NAME TieredQ)
39
+ target_compile_features(dv_tieredq PUBLIC cxx_std_20)
40
+ target_include_directories(dv_tieredq PUBLIC
41
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
42
+ $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
43
+ target_compile_options(dv_tieredq PRIVATE -O3 -Wall -Wextra -Wno-misleading-indentation -ffp-contract=off)
44
+ target_link_libraries(dv_tieredq PUBLIC LAPACK::LAPACK OpenMP::OpenMP_CXX ${CMAKE_DL_LIBS})
45
+ # Optional io_uring for concurrent file-tier reads (TieredQFileRows queue_depth > 1); without it reads are serial.
46
+ option(TIEREDQ_URING "Use liburing for concurrent tier reads when found" ON)
47
+ if(TIEREDQ_URING)
48
+ find_package(PkgConfig QUIET)
49
+ if(PkgConfig_FOUND)
50
+ pkg_check_modules(LIBURING QUIET IMPORTED_TARGET liburing)
51
+ endif()
52
+ if(LIBURING_FOUND)
53
+ target_link_libraries(dv_tieredq PUBLIC PkgConfig::LIBURING)
54
+ target_compile_definitions(dv_tieredq PRIVATE DV_TIEREDQ_URING)
55
+ endif()
56
+ endif()
57
+ message(STATUS "TieredQ io_uring tier reads: ${LIBURING_FOUND}")
58
+ if(TIEREDQ_TESTS)
59
+ enable_testing()
60
+ add_executable(test_tieredq tests/test_tieredq.cpp tests/test_tieredq_model.cpp tests/test_tieredq_encoder.cpp tests/test_tieredq_neon.cpp tests/test_tieredq_lookup.cpp tests/test_tieredq_projection.cpp tests/test_tieredq_search.cpp tests/test_tieredq_fast_mode.cpp tests/test_tieredq_range_checkpoint.cpp tests/test_tieredq_additive.cpp tests/test_tieredq_metric.cpp tests/test_tieredq_ivf.cpp tests/test_tieredq_refinement.cpp tests/test_tieredq_nested.cpp tests/test_tieredq_nested_flat.cpp tests/test_tieredq_blocks.cpp tests/test_tieredq_fast_fine.cpp)
61
+ target_include_directories(test_tieredq PRIVATE src)
62
+ target_compile_options(test_tieredq PRIVATE -ffp-contract=off)
63
+ target_link_libraries(test_tieredq PRIVATE TieredQ::TieredQ)
64
+ add_test(NAME tieredq COMMAND test_tieredq)
65
+ add_executable(test_training_rng tests/test_training_rng.cpp)
66
+ target_compile_features(test_training_rng PRIVATE cxx_std_20)
67
+ target_include_directories(test_training_rng PRIVATE src)
68
+ add_test(NAME training_rng COMMAND test_training_rng)
69
+ endif()
70
+ if(TIEREDQ_EXAMPLES)
71
+ add_executable(tieredq_scan_benchmark examples/scan_benchmark.cpp)
72
+ target_link_libraries(tieredq_scan_benchmark PRIVATE TieredQ::TieredQ)
73
+ target_compile_options(tieredq_scan_benchmark PRIVATE -O3 -ffp-contract=off)
74
+ add_executable(tieredq_fast_scan_benchmark examples/fast_scan_benchmark.cpp)
75
+ target_link_libraries(tieredq_fast_scan_benchmark PRIVATE TieredQ::TieredQ)
76
+ add_executable(tieredq_example examples/search.cpp)
77
+ target_link_libraries(tieredq_example PRIVATE TieredQ::TieredQ)
78
+ endif()
79
+ install(TARGETS dv_tieredq EXPORT TieredQTargets ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})
80
+ install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
81
+ install(FILES LICENSE NOTICE DESTINATION ${CMAKE_INSTALL_DATADIR}/TieredQ)
82
+ install(EXPORT TieredQTargets NAMESPACE TieredQ:: DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/TieredQ)
83
+ configure_package_config_file(cmake/TieredQConfig.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/TieredQConfig.cmake" INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/TieredQ)
84
+ write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/TieredQConfigVersion.cmake" VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion)
85
+ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/TieredQConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/TieredQConfigVersion.cmake" DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/TieredQ)
86
+
87
+ if(SKBUILD AND TIEREDQ_STANDALONE)
88
+ include(cmake/Python.cmake)
89
+ endif()
tieredq-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sean Pedersen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
tieredq-0.1.0/NOTICE ADDED
@@ -0,0 +1,14 @@
1
+ TieredQ is licensed under the MIT License; see LICENSE.
2
+
3
+ Training randomness uses the C++ standard library. No copied RNG code or
4
+ random-distribution tables are included.
5
+
6
+ LAPACK, BLAS and OpenMP implementations are supplied by the build environment
7
+ under their respective licenses. Release wheels bundle non-system dependencies:
8
+ Linux wheels use OpenBLAS and GCC runtime libraries; macOS wheels use LLVM
9
+ OpenMP and the system Accelerate framework. Their license texts are included in
10
+ licenses/ (OpenBLAS BSD, LLVM Apache-2.0 with LLVM exceptions, GCC GPL-3.0 with
11
+ the GCC Runtime Library Exception, and LGPL-2.1 for libquadmath if bundled).
12
+
13
+ Python bindings use nanobind (BSD-3-Clause) and its robin-map dependency (MIT).
14
+ Their notices are in licenses/ and included in Python installations.
tieredq-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,268 @@
1
+ Metadata-Version: 2.4
2
+ Name: tieredq
3
+ Version: 0.1.0
4
+ Summary: Trained exhaustive in-memory vector index with variance-aware bit allocation
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ License-File: NOTICE
8
+ License-File: licenses/gcc-gpl3.txt
9
+ License-File: licenses/gcc-lgpl21.txt
10
+ License-File: licenses/gcc-runtime-exception.txt
11
+ License-File: licenses/llvm-openmp.txt
12
+ License-File: licenses/nanobind.txt
13
+ License-File: licenses/openblas.txt
14
+ License-File: licenses/robin-map.txt
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: C++
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Project-URL: Homepage, https://github.com/SeanPedersen/TieredQ
19
+ Project-URL: Issues, https://github.com/SeanPedersen/TieredQ/issues
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: numpy>=1.24
22
+ Description-Content-Type: text/markdown
23
+
24
+ # TieredQ: Variance-Aware Bit Allocation
25
+
26
+ TieredQ is a new vector quantization algorithm that dynamically allocates bits per dimension, providing higher recall compression ratio (compared to RaBitQ or TurboQuant). TieredQ centers and rotates vectors into a PCA basis, then greedily allocates 0–8 bits per dimension to reduce quantization error (0–12 in the flat fast index, whose fine tiers scan only each code's top bits and rescore candidates exactly). Existing ANN indexes (HNSW / IVF) can use the codec directly as drop-in for RaBitQ or TurboQuant.
27
+
28
+ TieredQ core algorithm:
29
+ 1. PCA
30
+ 2. Variance-aware greedy bit allocation per dimension (principal component)
31
+ 3. Scalar quantization per dimension with allocated bit budget
32
+
33
+ ## Install
34
+
35
+ Install [TieredQ from PyPI](https://pypi.org/project/tieredq/):
36
+
37
+ ```sh
38
+ uv add tieredq
39
+ # or: pip install tieredq
40
+ ```
41
+
42
+ Wheels are available for CPython 3.10–3.14 on Linux x86-64 (glibc 2.28+) and
43
+ macOS Apple Silicon (macOS 26+). They bundle their non-system native dependencies.
44
+
45
+ To install from source, use the repository below. Source builds require C++20,
46
+ CMake, LAPACK/BLAS and OpenMP. On macOS, install `libomp` with Homebrew;
47
+ Accelerate provides LAPACK. On Ubuntu, install `liblapack-dev`, `libblas-dev`
48
+ and OpenMP support.
49
+
50
+ ```sh
51
+ uv add "tieredq @ git+ssh://git@github.com/SeanPedersen/TieredQ.git"
52
+ ```
53
+
54
+ See [the release guide](docs/releasing.md) for local builds, validation and publishing.
55
+
56
+ ## Python
57
+
58
+ ```python
59
+ import gensim.downloader as api
60
+ from tieredq import TieredQIndex
61
+
62
+ vectors = api.load("glove-wiki-gigaword-100").vectors # (400000, 100), float32
63
+ index = TieredQIndex(dimensions=100, code_bits=200) # 2 bits per dim
64
+ index.add(vectors) # first add trains (a seeded 32,768-row sample of larger batches); later adds only encode
65
+ scores, ids = index.search(vectors[:2], k=10) # (2, 10) float32, uint32
66
+
67
+ index.save("index.tq")
68
+ index = TieredQIndex.load("index.tq")
69
+ ```
70
+
71
+ - `metric="ip"` (inner product) is the default; `metric="cosine"` normalizes rows and
72
+ queries, and `metric="l2"` serves Euclidean search with scores of `-|q - x|^2`.
73
+ L2 lifts vectors into one extra dimension with a lift whitened for the query
74
+ distribution, stored in the model, so saved indexes stay self-contained.
75
+ - Bit allocation follows the metric: inner-product and cosine models allocate
76
+ on PCA variance^1.5 (recall-optimal when queries are distributed like the data,
77
+ +0.5 to +0.8 R@10 points at equal disk on DBpedia OpenAI/Gemini, GloVe neutral to
78
+ +1.3, issue #9) in both modes; L2 models use variance (MSE), which is optimal for
79
+ whitened queries.
80
+ `allocation_power=` overrides it.
81
+ - `mode="fast"` is the default and stores the native fast search representation. Models with tiers above 4 bits score
82
+ through approximate byte tables (L2 models keep exact FP32 scoring); `exact_fast_scores=True` keeps bit-exact
83
+ FP32 scoring at lower speed.
84
+ Use `mode="full"` for compact packed rows and FP32 scoring of quantized vectors,
85
+ with slower search. Both modes scan every vector; saving and loading preserve
86
+ the selected representation. Read the selection from `index.mode`.
87
+ The former `fast_mode` boolean argument/property has been removed; use `mode`.
88
+ - Fast ip/cosine indexes may add fine tiers (#26): ranks coded with up to `max_fine_bits=12` bits at the same bytes,
89
+ whose top bits are an ordinary fast code at the automatic cap (6 bits, or ceil(bits/dim) when larger). The scan reads
90
+ only those prefixes, then rescores max(32, 1.5·k) candidates exactly. `fine_tiers="auto"` (default) uses them when
91
+ training predicts at least 1 dB lower weighted error; `"on"` forces and `"off"` disables them; `index.fast_prefix_bits`
92
+ is the prefix width (0: none). 100k vectors, 6-8 bits/dim: missed top-10 hits fell 58-76% on Gemini-768 and
93
+ OpenAI-1536 at +3-18% batch time per query (top-100: +11-34%); GloVe-200 predicted no gain and is unchanged. On 1M sets
94
+ (OpenAI-1536, MS MARCO Cohere-1024) top-10 misses fell 46-79% and search got 3-24% faster (likely as the scan reads
95
+ fewer bits); encoding is 11-34% slower (wider codes). Only the flat `TieredQIndex` serves fine-tier models; IVF and
96
+ nested indexes train without them.
97
+ - In C++, use `dv::TieredQIndex(codec)` for fast mode or
98
+ `dv::TieredQIndex(codec, dv::TieredQIndexMode::full)` for full mode.
99
+ Inspect the enum with `index.mode()`; the boolean constructor is removed.
100
+ - The first `add()` needs a representative batch of at least two vectors.
101
+ Inputs must be contiguous float32 NumPy arrays.
102
+ - Queries accept one vector or a matrix. Scores estimate inner products.
103
+ - Fast storage size depends on the model and CPU. Inspect `bytes_per_vector`
104
+ for the representation stride and `storage_bytes` for allocated vector data.
105
+ Fast scans use AVX-512 or AVX2+F16C on x86, and NEON on ARM.
106
+ On AVX-512, single-vector queries with compatible packed-code models use a
107
+ range checkpoint after four tiers to skip uncompetitive suffixes while preserving
108
+ fast-scan IDs and score bits. Bounds are prepared per query and add no stored bits
109
+ per vector. Other layouts, backends, and multi-vector queries use the full scanner.
110
+ - Matrix queries use OpenMP workers. Set `OMP_NUM_THREADS` before starting Python
111
+ to control parallelism.
112
+ - `mode` also steers training. Fast models whose tiers all fit 4 bits use an additive
113
+ 3-bit codebook, so the x86 nibble lookup stores 3-bit tiers at exactly 3 bits.
114
+ This adds +1.2 to +3.6 R@10 points on GloVe-200 at 2.25–3.9 bits/dim, at equal
115
+ storage and speed. It is neutral on SIFT, and DBpedia OpenAI/Gemini at 2–4 bits/dim
116
+ are unchanged. Full mode keeps the Lloyd-Max 3-bit codebook, and either model
117
+ serves in either mode. See [issue #20 results](benchmark/results/issue20-additive-20261029/README.md).
118
+
119
+ See [fast-mode measurements](docs/glove200-fast-scan.md) for accuracy and speed.
120
+ Models with tiers wider than 4 bits now retain original codebook precision in fast mode.
121
+ This improves recall at a throughput cost; existing numerical-level fast indexes
122
+ need rebuilding to recover that precision. Their fast storage keeps codes at tier
123
+ width; files saved with the earlier one-byte-per-coordinate layout still load and are
124
+ repacked. See the [high-bit investigation](docs/high-bit-recall.md).
125
+
126
+ ### IVF and nested search (Python)
127
+
128
+ ```python
129
+ from tieredq import TieredQIVFIndex, TieredQNestedIVFIndex
130
+
131
+ ivf = TieredQIVFIndex(dimensions=d, code_bits=2 * d, lists=1024) # lists=0: one list per codec centroid
132
+ ivf.build(vectors) # trains the codec (seeded 32,768-row sample) and the lists, then indexes
133
+ scores, ids = ivf.search(queries, k=10, nprobe=32)
134
+
135
+ # Prefix codes in an in-RAM IVF; candidates rescored with a 4 bits/dim refinement tier on SSD.
136
+ index = TieredQNestedIVFIndex.build(vectors, prefix_bits=768, total_bits=4 * d, lists=256)
137
+ index.write_tier("tier.rows", vectors) # or index.attach_tier(index.encode_tier(vectors)) to keep it in RAM
138
+ index.attach_tier("tier.rows", reads="direct", queue_depth=32)
139
+ scores, ids = index.search(queries, k=10, nprobe=64, candidates=50)
140
+ index.save("nested.tq") # IVF, refinement and tier layout; attach the tier again after load()
141
+ ```
142
+
143
+ - One `build` trains the prefix, the lists and the refinement from the same seeded sample, so they cannot be
144
+ mismatched. Pass `sample=` to train on earlier data instead; its rows also fix the lists for later builds (as
145
+ `TieredQIVFIndex.train_lists` does).
146
+ - `tier_rows="suffix"` stores a smaller tier and decodes each candidate's prefix from the lists (see the C++ notes).
147
+ - Tier rows kept in another store (a database, an object store): `ids = index.candidates(queries, 50, nprobe=64)`,
148
+ fetch the `encode_tier` rows of those ids (shape `ids.shape + (index.tier_bytes_per_vector,)`; ids of
149
+ `2**32 - 1` are padding), then `scores, ids = index.rescore(queries, ids, rows, k=10)`.
150
+ - `pruning=TieredQPruning(confidence=8)` opts IVF and nested scans into approximate pruning (see below).
151
+
152
+ ## C++
153
+
154
+ ```sh
155
+ cmake -S . -B build
156
+ cmake --build build -j
157
+ OMP_NUM_THREADS=2 ctest --test-dir build --output-on-failure
158
+ ./build/tieredq_example
159
+ cmake --install build --prefix /desired/prefix
160
+ ```
161
+
162
+ Include `<tieredq/tieredq.h>`, use `find_package(TieredQ CONFIG REQUIRED)`, and
163
+ link `TieredQ::TieredQ`. FetchContent also works. See [integration.md](integration.md)
164
+ for layouts, threading, ownership and dependency setup.
165
+
166
+ `dv::TieredQ::train(config, sample, rows, seed)` trains on a contiguous sample. To train on part of a larger
167
+ array without copying it, `dv::TieredQ::train_sampled(config, data, count, sample_ids, sample_rows, seed)` reads
168
+ the named rows in place (the same model). About 8–16K training rows reach full recall on 1M-row sets; fewer than
169
+ ~32 rows per centroid lose recall ([#19.2 results](benchmark/results/issue19-drift-20261031/README.md)).
170
+
171
+ ### IVF (C++)
172
+
173
+ `dv::TieredQIVFIndex` is a fast-mode inverted-file index. With `lists = 0` the codec's k-means centroids
174
+ (at most 256) are the lists, at no extra storage. Any other count trains a separate coarse quantizer, whose
175
+ centroids are stored.
176
+
177
+ ```cpp
178
+ dv::TieredQIVFIndex index(codec, /*lists=*/1024);
179
+ index.train_lists(sample, sample_rows); // optional: fix the lists on a sample; builds then only assign rows
180
+ index.build(rows, count);
181
+ auto hits = index.search(query, /*k=*/10, /*nprobe=*/16);
182
+ // Opt-in approximate pruning for models with more than four tiers (about 2+ bits/dim):
183
+ auto fast = index.search(query, 10, 16, dv::TieredQPruning{/*confidence=*/8, /*prefix_tiers=*/4});
184
+ ```
185
+
186
+ - **Pruning.** By default the scan prunes exactly: hits equal an unpruned scan. A positive `confidence`
187
+ bounds the remaining tiers statistically instead, which is faster and approximate. On OpenAI 100k, confidence 8
188
+ kept recall unchanged (±0.0001) at 1.7× the QPS at 4 bits/dim. With `prefix_tiers = 3` it gave 1.6–2.4× the QPS
189
+ for 0.001–0.003 recall at 2 and 4 bits/dim. Confidence 4 or less loses clearly more recall.
190
+ Platforms: the exact FP32 code scan prunes on AVX-512 only. The nibble byte-table scan (default fast models with
191
+ tiers above 4 bits) prunes on AVX-512, AVX2 and NEON. The scalar fallback validates the setting and scans unpruned.
192
+ By default (`prefix_tiers = 0`, automatic) a nibble scan prunes after all but the last 12.5% of its sections
193
+ (37.5% with a positive confidence), or after the first four tiers where those leave more; an explicit
194
+ `prefix_tiers` prunes after that many tiers. Single-core, 100k documents, 256 lists, on Apple M1 (NEON), EPYC Milan
195
+ (AVX2) and i9-7980XE (AVX-512) with 384-, 768-, 1024- and 1536-d embeddings: exact pruning returned the unpruned
196
+ hits in every run and was 1.0–1.3× faster than unpruned at nprobe 64–256 (about 1.0× at nprobe 16); confidence 8
197
+ was 1.0–1.8× faster with unchanged recall@10. Flat single-query search prunes the same way, 1.0–1.2× faster except
198
+ Gemini-768 at 2 bits/dim, 5–18% slower. Larger suffix shares lose with the exact bound, which loosens faster than it
199
+ saves work.
200
+ To prune through the exact FP32 scan instead, train with `TieredQConfig::target_mode = TieredQIndexMode::full`
201
+ (Python `exact_fast_scores=True`). Measured at 4 bits/dim, before nibble scans were pruned: on OpenAI-1536 100k
202
+ exact with confidence 8 was 4–11% faster than nibble scoring at equal or slightly higher recall, and on OpenAI 1M
203
+ 17–21% faster but 0.4 pt R@10 lower; on Gemini-768 nibble scoring was 20–38% faster, at 0.2 pt R@10 lower. At
204
+ 2 bits/dim nibble scoring was faster on both (#13).
205
+ - **Bit cap.** At about 2 bits/dim or less, training with `TieredQConfig::max_bits = 4` keeps every tier on the
206
+ nibble lookup kernel. That gave 1.7× RaBitQ's IVF QPS at equal recall and memory (OpenAI 100k), for about
207
+ 0.006 recall at full probe. Don't use it at 4 bits/dim, where it loses recall.
208
+ - **Sub-1-bit index + rerank.** With rerank, the in-RAM IVF code can be far below 1 bit/dim. On OpenAI-1M (256
209
+ lists), a 512-bit prefix (627 index bits/vector) with an INT8 rerank tier reached R@10 0.95 / 0.99 at 2.8x / 3.1x
210
+ the QPS of RaBitQ 1 bit (1,683 bits) + INT8.
211
+ - **Nested rerank codes.** `dv::TieredQRefinement` (`<tieredq/tieredq_refinement.h>`) trains a suffix that refines
212
+ the prefix towards a finer model. The suffix is read by id for the scan's candidates and scored with the scan's query
213
+ weights; nesting costs about no recall against a flat code of the full width:
214
+
215
+ `dv::TieredQNestedIVFIndex` (`<tieredq/tieredq_nested.h>`) serves this end to end. The prefix codes stay in the
216
+ in-RAM IVF lists. Each candidate's tier row is read by id from a `TieredQRowSource`: `TieredQMemoryRows` in RAM,
217
+ `TieredQFileRows` with one `pread` per candidate on SSD (`TieredQFileReads::direct` bypasses the page cache), or your
218
+ own store. By default a tier row holds the prefix
219
+ row and the suffix (`TieredQTierRows::prefix_and_suffix`, one contiguous read). `TieredQTierRows::suffix` stores
220
+ only the suffix and decodes each candidate's prefix from the IVF lists (`TieredQIVFIndex::document_rows`). That tier
221
+ is smaller (651 vs 784 B/vector at a 1024-bit prefix, d = 1536) but costs about 3.4 us of decoding per candidate and
222
+ a 4-byte id-to-document map in RAM.
223
+
224
+ ```cpp
225
+ auto refinement = dv::TieredQRefinement::train(codec, /*total_code_bits=*/4 * d, sample, rows, seed);
226
+ dv::TieredQIVFIndex ivf(codec);
227
+ ivf.build(data, n);
228
+ dv::TieredQNestedIVFIndex index(std::move(ivf), std::move(refinement));
229
+ dv::TieredQRowFileWriter writer("tier.rows", index.tier_bytes_per_vector());
230
+ for (size_t first = 0; first < n; first += chunk) { // ids in build order
231
+ index.encode_tier(data + first * d, first, count, buffer); // count = min(chunk, n - first)
232
+ writer.append(buffer, count);
233
+ }
234
+ writer.finish();
235
+ index.attach_tier(std::make_shared<dv::TieredQFileRows>("tier.rows"));
236
+ auto hits = index.search(query, /*k=*/10, /*nprobe=*/32, /*candidates=*/50);
237
+ ```
238
+ - **Flat nested search.** `dv::TieredQNestedIndex` (`<tieredq/tieredq_nested_flat.h>`) scans a narrow prefix for
239
+ every document and rescores the top candidates with an in-RAM suffix tier (a one-list `TieredQNestedIVFIndex`). On
240
+ OpenAI-1536 it is the fast flat path from about 3 bits/dim. A 512-bit prefix with a 3 bits/dim total tier reached
241
+ RaBitQ's 4-bit recall at 2.2x (100K, 50 candidates) and 2.9x (1M, 100 candidates) its single-query QPS, with
242
+ 0.76-0.79x its stored bits. The plain flat index is at 0.54x there.
243
+
244
+ ```cpp
245
+ auto prefix = dv::TieredQ::train(dv::TieredQConfig::target_code_bits(d, 512), sample, rows, seed);
246
+ auto refinement = dv::TieredQRefinement::train(prefix, /*total_code_bits=*/3 * d, sample, rows, seed);
247
+ dv::TieredQNestedIndex index(std::move(prefix), std::move(refinement), data, n);
248
+ auto hits = index.search(query, /*k=*/10, /*candidates=*/100);
249
+ ```
250
+
251
+ `serialize()` stores the IVF, the refinement and the tier layout; after loading, attach the tier again. For a
252
+ tier in an external store, run `index().search_batch` for the candidates and `rescore_batch` with their fetched
253
+ tier rows; it needs no attached tier.
254
+
255
+ At 1M, 512 / 768 bit prefixes + a 4-bit refinement (about 700 B/vector) beat RaBitQ's own 5/6-bit codes by 1.2-2.4x
256
+ QPS at R@10 0.95-0.98 with 28-43% fewer rerank bytes. Refinement recall tops out near 0.984; above that use INT8.
257
+ - **Results** against RaBitQ's IVF at equal measured memory are in
258
+ [issue #19](https://github.com/SeanPedersen/TieredQ/issues/19), [#23](https://github.com/SeanPedersen/TieredQ/issues/23)
259
+ and the [issue #6 eval](benchmark/experiments/issue6_ivf/ivf_eval.cpp)
260
+ ([rerank results](benchmark/results/issue6-ivf-rerank-20261030/README.md)).
261
+
262
+ ## Codec comparison benchmark
263
+
264
+ See [benchmark/README.md](benchmark/README.md) for the one-command CPU comparison
265
+ against official RaBitQ on SIFT1M, GloVe-200 and OpenAI3-1536, including measured
266
+ storage overhead, recall, throughput and latency.
267
+
268
+ Disclaimer: everything but the core idea and the README was created using both Opus 5.5 and GPT 6.1 Sol