Faiss SIMD 动态分派迁移指南:从单体编译到运行时调度的完整实践
Faiss 正在将其 SIMD 优化体系从"每个 SIMD 等级单独编译一份库"的单体(monolithic)模式,迁移到单一库 + 运行时动态分派(Dynamic Dispatch,DD)模式,以覆盖 x86 的 AVX2/AVX512/AVX512_SPR、ARM 的 NEON/SVE 以及 RISC-V 的 RVV。本文基于仓库中的迁移指南 simd_dynamic_dispatch_migration.md,结合 faiss/utils/simd_levels.h、faiss/impl/simd_dispatch.h、faiss/CMakeLists.txt 及已转换模块的真实源码,讲清:为什么简单的"运行时 if 分支"行不通、四步转换配方每一步的正确写法、COMPILE_SIMD_* 宏与编译器宏的本质区别,以及多模式构建测试的完整流程。读完后,你应能为任意 Faiss 模块编写符合 DD 规范的 SIMD 代码并正确接入构建系统。
一、背景:为什么 Faiss 要放弃单体 SIMD 编译
Faiss 在距离计算、PQ 查表、量化器编解码等热路径上大量使用 SIMD 指令。问题在于:在一台不支持某条 SIMD 指令的机器上执行这些指令,进程会直接收到 SIGILL(非法指令信号)而崩溃。因此必须确保运行时选择的实现与当前 CPU 能力匹配。
1.1 单体 SIMD(遗留模式)
单体模式下,整个库统一以例如 -mavx2 编译,__AVX2__ 宏被全局点亮;AVX2 专属代码用 #ifdef __AVX2__ 保护,编译器甚至会把普通标量代码自动向量化成 AVX2 指令。
这对开发者很省事,但代价是:Faiss 必须为每个 SIMD 等级(avx2、avx512、avx512_spr、sve 等)分别编译一份库,客户端必须链接到与自己机器匹配的那一份。从 faiss/CMakeLists.txt 的构建逻辑可以看到,单体模式会为 faiss_avx2、faiss_avx512、faiss_avx512_spr、faiss_sve 分别建立独立 target(L466-L525 附近),每种 FAISS_OPT_LEVEL 产出一个互不兼容的库。
1.2 为什么"运行时检测 + if 分支"不可行
最诱人的方案显然是在公共代码里这样写:
if (avx2_detected()) {
// AVX2 intrinsics
} else {
// scalar fallback
}
这行不通。 原因在于 -mavx2 是"对整个翻译单元(预处理后的 .cpp 文件)的 blanket permission"——它授权编译器在该文件任何位置(包括你的"标量 fallback"分支里)自动向量化出 AVX2 指令。于是所谓 fallback 路径在 AVX2 不支持的机器上照样触发 SIGILL。
GCC 确实提供了函数级 pragma(#pragma GCC target)来局部设置标志,但这类手段不可移植(MSVC、Clang 各自一套语法),Faiss 选择了一条更稳健的架构路线。
这直接推导出两条硬性要求:
- SIMD 优化代码必须放进独立的编译单元(translation unit,TU),用
-mavx2、-mavx512f等专属标志编译; - 公共代码必须在不带 SIMD 标志的条件下编译,从根上杜绝自动向量化。
1.3 动态分派(DD)模式
DD 模式下,SIMD 代码集中在特定文件(如 distances_avx2.cpp)里,只有这些文件带 -mavx2 编译;公共文件则使用基线标志。以 faiss/CMakeLists.txt 中的实际配置为例:
- 公共文件基线标志(防自动向量化):
-mpopcnt -msse4 -mno-avx -mno-avx2(x86 平台);ARM 上不加特殊标志; - 每个 SIMD 文件按文件级属性设置专属标志,例如 AVX2 文件
-mavx2;-mfma;-mf16c;-mpopcnt,AVX512 文件-mavx512f;-mavx512cd;-mavx512vl;-mavx512dq;-mavx512bw;-mfma;-mf16c;-mpopcnt,SPR 专属文件再加-mavx512vpopcntdq等(L547-L563)。
与此同时,DD 模式通过 set_source_files_properties 实现 CMake 的 per-file 编译选项,把"哪些文件算 SIMD 文件"固化在 faiss/CMakeLists.txt 顶部的 SIMD FILE REGISTRY 中——FAISS_SIMD_AVX2_SRC、FAISS_SIMD_AVX512_SRC、FAISS_SIMD_AVX512_SPR_SRC、FAISS_SIMD_NEON_SRC、FAISS_SIMD_SVE_SRC、FAISS_SIMD_RVV_SRC 六个列表,并带有"Keep in sync with Buck's SIMD_FILES in xplat.bzl"的同步注释。
DD 模式的三条核心机制:
(1)SIMDLevel 模板参数。 函数签名被模板化,例如 fvec_L2sqr<SIMDLevel::AVX2>。SIMDLevel 是定义在 faiss/utils/simd_levels.h 中的枚举:
enum class SIMDLevel {
NONE,
// x86
AVX2,
AVX512,
AVX512_SPR, // Sapphire Rapids: AVX512 + BF16 + FP16 + VNNI
// arm & aarch64
ARM_NEON,
ARM_SVE, // Scalable Vector Extension (ARMv8.2+)
// riscv
RISCV_RVV, // RISC-V Vector Extension (rv64gcv)
COUNT
};
(2)非模板包装函数做运行时分派。 公共 API 仍是无模板的 fvec_L2sqr(...),内部根据 SIMDConfig::level 选择特化版本。为避免热循环中每次调用都走一次 switch,simd_dispatch.h 提供了 with_simd_level:它在整个代码块上只分派一次,把选定的级别作为编译期常量注入 lambda:
with_simd_level([&]<SIMDLevel SL>() {
for (size_t i = 0; i < n; i++) {
distances[i] = fvec_L2sqr<SL>(query, vectors + i * d, d);
}
});
对照 simd_dispatch.h 的文档注释,官方推荐写法正是"分派发生在循环之外,循环体以最优 SIMD 实现运行,无每次迭代的分派开销"。
(3)COMPILE_SIMD_* 链接期承诺。 DD 模式构建时,COMPILE_SIMD_AVX2(及其他 COMPILE_SIMD_*)宏被传给 target 的所有源文件,而不仅是 SIMD TU(见 faiss/CMakeLists.txt:target_compile_definitions(faiss PRIVATE COMPILE_SIMD_AVX2 COMPILE_SIMD_AVX512 COMPILE_SIMD_AVX512_SPR))。这允许公共 TU 里的 dispatch switch 写出 case SIMDLevel::AVX2: return fvec_L2sqr<SIMDLevel::AVX2>(...),即使该特化定义在另一个文件里——COMPILE_SIMD_* 宏本质上是"链接器一定能找到这个特化"的链接期承诺(link-time promise)。
运行时,SIMD 级别默认取最高可用值,可通过 SIMDConfig::set_level 或 FAISS_SIMD_LEVEL 环境变量覆盖(仅 DD 模式有效)。faiss/utils/simd_levels.cpp 中构造函数即读取该环境变量(L117-L120 附近)。
二、四步转换配方(The Conversion Recipe)
每个模块的转换都遵循同一套四步流程。下面以文档中的 fvec_madd(c[i] = a[i] + bf·b[i])为例逐步展开。
Step 1:模板化到 SIMDLevel
把 #ifdef __AVX2__ 保护替换为 template <SIMDLevel SL>,并在 #ifdef COMPILE_SIMD_AVX2 内声明/定义。
转换前(单体模式):
// functions.h
void fvec_madd(size_t n, const float* a, float bf, const float* b, float* c);
// functions.cpp
void fvec_madd(size_t n, const float* a, float bf, const float* b, float* c) {
#ifdef __AVX2__
// AVX2 implementation
#else
for (size_t i = 0; i < n; i++)
c[i] = a[i] + bf * b[i];
#endif
}
转换后:
// functions.h
#include <faiss/utils/simd_levels.h>
template <SIMDLevel SL>
void fvec_madd(size_t n, const float* a, float bf, const float* b, float* c);
// functions.cpp
// Non-template public API (dispatches at runtime in DD mode)
void fvec_madd(size_t n, const float* a, float bf, const float* b, float* c);
可选动作:把裸 intrinsics 类型(__m256、float32x4x2_t)替换为可移植的 simdlib 包装(simd8float32、simd8uint32),但注意下文"smdlib 覆盖范围"一节中 SVE/RVV 的限制。
Step 2:拆分为按 ISA 划分的编译单元
把 SIMD 特化移入按 ISA 命名的 .cpp 文件,每个文件整体被 #ifdef COMPILE_SIMD_* 保护;原文件只保留 NONE(标量)特化和 dispatch 包装。
// functions_avx2.cpp — compiled with -mavx2
#ifdef COMPILE_SIMD_AVX2
#include "functions.h"
#include <immintrin.h>
template <>
void fvec_madd<SIMDLevel::AVX2>(
size_t n, const float* a, float bf, const float* b, float* c) {
// AVX2 intrinsics implementation
}
#endif
// functions.cpp — compiled with baseline flags (no SIMD)
#include "functions.h"
#include <faiss/impl/simd_dispatch.h>
namespace faiss {
template <>
void fvec_madd<SIMDLevel::NONE>(
size_t n, const float* a, float bf, const float* b, float* c) {
for (size_t i = 0; i < n; i++)
c[i] = a[i] + bf * b[i];
}
void fvec_madd(size_t n, const float* a, float bf, const float* b, float* c) {
with_simd_level([&]<SIMDLevel SL>() {
fvec_madd<SL>(n, a, bf, b, c);
});
}
} // namespace faiss
per-SIMD TU 的三条关键规则:
- 用
#ifdef COMPILE_SIMD_AVX2(不是#ifdef __AVX2__)保护整个文件。两者的语义差异是 DD 模式的命门:COMPILE_SIMD_*是构建系统注入的宏,在 DD 模式下对公共 TU 也成立(它只是承诺链接器能找到特化);__AVX2__是编译器宏,只在真正带-mavx2编译的 TU 中成立。正因如此,公共 TU 才能写出引用fvec_madd<SIMDLevel::AVX2>的 dispatch 代码而无需本地定义。仓库中的真实案例 faiss/impl/approx_topk/avx2.cpp 首行即#ifdef COMPILE_SIMD_AVX2。 - ISA 专属 intrinsics 头(
<immintrin.h>、<arm_neon.h>、<arm_sve.h>)必须写在 guard 内部。 - 严禁让 SIMD 数据类型(
__m256、float32x4_t)跨越 TU 边界——公共接口一律使用float*、uint8_t*等普通指针。
Step 3:接入分派(Wire up dispatch)
在分派边界(dispatch boundary)调用 with_simd_level——即热内核或工厂函数第一次被调用的位置。分派只发生一次,且必须在外层。
对于返回对象的工厂函数(距离计算器、scanner),把 SIMDLevel 作为构造期模板参数"烧进"对象:
FlatCodesDistanceComputer* get_distance_computer() {
return with_simd_level([&]<SIMDLevel SL>() -> FlatCodesDistanceComputer* {
FlatCodesDistanceComputer* dc = new MyDistanceComputer<SL>();
// ... configure ...
return dc;
});
}
分派掩码(dispatch mask)。 with_simd_level 默认假设 NONE + AVX2 + AVX512 + ARM_NEON + RISCV_RVV 五个实现都存在。如果你的函数只实现了其中一部分,用 with_selected_simd_levels<mask> 传入位掩码;掩码中缺失的级别会沿同架构家族的降级链 fall through 到下一个可用级别:x86 为 AVX512_SPR → AVX512 → AVX2 → NONE,ARM 为 ARM_SVE → ARM_NEON → NONE,RISC-V 为 RISCV_RVV → NONE(三条链互相独立)。这条降级链在 simd_dispatch.h 的 get_simd_fallback 函数中逐条实现。
// Only NONE, AVX2, and ARM_SVE implementations exist.
// AVX512 will fall through to AVX2; ARM_NEON will fall through to NONE.
constexpr int MY_LEVELS = (1 << int(SIMDLevel::NONE))
| (1 << int(SIMDLevel::AVX2))
| (1 << int(SIMDLevel::ARM_SVE));
with_selected_simd_levels<MY_LEVELS>([&]<SIMDLevel SL>() {
my_kernel<SL>(...);
});
simd_dispatch.h 中预定义了常用掩码(也可以随时用 (1 << int(SIMDLevel::X)) | ... 自拼):
| 掩码 | 覆盖级别 | 适用场景 |
|---|---|---|
AVAILABLE_SIMD_LEVELS_NONE |
仅 NONE | 纯标量函数 |
AVAILABLE_SIMD_LEVELS_AVX2_NEON |
NONE、AVX2、ARM_NEON | 256 位 simdlib 操作(with_simd_level_256bit) |
AVAILABLE_SIMD_LEVELS_A0 |
NONE、AVX2、AVX512、ARM_NEON、RISCV_RVV | 默认(with_simd_level) |
AVAILABLE_SIMD_LEVELS_A1 |
A0 + ARM_SVE | 有专属 SVE 实现的函数 |
AVAILABLE_SIMD_LEVELS_ALL |
全部级别 | 恒等/诊断类函数 |
仓库源码比文档表格还多两个掩码,值得补充说明:AVAILABLE_SIMD_LEVELS_A0_SPR(A0 + AVX512_SPR,目前用于使用 VPOPCNTDQ 的 RaBitQ popcount 内核)和 AVAILABLE_SIMD_LEVELS_A2(仅 NONE + AVX2 + ARM_SVE)。此外 simd_dispatch.h 还暴露了 with_simd_level_fallback:当工厂函数可以"返回 null 表示拒绝该级别"(例如 AVX-512 路径要求 d % 16 == 0)时,它会从当前级别逐级向下重试直到 NONE。
Step 4:注册到构建系统
新源文件必须加入构建系统。编辑 faiss/CMakeLists.txt(不是顶层 CMakeLists.txt):
set(FAISS_SIMD_AVX2_SRC
# ... existing entries ...
path/to/functions_avx2.cpp # <-- add
)
set(FAISS_SIMD_AVX512_SRC
# ... existing entries ...
path/to/functions_avx512.cpp # <-- add
)
set(FAISS_SIMD_NEON_SRC
# ... existing entries ...
path/to/functions_neon.cpp # <-- add
)
set(FAISS_SIMD_SVE_SRC
# ... existing entries ...
path/to/functions_sve.cpp # <-- add (if SVE implementation exists)
)
set(FAISS_SIMD_RVV_SRC
# ... existing entries ...
path/to/functions_rvv.cpp # <-- add (if RVV implementation exists)
)
# 同时把新头文件加入 FAISS_HEADERS
当前注册表的真实规模可以感受这套体系的覆盖面:AVX2 列表下有 impl/fast_scan/impl-avx2.cpp、impl/hnsw/avx2.cpp、impl/scalar_quantizer/sq-avx2.cpp、impl/approx_topk/avx2.cpp、utils/simd_impl/distances_avx2.cpp 等十余个文件(faiss/CMakeLists.txt)。
用 CMake 启用 DD 模式,配置参数为 FAISS_OPT_LEVEL=dd:
cmake -B build_dd -DFAISS_OPT_LEVEL=dd -DBUILD_TESTING=ON .
这会构建出单一 faiss 库,把所有 SIMD 变体都编进去并启用运行时分派——对照单体模式(avx2、avx512、generic 等)产出按级别分离的多个库。注意 faiss/CMakeLists.txt 的注释提醒:FAISS_OPT_LEVEL=dd 才是把 DD 支持编进主 target 的开关,generic 模式既没有 SIMD 也没有分派。
若原来 FAISS_SRC 里还有该模块的单体源文件,应将其移除。
Buck(Meta 内部) 需保持同步,在 xplat.bzl 中:
SIMD_FILES = {
# ... existing entries ...
"path/to/functions_avx2.cpp": (X86_64, AVX2),
"path/to/functions_avx512.cpp": (X86_64, AVX512),
"path/to/functions_neon.cpp": (AARCH64, ARM_NEON),
"path/to/functions_rvv.cpp": (RISCV64, RISCV_RVV),
}
# 同时把头文件加入 header_files()
两个构建系统必须保持一致——CMake 注册表顶部和 Buck 侧都有互相引用的 # Keep in sync 注释。
三、常见模式与源码实例
3.1 跨 ISA 级别共享内核代码
当同一份内核代码在多个 ISA 级别上都成立时,可以通过共享 -inl.h 头文件在多个 per-ISA .cpp 间复用。关键在于每个 per-ISA TU 用自己的 SIMD 标志编译这份共享代码;若走 -inl.h 方案,各 per-ISA .cpp 在 include 前先定义宏 THE_SIMD_LEVEL 指明要实例化哪个模板。
仓库中距离函数就是这种模式:faiss/utils/simd_impl/distances_avx2.cpp 中:
#define THE_SIMD_LEVEL SIMDLevel::AVX2
#include <faiss/utils/simd_impl/distances_autovec-inl.h>
AVX512 文件则定义 THE_SIMD_LEVEL SIMDLevel::AVX512 后 include 同一份 -inl.h。
3.2 触发模板实例化的两种方式
per-ISA .cpp 必须让编译器真正发出该级别的特化代码,仓库中存在两种模式:
(a)显式模板实例化——逐个列出要实例化的具体类型,适合模板参数集合小而固定的场景。faiss/impl/approx_topk/avx2.cpp 是标准范例:
template struct HeapWithBucketsCMaxFloat<8, 3, SIMDLevel::AVX2>;
template struct HeapWithBucketsCMaxFloat<16, 2, SIMDLevel::AVX2>;
// ...
该文件还展示了用 INSTANTIATE_ACCUM_TAB(M) 宏批量展开 accum_and_store_tab/accum_and_add_tab/accum_and_finalize_tab 一族函数模板的写法(L25-L62),并严格把 #include <faiss/impl/simdlib/simdlib_avx2.h> 放在 -inl.h 之前(见 3.4 的 include 顺序规则)。
(b)宏驱动定义——#define 一个级别常量,再 include 一个"展开"所有该级别特化的 dispatch 头。适合需要同时实例化多个互相依赖的类型的场景。两个真实案例:
// faiss/impl/scalar_quantizer/sq-avx2.cpp(L899-L900)
#define THE_LEVEL_TO_DISPATCH SIMDLevel::AVX2
#include <faiss/impl/scalar_quantizer/sq-dispatch.h>
// faiss/utils/simd_impl/distances_avx2.cpp(L38-L40)
#define THE_SIMD_LEVEL SIMDLevel::AVX2
#include <faiss/utils/simd_impl/distances_autovec-inl.h>
sq-dispatch.h 是一个 X-macro 风格的分派头:它在内部 #ifndef THE_LEVEL_TO_DISPATCH / #error 强制调用方先定义级别,然后以 SL 为别名统一生成 ScalarQuantizer::SQuantizer* sq_select_quantizer<THE_LEVEL_TO_DISPATCH>、sq_select_distance_computer、sq_select_InvertedListScanner 等工厂特化——这正是 Step 3 中"工厂函数按级别返回对象"模式的完整落地。
3.3 共享 inline 头 + 编译器自动向量化
对于"编译器自动向量化就够了"的函数,可以在共享 -inl.h 里写标量循环,让它在不同 ISA 标志的 TU 中被自动向量化。FAISS_PRAGMA_IMPRECISE_LOOP 宏(定义于 faiss/impl/platform_macros.h)为标注的循环开启 fast-math 风格的优化,可显著改善自动向量化质量。该宏在 distances_autovec-inl.h 中至少出现 7 处(L33、L47、L60、L73、L86、L111、L143),说明自动向量化路径是距离函数的主力实现之一:
// autovec-inl.h
template <SIMDLevel SL>
float fvec_L2sqr_autovec(const float* x, const float* y, size_t d) {
float result = 0;
FAISS_PRAGMA_IMPRECISE_LOOP
for (size_t i = 0; i < d; i++) {
float diff = x[i] - y[i];
result += diff * diff;
}
return result;
}
// distances_avx2.cpp — compiled with -mavx2, auto-vectorizer uses AVX2
template <>
float fvec_L2sqr<SIMDLevel::AVX2>(...) {
return fvec_L2sqr_autovec<SIMDLevel::AVX2>(...);
}
3.4 simdlib 的覆盖范围与 include 顺序
smdlib 包装(simd8float32_tpl、simd8uint32_tpl)在 AVX2、AVX512、NEON 之间提供可移植的 256/512 位操作(NEON 用两个 128 位寄存器拼成 256 位)。SVE 和 RVV 没有 simdlib(simdlib_sve.h、simdlib_rvv.h 不存在)——需要 SVE 时用可变长向量(svcntw()),需要 RVV 时用可变长向量(__riscv_vsetvl*)的原始 intrinsics。这一点在 simd_levels.h 的 simd_width() 中也有编译期断言呼应:static_assert 直接禁止 simd_width<ARM_SVE> 与 simd_width<RISCV_RVV>,因为可变宽度架构不存在单一正确的 lane 数常量。
include 顺序是硬约束:-inl.h 需要在其视野内看到 simd8float32_tpl<SL> 的 ISA 专属特化,因此 simdlib 头必须先行:
// kernel-inl.h — shared kernel using simd8float32_tpl<SL>
template <size_t DIM, SIMDLevel SL>
void my_kernel(...) {
using simd_float = simd8float32_tpl<SL>;
// ... kernel using simd_float ...
}
// avx2.cpp
#include <faiss/impl/simdlib/simdlib_avx2.h> // must come first
#include "kernel-inl.h" // sees AVX2 specialization of simd8float32_tpl
// trigger instantiation here (see below)
// neon.cpp
#include <faiss/impl/simdlib/simdlib_neon.h> // must come first
#include "kernel-inl.h" // sees NEON specialization of simd8float32_tpl
// trigger instantiation here (see below)
simd_levels.h 还提供了 simd256_level_selector / simd512_level_selector 两个映射器:256 位类型把 AVX512/SVE 映射到 AVX2/NEON,512 位类型把 AVX512_SPR 映射到 AVX512——使用固定宽度 simd 类型时必须经过这类"显式选择器",这正是下文关键原则第 5 条的底层支撑。
四、七条关键原则
- 静态分派零开销。 静态模式下
with_simd_level编译为直接调用——没有 switch、没有间接跳转。从 simd_dispatch.h 可见,非FAISS_ENABLE_DD分支下整个分派退化为dispatch_with_fallback<available_levels, SINGLE_SIMD_LEVEL>的编译期递归,全部由if constexpr消解。 - 按文件设置 SIMD 标志。 只有 SIMD TU 拿到
-mavx2等标志;公共文件拿基线标志以防自动向量化引发 SIGILL。 COMPILE_SIMD_*优先于编译器宏。 分派 guard 一律使用构建系统控制的COMPILE_SIMD_AVX2,而不是编译器特性探测的__AVX2__。但更低层的头(如simdlib.h)仍用编译器宏做 intrinsics 类型选择,各司其职。- 分派一次,在循环外。 在工厂/构造函数边界调用
with_simd_level;构造出的对象把SIMDLevel作为编译期模板参数随身携带。 - 可变宽度 SIMD 不是固定宽度的 simdlib。 SVE 和 RVV 是可变宽度架构,除非有显式选择器把它们映射到受支持的固定宽度回退,否则不得经
with_simd_level_256bit、with_simd_level_512bit或simd8float32_tpl路由。 - 分派机制是私有的。
simd_dispatch.h是内部头(其文件头注释明确写着 "PRIVATE header - do not include in public APIs");对外 API 是 utils/simd_levels.h 中的SIMDConfig与SIMDLevel。 - 构建系统对等。 任何改动都必须同时反映到 CMakeLists.txt 和 Buck 的 xplat.bzl 两处。
五、渐进式迁移策略与 SINGLE_SIMD_LEVEL
向 DD 的转换是跨越数月的渐进过程,期间的要求是:静态分派必须持续可用,且不得出现性能退化。
支撑这一渐进过程的脚手架是 simd_levels.h 中的编译期常量 SINGLE_SIMD_LEVEL:
- 静态模式下,它等于编译进去的那个 SIMD 级别(
COMPILE_SIMD_AVX512_SPR→AVX512_SPR,依次降到 NONE,取第一个命中的); - DD 模式下,x86 上它恒为
NONE,ARM64 上恒为ARM_NEON(因为 NEON 在 aarch64 上强制可用)。
它存在的唯一目的,是让尚未转换的 pre-DD 代码在 DD 模式下原样可编译。但已转换的 DD 代码必须使用 with_simd_level 做真正的分派,不应依赖默认模板参数——源码注释也明确写了 "SINGLE_SIMD_LEVEL is migration scaffolding, not permanent API"。
simd_levels.h 中面向用户的 SIMDConfig 结构体则提供了统一的查询/设置 API:get_level()、set_level()(不支持的级别抛 FaissException)、supported_simd_levels()(返回已编译级别位掩码)、has_dynamic_dispatch();DD 模式下 set_level 可以真正切换运行时级别,静态模式下仅当请求级别与编译级别一致时才成功。此外还有一个与 CPU 微架构相关的细节字段 avx512_split:AMD Zen 4/Zen 4c 通过 256 位数据通路拆分执行 AVX-512,512 位指令并无吞吐收益,因此 fast-scan 的 QBS 路径在这类 CPU 上偏好 256 位内核。
六、已完成转换的模块
| 模块 | per-SIMD 文件 | 关键模式 |
|---|---|---|
| 距离函数 | distances_avx2.cpp | THE_SIMD_LEVEL + 共享 -inl.h 的自动向量化 |
| PQ 码距计算 | pq_code_distance-avx2.cpp | 以结构体模板为分派单元 |
| ScalarQuantizer | sq-avx2.cpp | X-macro 分派头(sq-dispatch.h) |
| PQ4 fast scan | impl-avx2.cpp | THE_LEVEL_TO_DISPATCH + scanner 分派 |
| 近似 top-k | approx_topk/avx2.cpp | 对 simdlib 类做显式模板实例化 |
上表每行都对应本文第三节讲过的一种触发模式,可作为新模块转换时的"先例"参照。
七、多模式构建与测试
DD 相关缺陷(缺失特化、错误的 guard、遗漏的注册项)往往只在特定构建模式下暴露,因此必须在多种模式下构建和测试:
# Build with dynamic dispatch
cmake -B build_dd -DFAISS_OPT_LEVEL=dd -DBUILD_TESTING=ON .
cmake --build build_dd -j$(nproc)
# Run C++ tests
cd build_dd && ctest --output-on-failure
# Verify dispatch at different levels (DD mode only)
FAISS_SIMD_LEVEL=NONE ctest --output-on-failure
FAISS_SIMD_LEVEL=AVX2 ctest --output-on-failure
FAISS_SIMD_LEVEL=RISCV_RVV ctest --output-on-failure
# Also build/test static modes for comparison
cmake -B build_avx2 -DFAISS_OPT_LEVEL=avx2 -DBUILD_TESTING=ON .
cmake --build build_avx2 -j$(nproc) && cd build_avx2 && ctest --output-on-failure
对 RVV,需要在 riscv64 上构建或交叉构建后,在有向量扩展的硬件或 QEMU(例如 QEMU_CPU=rv64,v=true)下运行测试。仓库提供了现成的 RISC-V 交叉工具链文件 cmake/toolchains/riscv64-linux-gnu.cmake 可供交叉构建使用。
Buck(内部)侧:
# Static (default AVX2)
buck2 build fbcode//faiss:faiss
buck2 test fbcode//faiss/tests:test_your_module
# Dynamic Dispatch
buck2 build -c faiss.dynamic_dispatch=true fbcode//faiss:faiss
buck2 test -c faiss.dynamic_dispatch=true fbcode//faiss/tests:test_your_module
仓库还配套了针对分派机制本身的测试,例如 tests/test_simd_dispatch.py、tests/test_simd_levels.cpp(含 test_simd_levels_x86_avx2.cpp、test_simd_levels_x86_avx512.cpp 的按级别变体)与 tests/test_distances_dispatch.cpp,用于校验 SIMDConfig::get_level() 与 get_dispatched_level() 的一致性、各级别结果的正确性等。
常见坑(Common Pitfalls)
- 只往一个源列表加了文件而漏掉其他架构(例如加了
FAISS_SIMD_AVX2_SRC却忘了FAISS_SIMD_NEON_SRC)——在缺失的架构上直接链接错误。 - dispatch guard 用
#ifdef __AVX2__而非#ifdef COMPILE_SIMD_AVX2——DD 模式下__AVX2__只在带-mavx2编译的 TU 中成立,公共 TU 里 guard 不成立,SIMD 代码会无声消失(编译通过、运行走标量,最隐蔽的一类 bug)。 - 忘了
NONE特化——标量/generic 构建时链接错误。 - 在热循环内部做分派而不是在边界做一次——
SIMDConfig::level的 switch 开销在循环里会被放大到可测量级别。 - 用 CMake 默认的
FAISS_OPT_LEVEL=generic以为 DD 已启用——generic 模式既无 SIMD 也无分派,必须显式FAISS_OPT_LEVEL=dd。 - 把 SVE/RVV 当固定宽度 simdlib 后端处理——它们是可变宽度 ISA,固定宽度辅助路径需要原始 intrinsics 实现或显式标量回退。
八、小结
Faiss 的 SIMD 动态分派迁移,本质上是用"独立编译单元 + 模板化特化 + 编译期分派 + 链接期承诺"四件套替代"全局编译标志":SIMDLevel 模板参数让每份实现类型安全地隔离,with_simd_level 把运行时决策压缩到循环边界外的一次 switch,COMPILE_SIMD_* 宏让公共代码可以安全地引用他处定义的特化,而按文件的 CMake 编译属性保证了"SIMD 代码只能存在于 SIMD TU"这条物理边界。对贡献者而言,按四步配方执行、遵守七条关键原则、并用 DD/静态/generic 三种模式交叉跑测试,就是在当前仓库中正确新增或迁移一个 SIMD 模块的完整路径。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00