Skip to content

feat: split vectordb engine by cpu variant#656

Merged
qin-ctx merged 1 commit intomainfrom
simd-dispatch
Mar 16, 2026
Merged

feat: split vectordb engine by cpu variant#656
qin-ctx merged 1 commit intomainfrom
simd-dispatch

Conversation

@zhoujh01
Copy link
Copy Markdown
Collaborator

@zhoujh01 zhoujh01 commented Mar 16, 2026

feat: split vectordb engine by cpu variant

背景

当前 vectordb 原生引擎的构建和分发方式更偏向单一扩展产物。这个模型在 x86 平台上有两个明显问题:

  1. 如果直接按高 SIMD 档位构建,wheel 在较老 CPU 上可能无法正常加载。
  2. 如果只按保守档位构建,又无法充分利用新 CPU 的 AVX2 / AVX512 能力。

这会导致“兼容性”和“性能”之间只能二选一,也让 wheel 的跨机器复用能力受限。

目标

本 PR 的目标是把 vectordb engine 从“单一原生扩展”升级为“按 CPU 变体构建 + 运行时自动选择”的模式:

  • 构建阶段同时产出多个 x86 后端变体
  • 运行时根据当前 CPU 能力自动选择最优后端
  • 对上层 Python 调用方保持稳定入口
  • 在后端不可用时保留明确的降级路径

方案概述

整体方案分成两层:

1. 构建层

  • x86 平台不再只生成一个 engine 扩展,而是分别构建:
    • _x86_sse3
    • _x86_avx2
    • _x86_avx512
  • 非 x86 平台继续构建单一 _native 后端
  • 额外生成 _x86_caps 模块,用于运行时探测 CPU 支持的指令集能力

2. 运行时加载层

  • 新增 openviking.storage.vectordb.engine 包装层作为稳定入口
  • 在 import 时根据:
    • 当前平台是否为 x86
    • wheel 中实际打包了哪些后端
    • 当前 CPU 实际支持哪些特性
  • 自动选择最合适的 backend
  • 对外继续导出统一的 engine API,避免业务侧感知底层模块拆分

核心改动

1. 新增构建配置抽象

新增 build_support/x86_profiles.py,统一管理以下逻辑:

  • 宿主机是否为 x86
  • x86 默认构建哪些变体
  • setuptools 侧 primary extension 应指向哪个模块

关键设计:

  • x86 默认构建 sse3 / avx2 / avx512
  • sse3 作为 baseline,保证最小兼容面
  • 支持通过 OV_X86_BUILD_VARIANTS 环境变量裁剪构建变体

2. 重构 setup.py 的原生扩展构建入口

setup.py 不再假设只有一个固定的 engine 扩展,而是:

  • 通过 build_support.x86_profiles 推导宿主机构建配置
  • 将 CMake 输出目录、Python 扩展后缀、x86 变体列表传入底层构建
  • 在 setuptools 侧将 primary extension 指向:
    • x86: openviking.storage.vectordb.engine._x86_sse3
    • non-x86: openviking.storage.vectordb.engine._native

