Skip to content

Commit ec8a78e

Browse files
richyreachyiaojnh
andauthored
refactor(diskann): decouple from libaio via dlopen (#532)
Co-authored-by: Zefeng Yin <yinzefeng.yzf@alibaba-inc.com>
1 parent 23538ab commit ec8a78e

49 files changed

Lines changed: 1108 additions & 809 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/03-macos-linux-build.yml

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,6 @@ jobs:
6060
sudo apt-get install -y clang libomp-dev
6161
shell: bash
6262

63-
- name: Install AIO
64-
if: runner.os == 'Linux' && runner.arch == 'X64'
65-
run: |
66-
sudo apt-get update
67-
sudo apt-get install -y --no-install-recommends \
68-
libaio-dev
69-
shell: bash
70-
7163
- name: Print CPU info
7264
if: runner.os == 'Linux'
7365
run: lscpu
@@ -183,3 +175,41 @@ jobs:
183175
./c_api_index_example
184176
./c_api_optimized_example
185177
shell: bash
178+
179+
# ------------------------------------------------------------------ #
180+
# DiskAnn libaio round: install libaio and re-run tests
181+
# ------------------------------------------------------------------ #
182+
- name: Install libaio runtime
183+
if: matrix.platform == 'linux-x64'
184+
run: |
185+
sudo apt-get update -y
186+
# libaio1t64 on Ubuntu 24.04+ (t64 transition), libaio1 on older
187+
sudo apt-get install -y libaio1t64 || sudo apt-get install -y libaio1
188+
echo "=== libaio status (should be present) ==="
189+
dpkg -l | grep -i libaio || true
190+
ldconfig -p | grep libaio || true
191+
shell: bash
192+
193+
- name: Run DiskAnn C++ Tests (w/ libaio)
194+
if: matrix.platform == 'linux-x64'
195+
run: |
196+
cd "$GITHUB_WORKSPACE/build"
197+
ctest -R diskann --output-on-failure --parallel $NPROC
198+
shell: bash
199+
200+
- name: Run DiskAnn Python Tests (w/ libaio)
201+
if: matrix.platform == 'linux-x64'
202+
run: |
203+
cd "$GITHUB_WORKSPACE"
204+
python -m pytest python/tests/test_collection_diskann.py -v
205+
shell: bash
206+
207+
# Verify installing libaio does not affect existing non-DiskAnn tests.
208+
- name: Run HNSW Tests (w/ libaio)
209+
if: matrix.platform == 'linux-x64'
210+
run: |
211+
cd "$GITHUB_WORKSPACE/build"
212+
ctest -R hnsw --output-on-failure --parallel $NPROC
213+
cd "$GITHUB_WORKSPACE"
214+
python -m pytest python/tests/test_hnsw_contiguous_memory.py -v
215+
shell: bash

.github/workflows/08-cmake-subproject-integration.yml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,6 @@ jobs:
3434
cache: 'pip'
3535
cache-dependency-path: 'pyproject.toml'
3636

37-
- name: Install system dependencies
38-
run: |
39-
sudo apt-get update
40-
sudo apt-get install -y --no-install-recommends \
41-
libaio-dev
42-
shell: bash
43-
4437
- name: Set up environment variables
4538
run: |
4639
NPROC=$(nproc 2>/dev/null || echo 2)

.github/workflows/clang_tidy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
if: steps.changed_files.outputs.any_changed == 'true'
4848
run: |
4949
sudo apt-get update
50-
sudo apt-get install -y clang-tidy=1:18.0-59~exp2 cmake ninja-build libomp-dev libaio-dev
50+
sudo apt-get install -y clang-tidy=1:18.0-59~exp2 cmake ninja-build libomp-dev
5151
5252
- name: Setup ccache
5353
if: steps.changed_files.outputs.any_changed == 'true'

.github/workflows/nightly_coverage.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ jobs:
4848
- name: Install system dependencies
4949
run: |
5050
sudo apt-get update
51-
sudo apt-get install -y --no-install-recommends \
52-
lcov libaio-dev
51+
sudo apt-get install -y --no-install-recommends lcov
52+
sudo apt-get install -y libaio1t64 || sudo apt-get install -y libaio1
5353
shell: bash
5454

5555
- name: Install dependencies

CMakeLists.txt

Lines changed: 4 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ else()
122122
endif()
123123
message(STATUS "RABITQ_ARCH_FLAG: ${RABITQ_ARCH_FLAG}")
124124

125-
# DiskAnn support (Linux x86_64 only, requires libaio)
125+
# DiskAnn support (Linux x86_64 only; libaio loaded at runtime via dlopen)
126126
if(CMAKE_SYSTEM_NAME STREQUAL "Linux" AND CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|i686|i386" AND NOT ANDROID AND NOT IOS)
127127
set(DISKANN_SUPPORTED ON)
128128
add_definitions(-DDISKANN_SUPPORTED=1)
@@ -180,24 +180,9 @@ if(BUILD_PYTHON_BINDINGS)
180180
install(TARGETS _zvec LIBRARY DESTINATION ${ZVEC_PY_INSTALL_DIR}/zvec
181181
COMPONENT python)
182182

183-
# DiskAnn ships as a runtime-loaded shared module
184-
# (libzvec_diskann_plugin.so) that is brought online implicitly the
185-
# first time a DiskAnn index is created — users never call any load
186-
# function. The Python extension resolves the module next to _zvec.so
187-
# (see the $ORIGIN rpath in src/binding/python/CMakeLists.txt); the
188-
# module must therefore be installed alongside _zvec.so, i.e. inside the
189-
# zvec package directory as well.
190-
#
191-
# Gate on DISKANN_SUPPORTED, not on the target's existence: on unsupported
192-
# platforms (e.g. macOS / ARM64) the core_knn_diskann target is still
193-
# defined, but built from an empty stub (src/core/algorithm/CMakeLists.txt)
194-
# with zero exported symbols and a runtime load path compiled out
195-
# (#if DISKANN_SUPPORTED). Shipping that stub is pure dead weight, so it is
196-
# only packaged where DiskAnn is real — currently Linux x86_64 with libaio.
197-
if(DISKANN_SUPPORTED)
198-
install(TARGETS core_knn_diskann LIBRARY DESTINATION ${ZVEC_PY_INSTALL_DIR}/zvec
199-
COMPONENT python)
200-
endif()
183+
# DiskAnn is now statically linked into _zvec.so via --whole-archive
184+
# (see src/binding/python/CMakeLists.txt -> core_knn_diskann_static),
185+
# so no separate runtime .so needs to be installed in the wheel.
201186
# Bundle cppjieba's dictionary files so the `jieba` FTS tokenizer works
202187
# out of the box. python/zvec/__init__.py resolves this directory via
203188
# importlib.resources and registers it with set_default_jieba_dict_dir().

examples/c++/CMakeLists.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ get_filename_component(ZVEC_ROOT_DIR "${CMAKE_CURRENT_LIST_DIR}/../.." ABSOLUTE)
1717
set(ZVEC_INCLUDE_DIR ${ZVEC_ROOT_DIR}/src/include)
1818
set(ZVEC_LIB_DIR ${ZVEC_ROOT_DIR}/${HOST_BUILD_DIR}/lib)
1919

20-
# Add include and library search paths
2120
include_directories(${ZVEC_INCLUDE_DIR})
2221
set(ZVEC_LIB_SEARCH_DIRS ${ZVEC_LIB_DIR})
2322

pyproject.toml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,6 @@ test-command = "cd {project} && pytest python/tests -v --tb=short"
181181
build-verbosity = 1
182182

183183
[tool.cibuildwheel.linux]
184-
# libaio is required by the C++ backend; install it inside the manylinux
185-
# container (manylinux_2_28 is AlmaLinux 8 based, so use dnf/libaio-devel).
186-
# libaio-devel lives in BaseOS; disable EPEL so a flaky EPEL mirror cannot
187-
# break metadata refresh.
188-
before-all = "dnf install -y --disablerepo=epel libaio-devel"
189184
archs = ["auto"]
190185
environment = { CMAKE_GENERATOR = "Unix Makefiles", CMAKE_BUILD_PARALLEL_LEVEL = "16" }
191186
manylinux-x86_64-image = "manylinux_2_28"

python/tests/detail/fixture_helper.py

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,14 @@
1313
from zvec.typing import DataType, StatusCode, MetricType, QuantizeType
1414
import zvec
1515

16-
17-
# Cache the DiskAnn plugin preload status so we pay the load cost once per
18-
# test session. The plugin normally auto-loads on first DiskAnn use, but we
19-
# preload it explicitly here so a missing libaio / misplaced plugin .so
20-
# surfaces as a clear pytest skip instead of a confusing
21-
# "Create vector column indexer failed" deep inside the collection code path.
2216
_DISKANN_PRELOAD_REASON: str | None = None
2317
_DISKANN_PRELOAD_DONE: bool = False
2418

2519

2620
def _ensure_diskann_runtime_or_reason() -> str | None:
27-
"""Preload the DiskAnn plugin and return None on success or a human-readable
28-
skip reason on failure. Idempotent across calls."""
21+
"""Check whether DiskAnn is available on this platform and return None
22+
on success or a human-readable skip reason on failure. Idempotent across
23+
calls."""
2924
global _DISKANN_PRELOAD_DONE, _DISKANN_PRELOAD_REASON
3025
if _DISKANN_PRELOAD_DONE:
3126
return _DISKANN_PRELOAD_REASON
@@ -35,22 +30,6 @@ def _ensure_diskann_runtime_or_reason() -> str | None:
3530
_DISKANN_PRELOAD_REASON = "DiskAnn only supported on Linux x86_64"
3631
return _DISKANN_PRELOAD_REASON
3732

38-
if not zvec.is_libaio_available():
39-
_DISKANN_PRELOAD_REASON = (
40-
"libaio is not available on this host; DiskAnn cannot run. "
41-
"Install libaio1 (or libaio1t64 on Ubuntu 24.04+) and retry."
42-
)
43-
return _DISKANN_PRELOAD_REASON
44-
45-
status = zvec.load_diskann_plugin()
46-
if status != zvec.DISKANN_PLUGIN_OK:
47-
_DISKANN_PRELOAD_REASON = (
48-
f"Failed to load DiskAnn plugin (status={status}); "
49-
"check that libzvec_diskann_plugin.so is installed alongside "
50-
"_zvec.so in the Python site-packages directory."
51-
)
52-
return _DISKANN_PRELOAD_REASON
53-
5433
_DISKANN_PRELOAD_REASON = None
5534
return None
5635

@@ -148,8 +127,7 @@ def full_schema_new(request) -> CollectionSchema:
148127
else:
149128
nullable, has_index, vector_index = True, False, HnswIndexParam()
150129

151-
# Skip DiskAnn tests on unsupported platforms or when the runtime cannot
152-
# be brought up (missing libaio, plugin .so not installed, etc.).
130+
# Skip DiskAnn tests on unsupported platforms.
153131
from zvec.model.param import DiskAnnIndexParam
154132

155133
if isinstance(vector_index, DiskAnnIndexParam):

python/tests/test_collection_diskann.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,15 @@
1313
# limitations under the License.
1414
"""End-to-end collection tests for the DiskAnn index.
1515
16-
Mirrors ``test_collection_hnsw_rabitq.py`` but targets the DiskAnn plugin.
16+
Mirrors ``test_collection_hnsw_rabitq.py`` but targets the DiskAnn index.
1717
1818
Two platform-level prerequisites are enforced at module import time:
1919
2020
1. DiskAnn is currently built only for Linux x86_64 — other platforms are
2121
skipped wholesale.
22-
2. The DiskAnn backend lives in a *runtime-loaded* plugin
23-
(``libzvec_diskann_plugin.so``). It must be loaded with ``RTLD_GLOBAL |
24-
RTLD_NOW`` BEFORE ``import zvec`` so that the plugin's ``IndexFactory``
25-
singleton is unified with the one inside ``_zvec.so``. After ``import
26-
zvec`` we must also call ``zvec.load_diskann_plugin()`` exactly once.
22+
2. libaio is loaded eagerly (via dlopen) inside DiskAnnBuilder::init() /
23+
DiskAnnStreamer::init(). If libaio is missing, DiskAnn falls back to
24+
synchronous pread() — the tests still run but with degraded performance.
2725
2826
If either prerequisite fails the whole module is skipped so the rest of the
2927
test-suite is not affected.
@@ -32,7 +30,6 @@
3230
from __future__ import annotations
3331

3432
import math
35-
import os
3633
import platform
3734
import sys
3835

@@ -46,13 +43,6 @@
4643
reason="DiskAnn plugin is only supported on Linux x86_64",
4744
)
4845

49-
# Promote all symbols in subsequently-loaded DSOs to the global namespace and
50-
# resolve relocations eagerly. This is REQUIRED so the DiskAnn plugin can see
51-
# the ``IndexFactory`` singleton that lives in ``_zvec.so`` and vice versa.
52-
# See: DiskAnn RTLD_GLOBAL + RTLD_NOW Requirement.
53-
if sys.platform == "linux":
54-
sys.setdlopenflags(sys.getdlopenflags() | os.RTLD_GLOBAL | os.RTLD_NOW)
55-
5646
import zvec # noqa: E402
5747

5848
from zvec import ( # noqa: E402

python/tests/test_typing.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from zvec import (
1818
DataType,
1919
IndexType,
20+
IOBackendType,
2021
MetricType,
2122
QuantizeType,
2223
Status,
@@ -32,6 +33,7 @@
3233
[
3334
(DataType.FLOAT, "FLOAT"),
3435
(IndexType.HNSW, "HNSW"),
36+
(IOBackendType.PREAD, "PREAD"),
3537
(MetricType.COSINE, "COSINE"),
3638
(QuantizeType.INT8, "INT8"),
3739
(StatusCode.OK, "OK"),
@@ -46,6 +48,7 @@ def test_enum_names(member, name):
4648
[
4749
(DataType.FLOAT, 8),
4850
(IndexType.HNSW, 1),
51+
(IOBackendType.PREAD, 0),
4952
(MetricType.COSINE, 3),
5053
(QuantizeType.INT8, 2),
5154
(StatusCode.OK, 0),
@@ -91,11 +94,29 @@ def test_data_type_has_member(member):
9194
assert member in DataType.__members__
9295

9396

94-
@pytest.mark.parametrize("member", ["HNSW", "IVF", "FLAT", "INVERT"])
97+
@pytest.mark.parametrize(
98+
"member",
99+
[
100+
"UNDEFINED",
101+
"HNSW",
102+
"IVF",
103+
"FLAT",
104+
"HNSW_RABITQ",
105+
"DISKANN",
106+
"VAMANA",
107+
"INVERT",
108+
"FTS",
109+
],
110+
)
95111
def test_index_type_has_member(member):
96112
assert member in IndexType.__members__
97113

98114

115+
@pytest.mark.parametrize("member", ["PREAD", "LIBAIO"])
116+
def test_io_backend_type_has_member(member):
117+
assert member in IOBackendType.__members__
118+
119+
99120
@pytest.mark.parametrize("member", ["FP16", "INT8", "INT4", "UNDEFINED"])
100121
def test_quantize_type_has_member(member):
101122
assert member in QuantizeType.__members__

0 commit comments

Comments
 (0)