Skip to content

feat: expose the DiskANN I/O backend (#224) - #230

Merged
s2x merged 2 commits into
mainfrom
feat/224-diskann-io-backend
Sep 29, 2026
Merged

s2x merged 2 commits into
mainfrom
feat/224-diskann-io-backend

Conversation

@s2x

@s2x s2x commented Sep 28, 2026

Copy link
Copy Markdown
Member

Closes #224.

Summary

zvec picks an I/O backend for DiskANN disk reads on first use. On Linux it tries io_uring, then libaio, then falls back to synchronous pread(); macOS arm64 always uses pread(). The choice dominates DiskANN throughput — pread on Linux usually just means libaio is not installed — and it was not observable from PHP at all.

ZVec::IO_BACKEND_PREAD    = 0;
ZVec::IO_BACKEND_LIBAIO   = 1;
ZVec::IO_BACKEND_IO_URING = 2;

ZVec::getIoBackendType(): int
ZVec::getIoBackendTypeName(int $type): string   // "pread" | "libaio" | "io_uring" | "unknown"
ZVec::getIoBackendDescription(): string         // install hints on Linux when pread

Design notes

Static, not per-collection. The value is process-wide, it is not part of GlobalConfig (upstream config.h has no io-backend accessor), and upstream probes it lazily — so the getters work without ZVec::init(), which the test asserts. This mirrors the Python SDK's module-level zvec.io_backend_type() and the existing version API.

The #215 singleton rule holds. The adapter includes only the public zvec/ailego/io/io_backend.h and calls upstream's exported current_io_backend_type() / current_io_backend_description(), which are compiled inside libzvec, so the IOBackend singleton is created only there. The internal io_backend_def.h is not part of the SDK, and IOBackend::Instance() is never referenced from this module. There is a comment in ffi/zvec_ffi.cc explaining why, so it does not get "fixed" later.

$ nm -C ffi/build/libzvec_ffi.so | grep -c 'IOBackend::Instance'
0
$ nm -D --defined-only ffi/build/libzvec_ffi.so | grep io_backend
zvec_get_io_backend_description
zvec_get_io_backend_type
zvec_get_io_backend_type_name

No status handling needed. These return plain values rather than zvec_status_t, so self::checkStatus() is not involved, matching getVersion(). The C side owns the memory: a static literal for the name, a thread_local buffer for the description, so no FFI::free().

The name mapping is written as a local switch in the adapter rather than pulled from upstream's internal header, returning "unknown" for any unrecognised value.

Verification

$ php -n -d extension=ffi.so -r 'require "src/ZVec.php";
    $t = ZVec::getIoBackendType();
    echo $t, " ", ZVec::getIoBackendTypeName($t), "\n", ZVec::getIoBackendDescription(), "\n";'
2 io_uring
io_uring async I/O backend (raw kernel syscalls, zero depend...

Full suite from a clean build:

Number of tests : 192               192
Tests skipped   :   0 (  0.0%)
Tests failed    :   0 (  0.0%)
Expected fail   :   2 (  1.0%)
Tests passed    : 190 ( 99.0%)

tests/test_io_backend.phpt covers the constant values, the name mapping including the unknown fallback, that the value is valid before init(), that the description mentions the reported backend, the macOS pread rule, and that the cached value is stable across repeated calls and across init(). tests/test_ffi_load.phpt gained the three symbols (60 → 63).

test_dbs/ is left with only .gitignore.

🤖 Generated with Claude Code

s2x added 2 commits September 28, 2026 20:29
zvec picks an I/O backend for DiskANN disk reads on first use: on Linux
it tries io_uring, then libaio, then falls back to synchronous pread();
macOS arm64 always uses pread(). The choice dominates DiskANN
throughput -- pread on Linux usually means libaio simply is not installed
-- and until now there was no way to observe it from PHP.

Three static getters on ZVec, next to getVersion() and following the
same pattern the Python SDK uses for its module-level zvec.io_backend_type():

    ZVec::getIoBackendType(): int
    ZVec::getIoBackendTypeName(int $type): string
    ZVec::getIoBackendDescription(): string

plus IO_BACKEND_PREAD / IO_BACKEND_LIBAIO / IO_BACKEND_IO_URING (0/1/2),
the same values as the upstream C ABI. The description is where upstream
explains how to install an async backend when pread is in use.

These are deliberately static rather than per-collection: the value is
process-wide, it is not part of GlobalConfig, and upstream probes it
lazily, so the getters work without ZVec::init() at all. The test asserts
that.

Adhering to the #215 singleton rule: the adapter includes only the public
zvec/ailego/io/io_backend.h and calls upstream's exported
current_io_backend_type() / current_io_backend_description(), which are
compiled inside libzvec, so the IOBackend singleton is created only there.
The internal io_backend_def.h is not part of the SDK and IOBackend::Instance()
is never referenced from this module. The comment in ffi/zvec_ffi.cc records
why, so it does not get "fixed" later.

    $ nm -C ffi/build/libzvec_ffi.so | grep -c 'IOBackend::Instance'
    0
    $ nm -D --defined-only ffi/build/libzvec_ffi.so | grep io_backend
    zvec_get_io_backend_description
    zvec_get_io_backend_type
    zvec_get_io_backend_type_name

No status to check: these return plain values, matching getVersion(), so
self::checkStatus() is not involved and FFI::free() is not needed -- the
C side owns the memory (a static literal for the name, a thread-local
buffer for the description).

Tests: 192/192, 0 skipped, 0 failed, 2 expected fail. The new
tests/test_io_backend.phpt covers the constants, the name mapping including
the "unknown" fallback, that the value is valid before init(), that the
description mentions the reported backend, the macOS pread rule, and that
the cached value is stable across repeated calls and across init().
tests/test_ffi_load.phpt gained the three symbols (60 -> 63).
@s2x
s2x merged commit ffd6af4 into main Sep 29, 2026
6 checks passed
@s2x
s2x deleted the feat/224-diskann-io-backend branch September 29, 2026 07:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: expose DiskANN I/O backend (ZVec::getIoBackendType / getIoBackendDescription)

1 participant