Skip to content

[ROCm] Enable seekdb+hipvs on AMD GPU - #1314

Draft
zihaomu wants to merge 13 commits into
oceanbase:developfrom
zihaomu:feature/hipvs-cuvs
Draft

[ROCm] Enable seekdb+hipvs on AMD GPU#1314
zihaomu wants to merge 13 commits into
oceanbase:developfrom
zihaomu:feature/hipvs-cuvs

Conversation

@zihaomu

@zihaomu zihaomu commented Aug 18, 2026

Copy link
Copy Markdown

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.

  • OFF by default (OB_BUILD_CUVS=OFF) -> no new dependency, behavior identical to upstream.
  • Declarative, per-index opt-in: CREATE VECTOR INDEX ... WITH (..., lib=cuvs).
  • SQL-callable batched ANN: CALL dbms_vector.batch_knn(index_tbl, probe_tbl, k, out_tbl).
  • The GPU path is always "correct or safe-fallback" (filters / deletes / non-L2 / staleness -> VSAG).

1. How it works (support principle)

seekdb's vector index is backed by VSAG (CPU HNSW). All vector operations flow through the
obvsag C-API in src/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.so is a thin C shim that exposes plain
symbols (seekdb_cuvs_build/search/free, plus a one-shot batch entry) and internally runs a cuVS
CAGRA graph on the GPU via hipVS libcuvs_c. It hides all ROCm/cuVS headers from seekdb's
compiler (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_index buffers the incoming vectors per index handle.
  • knn_search lazily builds a CAGRA graph from that buffer (>= 256 points) and serves top-k
    from 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.
  • Correctness guards (the GPU path never returns wrong results):
    • Filters / deletes: cuVS returns the unfiltered top-k, then we post-filter with the same
      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).
    • Metric: the bridge builds an L2 graph, so only L2 indexes use the GPU; IP/cosine -> VSAG.
    • Freshness: the GPU index serves only when it matches the current buffer; if rows were added
      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 add ObVectorIndexAlgorithmLib::VIAL_CUVS and
accept lib=cuvs in the DDL. The storage plugin (ob_plugin_vector_index_adaptor) marks the
index handle for the GPU when the index is lib=cuvs + L2 + HNSW, and the adaptor hooks gate on that
per-handle mark -- not a global switch. So only indexes that declare lib=cuvs use 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 probe
and 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 the
bridge .so is 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):

path throughput recall@10 note
single query, 100k CAGRA 2,148 q/s (0.466 ms) -- ~= VSAG-snapshot 0.41 ms -> no win
VSAG-CPU, per query (10k) 4,598 q/s 0.926 best single-query baseline
cuVS-GPU, per query (10k) 470 q/s 0.873 slower (per-call overhead)
cuVS-GPU, batch (10k, nq=100) 54,777 q/s 0.873 116.7x vs per-query; 12x vs best CPU
bridge batch sweep, nq=5000 634,930 q/s -- 263x, GPU saturated
  • seekdb-native batch seam (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 bridge
    batch), one GPU call; per-probe cost amortizes 3.3 ms -> 0.165 ms as batch size grows.
  • Per-index correctness: 10k rows, lib=cuvs query recall@10 0.89 vs ground truth;
    WHERE / DELETE / cosine all safely fall back to VSAG with correct results.
  • Per-index routing verified: with the server started without any env var, a lib=cuvs index
    serves on the GPU (cuvs_serve) while a lib=vsag index stays on VSAG (knn_simple, 0 GPU calls).

3. How to use it

Build with the backend on:

./build.sh release \
  -DOB_BUILD_CUVS=ON \
  -DCUVS_BRIDGE_LIB=/path/to/libseekdb_cuvs_bridge.so \
  --make -j64

Runtime: make the bridge and libcuvs reachable (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:

ALTER SYSTEM SET ob_vector_memory_limit_percentage = 30;

CREATE TABLE t (
  c1 INT PRIMARY KEY,
  c2 VECTOR(128),
  VECTOR INDEX idx(c2) WITH (distance=l2, type=hnsw, lib=cuvs,
                             m=16, ef_construction=200, ef_search=64)
);
-- inserts + APPROXIMATE queries on this index run on the GPU; all other
-- indexes in the same server stay on VSAG. No environment variable needed.
SELECT c1 FROM t ORDER BY l2_distance(c2, '[...]') APPROXIMATE LIMIT 10;

B) Batched ANN operator -- one GPU call for many probes:

-- index_tbl and probe_tbl are (id INT, vec VECTOR(dim)); out_tbl is pre-created:
CREATE TABLE bk_out(probe_id BIGINT, neighbor_id BIGINT, distance FLOAT, rk INT);

CALL dbms_vector.batch_knn('index_tbl', 'probe_tbl', 10, 'bk_out');
SELECT * FROM bk_out ORDER BY probe_id, rk;

The bridge libseekdb_cuvs_bridge.so is built out-of-tree from hipVS
(hipvs-compile -shared -fPIC seekdb_cuvs_bridge.c); see
docs/gpu-vector-index-hipvs-cuvs/ for the source, the reproduction steps and the smoke tests.


4. What's in this PR

  • Compile switch OB_BUILD_CUVS (+ CUVS_BRIDGE_LIB, + developer-only OB_BUILD_CUVS_TRACE).
  • Adaptor GPU path in ob_vsag_adaptor.* (per-index mark, lazy CAGRA build/serve, safe fallback,
    batch seam, one-shot raw batch).
  • DDL: VIAL_CUVS + lib=cuvs accepted for dense L2 HNSW (rejected for IVF).
  • Plugin routing: mark lib=cuvs index handles for the GPU path.
  • dbms_vector.batch_knn PL procedure.
  • CI-safe DDL contract test + a GPU-required smoke template + design/benchmark docs.

When OB_BUILD_CUVS=OFF (default): the bridge is not linked, the GPU code never runs, and the
runtime behavior is identical to upstream VSAG.


5. Not included / follow-ups

  • Snapshot persistence: rebuild the GPU index after an fdeserialize snapshot reload
    (currently a reloaded snapshot falls back to VSAG until the next in-process rebuild).
  • Persistent GPU worker instead of a per-query pthread; reuse device buffers (cut single-query overhead).
  • Memory: avoid keeping a second copy of the vectors in the GPU buffer for large tables.
  • Stricter DDL: reject lib=cuvs with a non-L2 distance at DDL time; add a
    _enable_gpu_vector_index global kill-switch.
  • Cleaner OFF build: fully #ifdef-out the OFF-path code (today it is compiled but gated by an
    inert stub + a runtime-false check).
  • CI: regenerate the mysqltest .result via --record and add a GPU-present skip guard for the
    runtime tests; the GPU behavior test needs a GPU runner.
  • batch_knn: cache the built index across calls so repeated calls skip the rebuild.
  • Portability / perf: broaden GPU support beyond gfx1100 and enable performance-tuned cuVS
    (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

zihaomu added 13 commits August 17, 2026 17:03
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.
@CLAassistant

CLAassistant commented Aug 18, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

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.

2 participants