[ROCm] Enable seekdb+hipvs on AMD GPU - #1314
Draft
zihaomu wants to merge 13 commits into
Draft
Conversation
Design for an optional GPU ANN backend behind the obvsag adaptor seam
(src/oblib/lib/vector/ob_vsag_adaptor.{h,cpp}), routing build/knn_search to
hipVS's libcuvs_c. Algorithm mapping: HNSW<->CAGRA, IVF-PQ<->cuVS IVF-PQ.
Includes a loose-coupling PoC (poc/cuvs_bridge.c builds CAGRA on GPU over the
same vectors; poc/m1_seekdb_baseline.py + poc/l1_util.py measure recall vs a
shared brute-force ground truth). Measured on AMD Radeon PRO W7900 (gfx1100),
ROCm 7.2.4: cuVS CAGRA recall@10=0.879 @0.006ms/query vs seekdb CPU HNSW/VSAG
0.648 @0.41ms on a 10k x 128 L2-normalized set.
Groundwork for an upstream PR; not yet a source-level backend.
Adds docs/gpu-vector-index-hipvs-cuvs/BUILD.md: a verified recipe to build seekdb from source on Ubuntu 24.04 inside the hipVS/ROCm image (rpm/cpio/bison/flex/libaio, modern Rust via rustup for edition2024, bundled clang-17). Produces a 1.3G seekdb observer binary (OceanBase seekdb 1.4.0.0). Also comments out (#L2SKIP) obshell/ob-deploy/obclient/libobclient in deps/init/oceanbase.el9.x86_64.deps -- ENVIRONMENT-SPECIFIC build workaround for a network that cannot reach the internal ob-yum host / where client RPMs 404; these are client/deploy tools, not build deps. Drop for internal environments.
…_cagra_knn) [L2-B]
Adds obvsag::cuvs_cagra_knn in ob_vsag_adaptor (declared in .h, defined in .cpp)
that forwards to a thin C bridge (docs/.../poc/seekdb_cuvs_bridge.{c,h}) which runs
cuVS CAGRA on the GPU via hipVS libcuvs_c. src/oblib/lib/CMakeLists.txt (x86_64) links
the bridge .so. Verified: the seekdb observer binary defines obvsag::cuvs_cagra_knn,
references the bridge symbol, and DT_NEEDED libseekdb_cuvs_bridge.so -> libcuvs (hipVS).
PoC/mechanical integration only: the bridge .so path is hardcoded and the backend is
not yet wired into the runtime build_index/knn_search path. AMD gfx1100 / ROCm 7.2.4.
…time [L2-B step3]
Wire a GPU CAGRA data path into seekdb's real vector adaptor
(oceanbase::common::obvsag), gated by env OB_VSAG_USE_CUVS=1:
- build_index: also builds a cuVS CAGRA index from the base vectors on the GPU
(via libseekdb_cuvs_bridge -> hipVS libcuvs_c), keyed by the index handle.
- knn_search: when a GPU index exists, serves top-k from the GPU; results are
allocated with the handle's allocator (drop-in with the VSAG path).
- delete_index: releases the GPU index. Thread-safety: single writer per index
(PoC). If the GPU build fails, it silently falls back to CPU VSAG.
Bridge gains build-once/search-many (seekdb_cuvs_build/search/free) so the index
is built once, not per query.
Verified end-to-end through the real obvsag C-API (harness links the actual
liboblib.a via the observer link recipe), same 10000x128 data, topk=10:
flag OFF -> VSAG HNSW (CPU), recall@10 = 0.9260
flag ON -> cuVS CAGRA on gfx1100, recall@10 = 0.8790 (rocm-smi: VRAM
29MB->829MB, GPU use up to 22%)
flag ON, GPU hidden -> cuVS build fails -> falls back to VSAG, recall = 0.9260
PoC scope: exercises the exact adaptor C-API the SQL/storage layer calls; it does
not yet bootstrap a full observer + SQL. The bridge .so path is env-specific.
AMD gfx1100 / ROCm 7.2.4.
…ndex hook [L2-B step5]
Phase 0 tracing showed plain-HNSW builds BOTH its delta and snapshot indexes via
obvsag::add_index (one row at a time) and queries via knn_search's simple overload;
build_index is never called (only the HNSW_SQ bulk path uses it). So the earlier
build_index-based cuVS hook could never fire for a normal SQL query.
This moves the GPU path to where the data actually flows:
- add_index: buffer the incoming vectors per index handle.
- knn_search: lazily (re)build a cuVS CAGRA index from the buffer once it has
>= 256 points (or grew >= 2x), then serve top-k from the GPU; fall back to VSAG
otherwise. Results allocated with the handle's allocator (drop-in with VSAG).
- cuVS build/search run on a dedicated 32MB-stack pthread: OB worker threads have
a ~1.5MB stack and cuVS CAGRA graph-build overflows it (observed crash).
Adds an env-gated file tracer (OB_VSAG_TRACE) used to establish the call chain.
Verified end-to-end through a real single-node observer (OB_VSAG_USE_CUVS=1):
CREATE vector(128) type=hnsw + INSERT 500 rows + SELECT ... l2_distance(..)
APPROXIMATE LIMIT 10 -> served by cuVS on gfx1100 (rocm-smi VRAM 28MB->575MB,
cuvs_serve x41), recall 100% vs exact brute force, observer stable.
PoC scope: data in the delta index (uncompacted); snapshot reload via fdeserialize
not yet hooked (restart -> VSAG until re-add); one pthread per query; buffer copies
vectors. Env-gated; default behavior unchanged. AMD gfx1100 / ROCm 7.2.4.
…he GPU path [M-A] The cuVS GPU path returned UNFILTERED top-k, which is wrong for queries with a WHERE predicate or with deleted rows (seekdb passes a non-null row bitmap for *every* query, so presence of a filter object cannot be used to detect this). Fix: post-filter the cuVS top-k using the same semantics VSAG applies (reverse_filter ? filter->test(vid) : !filter->test(vid)); if any of the top-k is excluded, fall back to CPU VSAG (the true filtered top-k may rank beyond k in the unfiltered order). Unfiltered queries still serve from the GPU. Verified on a live observer: unfiltered -> cuVS (correct); WHERE c1<100 -> fallback (correct); after DELETE -> fallback, deleted rows absent (correct); observer stable.
The cuVS bridge builds CAGRA as L2, so IP/cosine indexes would get a wrong ordering (which the row post-filter cannot catch). Gate both add_index buffering and knn_search serving on get_metric()=="l2"; IP/cosine fall back to CPU VSAG. Verified: cosine index -> no cuVS build/serve, correct results; L2 unaffected.
…ess) [M-B B2] Between cuVS rebuilds, newly added rows were only in VSAG, but the query still served the (stale) cuVS index -> could miss recent inserts. Now knn_search serves from the GPU only when built_n_ == current buffer size; otherwise it falls back to CPU VSAG (always fresh). This also yields the intended split: streaming delta -> VSAG, stable snapshot -> cuVS. Verified interleaved insert+query: build/serve when current, VSAG fallback while growing (<2x), rebuild+serve on 2x growth; APPROX==EXACT at every step.
…GPU call) [batch-op PoC] seekdb similarity JOIN (LATERAL correlated ANN) works but nested-loops: N probes => N separate knn_search (traced). Single-query GPU has no win over CPU HNSW (C3), but BATCH does: obvsag cuvs_knn_search_batch feeds nq probes to ONE cuVS call, reusing the add_index buffer + registry + 32MB pthread, mapping row offsets back to vids. Harness (real obvsag adaptor, 10k+100): per-query 470 probes/s vs batch 54777 probes/s = 116.7x, recall identical 0.873, batch-vs-loop ids 1000/1000. cuVS OFF -> served=0 safe fallback. Bridge sweep: nq=5000 -> 263x, 635k probes/s.
…B PoC] New PL sys-package procedure dbms_vector.batch_knn(index_table, probe_table, topk, out_table): reads probe+index vectors via inner SQL (vector LOB -> raw float decode), builds one CAGRA, runs ONE GPU batch search (new obvsag::cuvs_batch_knn one-shot on a 32MB pthread), writes neighbors to out_table. mysql mode has no pipelined/table functions, so results go to an output table. Wiring mirrors rebuild_index (spec/body PRAGMA INTERFACE + ob_pl_interface_pragma.h + DECLARE_FUNC + impl). Verified (t10k=10000, probes_q=100, cuVS ON): call rc=0, wrote 1000 rows, recall@10 =0.8690 (identical to bridge/harness batch), trace=1 cuvs_raw_batch (one GPU call). Amortization per-probe 3.325ms(nq100)->0.165ms(nq4000), marginal ~0.084ms/probe vs nested-loop VSAG 0.187ms/probe. Note: syspack .sql embedded at build -> rebuild + fresh bootstrap required to create the procedure.
…p [harden 1-3] Step 1: remove *.batchbak backups; restore deps/init .deps to upstream (move the internal-host skip to a repro note, do not ship #L2SKIP); replace the hardcoded /work/bridge/*.so link with a CMake option. Step 2.1: add option OB_BUILD_CUVS (OFF default) + CUVS_BRIDGE_LIB in cmake/Env.cmake; conditional target_link_libraries; guard the extern bridge decls with inert static stubs when OFF so oblib links without the .so, and ob_cuvs_enabled() returns false when OFF (cuVS never runs -> behavior identical to upstream VSAG). Step 3: gate the ob_vsag_trace call-chain tracer behind OB_BUILD_CUVS_TRACE (no-op in normal builds, no file I/O on the hot path). Validated: -DOB_BUILD_CUVS=ON links libseekdb_cuvs_bridge.so (BUILD_RC=0); default OFF builds clean with no bridge dependency (ldd clean, no undefined refs).
… 2.2+4.1] Replaces the OB_VSAG_USE_CUVS env hijack with a declarative per-index opt-in: - DDL: add ObVectorIndexAlgorithmLib::VIAL_CUVS + accept lib=cuvs in the param parser and both resolver validations (WITH-option allow-list + hnsw lib check). - obvsag: per-handle mark set (g_ob_cuvs_marked) + mark_cuvs_index/unmark_cuvs_index; the add_index/knn_search/delete hooks gate on ob_cuvs_marked(handle) instead of the env var; ob_cuvs_enabled() (used only by dbms_vector.batch_knn) becomes a compile-time OB_BUILD_CUVS check. env fully removed. - plugin: ob_mark_cuvs_if_needed(algo_data_, handle) marks a handle for the GPU path when the index is lib=cuvs + L2 + HNSW; called before the incr/snap add_index. - cmake: wire the developer OB_BUILD_CUVS_TRACE flag (add_definitions). Verified (no OB_VSAG_USE_CUVS in env): lib=cuvs index -> cuvs_serve on the GPU; lib=vsag index -> knn_simple only (VSAG/CPU). Per-index opt-in works declaratively. Note: DDL lib validation lives in 3 places (param parse + 2 resolver checks).
…harden 4.2] vector_index_cuvs_ddl.test/.result: CI-safe DDL contract (no GPU) -- lib=cuvs is accepted for dense L2 HNSW, rejected for IVF (requires lib=OB). Verified live. cuvs_gpu_smoke.sql: GPU-required runtime smoke (lib=cuvs -> cuvs_serve, lib=vsag -> VSAG) for manual runs; not in CI (needs gfx1100 + OB_BUILD_CUVS=ON). Note: .result hand-authored to mysqltest convention; regenerate via --record when wired into the harness.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
This PR lets seekdb run approximate nearest-neighbor (ANN) vector search on AMD GPUs
(gfx1100 / RDNA3) through hipVS (AMD's port of NVIDIA cuVS), while keeping the existing
CPU (VSAG) engine as the default and the safe fallback.
OB_BUILD_CUVS=OFF) -> no new dependency, behavior identical to upstream.CREATE VECTOR INDEX ... WITH (..., lib=cuvs).CALL dbms_vector.batch_knn(index_tbl, probe_tbl, k, out_tbl).1. How it works (support principle)
seekdb's vector index is backed by VSAG (CPU HNSW). All vector operations flow through the
obvsagC-API insrc/oblib/lib/vector/ob_vsag_adaptor.*(
create_index/add_index/knn_search/delete_index). We hook that seam.1) Bridge (isolation layer).
libseekdb_cuvs_bridge.sois a thin C shim that exposes plainsymbols (
seekdb_cuvs_build/search/free, plus a one-shot batch entry) and internally runs a cuVSCAGRA graph on the GPU via hipVS
libcuvs_c. It hides all ROCm/cuVS headers from seekdb'scompiler (seekdb builds with a bundled clang; cuVS needs hipcc), so seekdb only ever sees C symbols.
2) Adaptor hooks. For an index that opted into the GPU path:
add_indexbuffers the incoming vectors per index handle.knn_searchlazily builds a CAGRA graph from that buffer (>= 256 points) and serves top-kfrom the GPU. The build + search run on a dedicated 32 MB-stack pthread because a cuVS graph
build overflows the small (~1.5 MB) OB worker-thread stack.
semantics VSAG uses; if any of the k rows is excluded, we fall back to VSAG (the true filtered
top-k may rank beyond k).
since the last build, we serve from VSAG (always fresh). This yields the split
streaming delta -> VSAG, stable snapshot -> GPU.
3) Declarative per-index opt-in (
lib=cuvs). We addObVectorIndexAlgorithmLib::VIAL_CUVSandaccept
lib=cuvsin the DDL. The storage plugin (ob_plugin_vector_index_adaptor) marks theindex handle for the GPU when the index is
lib=cuvs+ L2 + HNSW, and the adaptor hooks gate on thatper-handle mark -- not a global switch. So only indexes that declare
lib=cuvsuse the GPU;every other index stays on VSAG in the same process.
4) Batch operator (
dbms_vector.batch_knn). A PL system-package procedure that reads the probeand index vectors via SQL, builds one CAGRA, issues one batched GPU search for all probes, and
writes neighbors to an output table (mysql-mode PL has no pipelined table functions, hence the
output table).
5) Build gating.
OB_BUILD_CUVS(CMake option, OFF by default) controls everything. When OFF thebridge
.sois not linked (inert stubs satisfy the linker) and the GPU code never runs.Why this design -- the key finding
We benchmarked end-to-end and found that single-query SQL gets no speedup on GPU: a well-built
CPU HNSW is already sub-millisecond, and the per-call GPU overhead (device alloc + PCIe copy + kernel
launch + sync) erases the edge. Batched ANN, on the other hand, wins by ~100-260x. That is why
the GPU value is exposed two ways -- transparent per-index acceleration for higher recall, and an
explicit batch operator for the workload where GPU decisively wins (similarity joins / bulk scoring).
2. Measured performance (AMD Radeon PRO W7900, gfx1100, ROCm 7.2.4)
Single vs batch (this is the whole story):
cuvs_knn_search_batch, real adaptor, 10k index / 100 probes):per-query 470 q/s -> batch 54,777 q/s = 116.7x, recall identical (0.873), neighbors
bit-identical (1000/1000).
dbms_vector.batch_knn(SQL, 10k / 100 probes): recall@10 0.8690 (identical to the bridgebatch), one GPU call; per-probe cost amortizes 3.3 ms -> 0.165 ms as batch size grows.
lib=cuvsquery recall@10 0.89 vs ground truth;WHERE/DELETE/ cosine all safely fall back to VSAG with correct results.lib=cuvsindexserves on the GPU (
cuvs_serve) while alib=vsagindex stays on VSAG (knn_simple, 0 GPU calls).3. How to use it
Build with the backend on:
Runtime: make the bridge and
libcuvsreachable (LD_LIBRARY_PATH=/path/to/bridge:/opt/hipvs/lib,or install +
ldconfig). Requires an AMD gfx1100 GPU.A) Declarative per-index GPU acceleration -- just add
lib=cuvs:B) Batched ANN operator -- one GPU call for many probes:
The bridge
libseekdb_cuvs_bridge.sois built out-of-tree from hipVS(
hipvs-compile -shared -fPIC seekdb_cuvs_bridge.c); seedocs/gpu-vector-index-hipvs-cuvs/for the source, the reproduction steps and the smoke tests.4. What's in this PR
OB_BUILD_CUVS(+CUVS_BRIDGE_LIB, + developer-onlyOB_BUILD_CUVS_TRACE).ob_vsag_adaptor.*(per-index mark, lazy CAGRA build/serve, safe fallback,batch seam, one-shot raw batch).
VIAL_CUVS+lib=cuvsaccepted for dense L2 HNSW (rejected for IVF).lib=cuvsindex handles for the GPU path.dbms_vector.batch_knnPL procedure.When
OB_BUILD_CUVS=OFF(default): the bridge is not linked, the GPU code never runs, and theruntime behavior is identical to upstream VSAG.
5. Not included / follow-ups
fdeserializesnapshot reload(currently a reloaded snapshot falls back to VSAG until the next in-process rebuild).
lib=cuvswith a non-L2 distance at DDL time; add a_enable_gpu_vector_indexglobal kill-switch.#ifdef-out the OFF-path code (today it is compiled but gated by aninert stub + a runtime-false check).
.resultvia--recordand add a GPU-present skip guard for theruntime tests; the GPU behavior test needs a GPU runner.
batch_knn: cache the built index across calls so repeated calls skip the rebuild.(CK backend); the current bridge is dynamic-only and correctness-validated, not perf-tuned.
cc @zhangnju
Task Description
Solution Description
Passed Regressions
Upgrade Compatibility
Other Information
Release Note