同时补充了打包规则,确保 storage/vectordb/engine/*.so*.pyd 能进入 wheel。

3. 重构 CMake 为多后端构建模型

src/CMakeLists.txt 从“单模块 + 单一 SIMD level”改为“公共库 + 多变体 backend”:

  • 抽出 engine_common 作为公共实现
  • 按变体分别构建 index library
  • 为每个变体单独附加编译选项
  • 为每个变体生成独立 pybind11 模块
  • 在 x86 上额外生成 _x86_caps
  • 保留 engine_impl 兼容 target,避免现有 C++ 测试/依赖直接断掉

其中 x86 变体的编译规则为:

  • sse3 使用 -msse3
  • avx2 使用 -mavx2,并显式关闭更高 AVX512 指令
  • avx512 使用 -mavx512f/-bw/-dq/-vl

如果编译器不支持某个变体,对应变体会被跳过,而不是直接让整个构建失败。

4. 让 pybind11 接口支持动态模块名

src/pybind11_interface.cpp 通过 OV_PY_MODULE_NAME 宏接收模块名,使同一份绑定代码可以复用生成多个 Python 扩展:

  • _x86_sse3
  • _x86_avx2
  • _x86_avx512
  • _native

这一步是多产物构建能够成立的基础。

5. 新增统一运行时 loader

新增 openviking/storage/vectordb/engine/__init__.py,负责:

  • 识别当前平台
  • 探测 wheel 中实际存在的 backend
  • 在 x86 上调用 _x86_caps 获取 CPU 支持能力
  • AVX512 -> AVX2 -> SSE3 的优先级自动选择最优后端
  • 支持通过 OV_ENGINE_VARIANT 强制指定 backend
  • 校验强制指定是否与平台、打包内容、CPU 能力匹配
  • 将选中 backend 的符号重新导出成统一接口

这样上层仍然只需要:

import openviking.storage.vectordb.engine as engine

不需要关心底层真实加载的是哪个原生模块。

6. 新增 CPU 特性探测模块

新增 src/cpu_feature_probe.cpp,在 x86 上通过 cpuid + xgetbv 探测:

  • SSE3
  • AVX / AVX2
  • AVX512F / DQ / BW / VL

这里不仅检查硬件 feature bit,也检查 OS 是否启用了对应寄存器上下文保存能力,避免“CPU 声称支持但运行时不可安全使用”的误判。

7. 调整 Python fallback 行为

openviking/storage/vectordb/store/bytes_row.py 现在在导入 engine 后还会检查:

  • ENGINE_VARIANT 是否为 unavailable

如果 backend 不可用,则主动退回 Python 实现,而不是继续使用一个不完整的原生入口。

用户可见行为变化

本 PR 合入后:

  • x86 wheel 可以同时打包多个 SIMD backend
  • 同一个 wheel 在不同 x86 机器上可按 CPU 能力自动选最优实现
  • 非 x86 平台继续走 _native
  • Python 业务代码仍然使用同一个 import 路径
  • 当 backend 缺失或不兼容时,错误信息更明确,且部分场景可回退到 Python 实现

为什么这样设计

这个方案的主要考虑有三点:

1. 把兼容性和性能解耦

通过同时打包 sse3 baseline 和高阶变体,不再需要在“能跑”和“跑得快”之间二选一。

2. 对上层调用侧保持稳定

业务层继续依赖统一的 openviking.storage.vectordb.engine,避免底层产物拆分扩散到上层接口。

3. 让 wheel 分发更符合真实运行环境

相比在构建时硬编码单一 SIMD level,运行时基于实际 CPU 能力选择 backend,更符合 wheel 在多种机器上安装和运行的方式。

风险与兼容性评估

风险

  • 构建链路变复杂,CMake/setuptools/打包规则之间的耦合更高
  • x86 变体编译规则对不同编译器支持度更敏感
  • 运行时 loader 引入后,backend 缺失时的失败模式从“import 直接失败”变为“延迟到符号访问时失败”,需要上层正确处理

已做的兼容性处理

  • sse3 作为 x86 baseline
  • 非 x86 平台保留 _native
  • 保留 engine_impl 兼容 target
  • bytes_row.py 增加 unavailable 检测并保留 Python fallback
  • CI smoke test 改为验证正式 import 路径和 runtime variant,而不是只验证裸 .so 是否存在

测试

新增测试覆盖:

  • tests/misc/test_vectordb_engine_loader.py
    • 校验 x86 自动选择最优 backend
    • 校验非 x86 选择 native backend
    • 校验强制指定不受支持 backend 时正确报错
  • tests/misc/test_x86_profiles.py
    • 校验 x86 / non-x86 的构建配置推导结果

本地已执行:

pytest tests/misc/test_vectordb_engine_loader.py tests/misc/test_x86_profiles.py

结果:

  • 5 passed

后续可关注项

  • 是否需要为 wheel 内容增加更直接的打包产物校验测试
  • 是否需要补充对 OV_ENGINE_VARIANT 不同取值的更多边界测试
  • 是否需要在 release 文档中说明 x86 多变体 backend 的加载策略

@github-actions
Copy link
Copy Markdown

PR Reviewer Guide 🔍

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Auto-select fallback

When auto-selecting on x86, the fallback to "x86_sse3" doesn't check if it's in supported_x86. While supported_x86 is initialized with "x86_sse3", this could still lead to loading an incompatible variant on very old CPUs.

if "x86_sse3" in available:
    return "x86_sse3", available, None
Single extension build

The build_extension method skips building after the first extension using _engine_extensions_built. While currently only one extension is defined, this could break future extensions added to ext_modules.

if getattr(self, "_engine_extensions_built", False):
    return

ext_fullpath = Path(self.get_ext_fullpath(ext.name))
ext_dir = ext_fullpath.parent.resolve()
build_dir = Path(self.build_temp) / "cmake_build"
build_dir.mkdir(parents=True, exist_ok=True)

self._run_stage_with_artifact_checks(
    "CMake build",
    lambda: self._build_extension_impl(ext_fullpath, ext_dir, build_dir),
    [(ext_fullpath, f"native extension '{ext.name}'")],
)
self._engine_extensions_built = True

@github-actions
Copy link
Copy Markdown

Failed to generate code suggestions for PR

qin-ctx

This comment was marked as duplicate.

Copy link
Copy Markdown
Collaborator

@qin-ctx qin-ctx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bug] .github/workflows/_build.ymlverify-macos-14-wheel-on-macos-15 job(第 428-475 行)仍然使用旧的 smoke test 逻辑,通过 glob 匹配 engine*.so 单文件。但本 PR 已将 engine 从单个扩展模块改为 Python package(engine/__init__.py + _native.so 等子模块),旧的 glob 模式无法匹配到新的文件布局。合并后该 CI 验证 job 会始终失败,报 openviking storage engine extension was not installed。请更新为新的 engine package import 方式。

target_link_libraries(${INDEX_TARGET} PUBLIC Threads::Threads)
if(OV_PLATFORM_ARM)
target_link_libraries(${INDEX_TARGET} PUBLIC krl)
endif()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bug] if(OV_PLATFORM_ARM) 出现在 if(OV_PLATFORM_X86) 的循环内部,但这两个平台标志是互斥的(基于 CMAKE_SYSTEM_PROCESSOR 的匹配),所以这段代码永远不会执行,是死代码。

更关键的问题:拆分后 engine_common(包含 store/*.cppcommon/*.cpp)没有链接 krl。原来的单体 engine_impl STATIC 库包含所有源文件并通过 PRIVATE 链接了 krl。如果 store/common/ 中有代码依赖 KRL 符号或头文件,在 ARM 平台上会出现链接错误。

建议:移除这个不可达的代码块,并确认 engine_common 在 ARM 上是否需要 target_link_libraries(engine_common PUBLIC krl)


#define OV_EXPAND_MACRO(name) name

PYBIND11_MODULE(OV_EXPAND_MACRO(OV_PY_MODULE_NAME), m) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Bug] OV_EXPAND_MACRO 是一个单层恒等宏,用于 PYBIND11_MODULE 内部会做 token 拼接(PyInit_##name)的场景。这种写法比较脆弱——如果 pybind11 内部宏结构发生变化,OV_PY_MODULE_NAME 可能在拼接前未被完全展开。

pybind11 >= 2.6 内部已经提供了一层宏间接展开,可以直接传入宏定义:

PYBIND11_MODULE(OV_PY_MODULE_NAME, m) {

请用项目要求的最低 pybind11 版本(pyproject.toml 中为 2.13.0)验证这种写法是否可行。

target_link_libraries(engine_impl INTERFACE engine_common engine_index_sse3 Threads::Threads)
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "9.0")
target_link_libraries(engine_impl INTERFACE stdc++fs)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] engine_impl INTERFACE 目标的 filesystem 库链接逻辑在 x86 分支(第 278-290 行)和非 x86 分支(第 307-319 行)中完全重复,与 ov_link_filesystem_libs() 函数的功能一致。建议复用该函数(适配 INTERFACE 目标)以消除重复代码。

if variant in available and variant in supported_x86:
return variant, available, None

if "x86_sse3" in available:
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] 这个 if "x86_sse3" in available fallback 看起来是冗余的。_X86_PRIORITY(第 20 行)已经包含 x86_sse3,而 _supported_x86_variants() 默认就包含 x86_sse3(第 43 行:supported = {"x86_sse3"})。所以如果 x86_sse3available 中,它在上面的 for variant in _X86_PRIORITY 循环中就已经被选中了,不会走到这里。


def build_extension(self, ext):
"""Build a single Python native extension artifact using CMake."""
if getattr(self, "_engine_extensions_built", False):
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] _engine_extensions_built 标志会导致第一个 extension 构建后跳过所有后续 extension。如果将来有人在 ext_modules 中添加第二个非引擎的 Extension,它会被静默跳过。建议将判断条件限定到引擎模块名称:

if ext.name.startswith("openviking.storage.vectordb.engine") and getattr(self, "_engine_extensions_built", False):
    return

def fake_find_spec(name, package=None):
fullname = importlib.util.resolve_name(name, package) if name.startswith(".") else name
if fullname == "openviking.storage.vectordb.engine._x86_caps":
return object()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] fake_find_spec 对匹配的模块返回裸 object()。目前生产代码只做 is not None 检查所以能通过,但如果 _module_exists 的实现后续扩展为访问 .originModuleSpec 属性,测试会以难以理解的方式失败。建议返回 types.SimpleNamespace(origin="/fake/path.so") 以更真实地模拟。

"bin/ov.exe",
"storage/vectordb/engine/*.so",
"storage/vectordb/engine/*.pyd",
]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] glob 模式 storage/vectordb/engine/*.so 只匹配 engine/ 目录下一级的 .so 文件。如果将来 engine 下有嵌套子包,这些模式无法捕获。当前架构下不是问题,但 **/*.so 会更具前瞻性。

@qin-ctx qin-ctx merged commit 758ccd8 into main Mar 16, 2026
5 checks passed
@qin-ctx qin-ctx deleted the simd-dispatch branch March 16, 2026 09:50
@github-project-automation github-project-automation bot moved this from Backlog to Done in OpenViking project Mar 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants