已关闭
[RFC]: OPQ 算法 NPU 支持技术方案设计 #38
Qanly创建于 8月10日关闭于 8月12日
8月10日 添加了label:rfc
xiangjie10
8月10日 评论:
8月10日 评论:
👋 您好,感谢向 faiss 提交 Issue!
🎉 我们已收到您的反馈,感谢你对开源社区的支持!
📅 处理时效 维护团队将在工作日 24 小时内查看并回复您的问题。
🔍 自助排查(推荐优先查看) 在等待回复期间,您可以先查阅仓库README以及历史 Issue 中相似问题的解决方案,多数问题可快速解决。
💡 为了更快定位问题,请您确保 Issue 包含:
- 清晰的问题描述
- 可复现的操作步骤
- 相关日志、截图或环境信息
我们会尽快跟进,感谢您的理解与配合!


xiangjie10
8月10日 评论:
8月10日 评论:
/label add triaged


8月10日 添加了label:triaged
8月10日 修改了issue 的描述
8月10日 修改了issue 的描述
8月10日 关联了pull request:【feat】实现NPU版本的OPQ part1
8月11日 关联了pull request:【feat】实现NPU版本的OPQ part2 doc&test
8月11日 关联了pull request:[WIP]【feat】实现NPU版本的OPQ
8月12日 关闭了 issue
8月12日 添加了label:resolved
8月12日 关联了pull request:【doc】OPQ 算法 NPU 支持技术方案设计
8月13日 关联了pull request:【fix】分批 GEMM 避免大底库一次性上传超显存
8月17日 关联了pull request:【docs】OPQ方案设计采样约束补充
11 天前 issue状态由 TODO 改变为 DONE
状态 (Status): Draft
作者 (Authors): 马千里
创建日期 (Created): 2026-08-10
更新日期 (Updated): 2026-08-10
1. 概述
1.1 简介
本提案旨在为 faiss-npu 补齐 OPQ(Optimized Product Quantization,优化乘积量化) 预变换在 NPU 平台上的完整能力:旋转矩阵的训练与应用均下沉到昇腾 NPU 执行,可直接与 IVFPQ 索引搭配使用(OPQ-IVFPQ),覆盖训练、变换、入库、检索的完整生命周期。
主要工作包括:
NpuOPQMatrix(接口层),继承 CPUfaiss::OPQMatrix,调用方持有OPQMatrix*即可直接使用,API 完全兼容OPQ(faiss/npu/impl/OPQ.cpp),将训练主循环中的 GEMM / SVD / 子空间 K-means 距离计算下沉到 NPU 算子d2 < d(降维)、d2 == d、d2 > d(输入补零到 d2)三种维度配置,与 CPU 行为一致OPQMatrix::train对齐(旋转矩阵相对误差 < 1e-2),可无缝替换 CPU OPQ 前置变换1.2 动机
背景
OPQ 通过训练一个旋转矩阵,对原始向量做旋转后再进行乘积量化,可减小各子空间之间的方差相关性,显著提升 PQ / IVFPQ 在大规模向量检索场景下的召回精度。开源 Faiss 中 OPQ 仅有 CPU(
OPQMatrix,BLAS/LAPACK 实现)与 GPU(旋转在 CPU 端训练,用户手动管理)两种路径,NPU 平台缺少对应实现。痛点
NpuIndexIVFPQ,但 OPQ 旋转矩阵的训练仍停留在 CPU(BLAS/LAPACK),无法发挥 NPU 算力价值
1.3 目标
目标
NpuOPQMatrix::train/apply_noalloc)OPQMatrix对齐:旋转矩阵A的相对误差 < 1e-2,变换后数据相对误差 < 1e-1d2 < d、d2 == d、d2 > d三种维度配置NpuOPQMatrix继承OPQMatrix,对标 GPU 方案中用户在 CPU 端管理 OPQ 的使用方式)NpuIndexIVFPQ端到端搭配(OPQ-IVFPQ),检索精度与 CPU 基线对齐2. 用例分析
2.1 功能需求
核心功能
xt = x @ A^T(GEMM)NpuIndexIVFPQ的输入,支持训练、入库、检索全流程参数规格
d(d_in)d2(d_out)-1(默认等于 d)或正整数d2 < d、升维d2 > d(输入补零)Md2 % M == 0niterOPQMatrix::niter一致niter_pq_0niter_pqmax_train_pointsProductQuantizer(d2, M, 8)对齐功能要求
A(d2 × d,行主序)train/apply内部自动设置设备上下文(DeviceScope),并对每个 NPU 算子执行aclrtSynchronizeStream同步,返回即完成,调用端无需手动同步NpuOPQMatrix::train前将外部可能修改的M同步到实现层,并校验d2 % M == 02.2 性能需求
精度验收标准
精度测试方法(对齐 faiss 基准方法):
OPQMatrix)与 NPU(NpuOPQMatrix)上训练,参数完全一致(niter、niter_pq_0等)A,计算最大绝对/相对误差验收指标:
A最大相对误差性能验收标准
性能测试方法:
对比 CPU 与 NPU 的 OPQ 训练耗时、旋转耗时(同一数据、同一参数)。
验收指标:
2.3 DFX 要求
可靠性
resources为空、维度非正、d2 % M != 0、算子调用失败(ACL 返回非 0)时抛出明确的FaissExceptionaclnnMm/aclnnSvd/ 距离 / topk)调用后检查返回码,失败即报错而非静默可测试性
faiss/npu/test/TestNpuOPQ.cpp单元测试(Google Test):TestCpuNpuTrainProduceSameMatrix:CPU/NPU 训练旋转矩阵一致性,覆盖d2 < d/d2 == d/d2 > d三种情形TestEndToEndOpqIvfpqVsCpu:OPQ + IVFPQ 端到端,对比 L1 分配一致率与 recall@10FAISS_NPU_SKIP_IF_NO_DEVICE)兼容性
NpuOPQMatrix继承 CPUfaiss::OPQMatrix,对标GpuIndexIVFPQ方案中 OPQ 的使用方式)faiss/npu/目录aclnnMm/aclnnSvd)与 IVFPQ 已有的自定义算子,无新增算子依赖3. 方案设计
3.1 总体方案
设计思路
参考 faiss CPU
OPQMatrix::train的训练流程与 faiss GPU 的分层架构(用户层 → 实现层 → 算子层 → 资源层),在 NPU 平台上开发NpuOPQMatrix(接口层)+OPQ(实现层)。核心设计要点:OPQMatrix::train一一对应,保证训练结果对齐。aclnnMm,SVD 用aclnnSvd,子空间 K-means 的距离/argmin 复用 IVFPQ 自定义算子(aclnnDistanceFlatL2MinsAtFp32+aclnnTopkFlatFp32Cpu);质心更新、空簇分裂、编码/解码等轻量逻辑留在 host 端,与 CPU 语义完全一致。aclnnSvd输出按 CPU LAPACKsgesvd的列主序布局填充(vt存左奇异向量 U、u存右奇异向量转置 VT),旋转更新rotation = V @ U^T的读取方式与 CPU 完全一致,避免布局转换引入误差。NpuOPQMatrix继承 CPUOPQMatrix,持有OPQMatrix*的调用方(如IndexPreTransform/ 用户代码)可直接替换使用。技术架构
核心流程
训练阶段
应用阶段
apply构造A^T的转置视图(at[k][j] = A[j][k],行主序),一次aclnnMm完成变换,与 CPUapply_noalloc的xt[i][j] = sum_k A[j][k] · x[i][k]语义一致。3.2 技术选型
aclnnMm(GEMM)、aclnnSvd(SVD)aclnnDistanceFlatL2MinsAtFp32+aclnnTopkFlatFp32Cpu,零新增算子getOpApiFuncAddr运行时获取,与 IVFPQ 路径一致NpuResources(shared_ptr)3.3 功能与性能设计
功能实现方案
NpuOPQMatrix(接口层,
faiss/npu/NpuOPQ.h/.cpp)faiss::OPQMatrix,持有std::shared_ptr<OPQ> impl与公开成员device(默认 0)NpuOPQMatrix(provider, d, M, d2 = -1),校验 provider 非空,d2 == -1时取dtrain(n, x):DeviceScope设置设备上下文 →impl->setNumSubQuantizers(M)(同步外部可能修改的 M,校验d2 % M == 0)→impl->train(n, x, niter, niter_pq_0, niter_pq, max_train_points, A)→ 置is_trained = true、is_orthonormal = trueapply_noalloc(n, x, xt):校验is_trained→DeviceScope→impl->apply(n, x, A.data(), xt)OPQ(实现层,
faiss/npu/impl/OPQ.h/.cpp)matmul_aclrtMalloc+aclrtMemcpy上传 →aclnnMm(fp32,cubeMathType=1)→aclrtSynchronizeStream→ 下载结果;统一封装、统一异常检查svd_aclnnSvd分解xxr = U·diag(S)·VT^T;输出按 CPU LAPACKsgesvd列主序布局填充(vt存左奇异向量 U(d2×d2)、u存右奇异向量转置 VT(d×d)、sing_val存奇异值),后续旋转更新读取方式与 CPU 完全一致distAssign_aclnnDistanceFlatL2MinsAtFp32(距离 + 每块最小距离)与aclnnTopkFlatFp32Cpu(topk=1 → argmin),分块参数(blockNum/blockSize/coreNum、sizes/attr/flag 缓冲)与 IVFPQ 路径对齐kmeansNpu_niter轮「distAssign_+ host 质心更新」;质心更新与 CPUcompute_centroids等价(按归属累加后除以计数);空簇处理与 CPUClustering对齐(按概率轮询分裂大簇,EPS=1/1024,seed=1234);结束后对最终质心补一次分配,与 CPU PQ 训练后compute_codes语义一致trainapplyxt = x @ A^T(一次aclnnMm)与 CPU 对齐的关键点
fvecs_maybe_subsample(rand_perm)rand_perm(perm, n, 1234)d = max(d_in, d_out),输入补零到 dfloat_randn(d*d, 1234)+matrix_qr(d, d)sgemm("T","N"):rotation^T × xtrainmatmul_(n, d, d2, xtrain, rotationT)ProductQuantizer::train,cp.niter = iter==0 ? niter_pq_0 : niter_pqTrain_hot_start(保留上轮质心)sgemm("N","T"):pq_recons^T × xtrainmatmul_(d2, n, d, reconsT, xtrain)sgesvd(列主序输出 u/vt)aclnnSvd输出按列主序填充,读取方式一致sgemm("T","T"):rotation = VT^T × U^Tmatmul_(d2, d2, d, Umat, VTmat)等价的rotation = V @ U^Td > d_in时每行保留前 d_in 列memmove压缩,resize 为 d2 × d_in影响范围
faiss/npu/NpuOPQ.hOPQMatrix)faiss/npu/NpuOPQ.cppfaiss/npu/impl/OPQ.hfaiss/npu/impl/OPQ.cppfaiss/npu/test/TestNpuOPQ.cppfaiss/npu/ops/distance_flat_l2_mins_at_fp32/topk_flat_fp32_cpu已有算子3.4 安全隐私与DFX设计
安全隐私
兼容性
NpuOPQMatrix继承 CPUOPQMatrix,调用方持有OPQMatrix*即可直接使用,如IndexPreTransform)faiss/npu/目录可维护性
OPQMatrix::train结构一一对应,便于后续跟进 faiss 主干演进matmul_/svd_/makeDeviceTensor等公共工具统一封装,避免重复实现可测试性
d2 < d/d2 == d/d2 > d三种维度配置、OPQ+IVFPQ 端到端、L1 分配一致率、recall@10FAISS_NPU_SKIP_IF_NO_DEVICE)可靠性
resources为空、维度非正、d2 % M != 0时抛出FaissExceptionis_trained为 false 时调用apply_noalloc抛出异常3.5 编程与调用设计
3.5.1 编程模型基本设计
开发环境
FAISS_ENABLE_NPUoption)开发约束
aclnnMm/aclnnSvd算子由 CANN 提供,距离/topk 自定义算子随 IVFPQ 一同编译部署可验收设计
3.5.2 接口定义与设计
3.5.2.1 NpuOPQMatrix 构造
OPQMatrix,内部持有NpuResources(shared_ptr)与实现层OPQ。NpuOPQMatrix(NpuResourcesProvider* provider, int d, int M, int d2 = -1); // 公开成员(继承自 faiss::OPQMatrix,构造后可修改) int device = 0; // NPU 设备号 int M; // 子量化器个数(train 前同步到实现层) int niter; // OPQ 外层迭代次数,默认 50 int niter_pq_0; // 首个外层迭代的 PQ K-means 迭代次数,默认 40 int niter_pq; // 后续外层迭代的 PQ K-means 迭代次数,默认 4 int max_train_points; // 训练点采样上限,默认 65536 std::vector<float> A; // 训练后的旋转矩阵(d2 × d,行主序) bool is_trained; // 是否已训练StandardNpuResources)-1(默认)等于 d-1或正整数#include <faiss/npu/NpuOPQ.h> #include <faiss/npu/StandardNpuResources.h> auto provider = std::make_shared<faiss::npu::StandardNpuResources>(); // d2 默认 -1 -> d=128;也支持降维(d2=64)与升维(d2=192) faiss::npu::NpuOPQMatrix opq(provider.get(), 128, 8); // 训练参数可后续修改(与 CPU OPQMatrix 一致) opq.niter = 3; opq.niter_pq_0 = 10;3.5.2.2 train / apply_noalloc
void train(idx_t n, const float* x) override; // x: n * d, 行主序, host void apply_noalloc(idx_t n, const float* x, float* xt) const override; // xt = x @ A^T异常处理:
train前校验d2 % M == 0,不满足抛出FaissExceptionapply_noalloc前校验is_trained,未训练抛出FaissExceptionFaissException调用参考代码(OPQ + IVFPQ 端到端):
// ---- 1. NPU 训练 OPQ 旋转矩阵 ---- faiss::npu::NpuOPQMatrix opq(provider.get(), d, M); opq.niter = 3; opq.train(nb, xb); // A 写入 opq.A (d2 × d, 行主序) // ---- 2. 变换底库与查询(xt = x @ A^T)---- std::vector<float> xbT(nb * d), xqT(nq * d); opq.apply_noalloc(nb, xb, xbT.data()); opq.apply_noalloc(nq, xq, xqT.data()); // ---- 3. 训练 / 入库 / 检索 NPU IVFPQ(使用旋转后的数据)---- faiss::npu::NpuIndexIVFPQConfig config; config.useNpuTrain = true; config.useFloat16LookupTables = true; faiss::npu::NpuIndexIVFPQ npuIvfpq( provider.get(), d, nlist, M, nbits, faiss::METRIC_INNER_PRODUCT, config); npuIvfpq.train(nb, xbT.data()); npuIvfpq.add_with_ids(nb, xbT.data(), ids.data()); npuIvfpq.nprobe = nprobe; npuIvfpq.search(nq, xqT.data(), k, distances, labels);3.5.2.3 Python 侧使用(与 IVFPQ 搭配)
import faiss import numpy as np d, nb, nq = 128, 50000, 64 nlist, M, nbits, nprobe, k = 1024, 8, 8, 32, 10 np.random.seed(12345) xb = np.random.random((nb, d)).astype('float32') xq = np.random.random((nq, d)).astype('float32') res = faiss.StandardNpuResources() # ---- 1. OPQ 训练 + 变换 (NPU) ---- opq = faiss.NpuOPQMatrix(res, d, M) opq.niter = 3 opq.train(xb) xb_t = np.ascontiguousarray(opq.apply_py(xb)) xq_t = np.ascontiguousarray(opq.apply_py(xq)) # ---- 2. NPU IVFPQ 训练 + 添加 + 搜索(旋转后的数据)---- config = faiss.NpuIndexIVFPQConfig() config.useNpuTrain = True config.useFloat16LookupTables = True npu_idx = faiss.NpuIndexIVFPQ(res, d, nlist, M, nbits, faiss.METRIC_INNER_PRODUCT, config) npu_idx.train(xb_t) npu_idx.add(xb_t) npu_idx.nprobe = nprobe D, I = npu_idx.search(xq_t, k)4. 缺点和风险
4.1 潜在风险
精度风险
aclnnSvd与 CPU LAPACKsgesvd的数值路径不同(分解算法、精度、收敛行为),奇异向量符号/数值可能存在微小差异;通过 SVD 输出布局按列主序对齐 + 精度验收阈值(相对误差 < 1e-2)控制性能风险
aclnnSvd性能4.2 负面影响
对 faiss 主干的影响
faiss/npu/目录下独立开发4.3 实现成本
维护成本
aclnnMm/aclnnSvd的影响OPQMatrix实现的演进(训练流程、默认参数)4.4 应对措施
OPQMatrix训练结果为基线,建立旋转矩阵与端到端 recall 的双重验收;SVD 布局严格按列主序对齐5. 现有技术
faiss 开源实现
OPQMatrix(faiss/VectorTransform.h/.cpp):本提案 NPU 实现的精度基线。训练流程:降采样(fvecs_maybe_subsample)→ 中心化(输入补零到d = max(d_in, d_out))→ 随机正交初始化(float_randn+matrix_qr)→ niter 轮外层迭代(sgemm投影 →ProductQuantizer::train(每子空间 K-means,热启动)→ 编码/解码 →sgemm重构 xxr →sgesvd→sgemm("T","T")旋转更新)→d > d_in时压缩 AGpuIndexIVFPQ:架构参考,分层设计(用户层 → 实现层 → 算子层 → 资源层);OPQ 旋转由用户在 CPU 端手动管理可复用的 faiss 组件
OPQMatrixNpuOPQMatrix的基类,公开成员(M/niter/niter_pq_0/niter_pq/max_train_points/A/is_trained)直接继承rand_perm/float_randn/matrix_qrRandomGeneratorFAISS_THROW_IF_NOT*distance_flat_l2_mins_at_fp32/topk_flat_fp32_cpu直接复用借鉴与差异
OPQMatrix::train的完整训练流程与数值语义、faiss GPU 的分层架构设计、IVFPQ 的 NPU 算子与资源管理方式aclnnMm/aclnnSvd与自定义距离/topk 算子替代 BLAS/LAPACK6. 未解决问题
待社区讨论/决策的开放问题,如硬件适配范围、参数默认值等(需在RFC通过前解决)。
附录
参考资料
faiss/VectorTransform.h/faiss/VectorTransform.cppfaiss/impl/ProductQuantizer.h/.cppfaiss/gpu/GpuIndexIVFPQ.hfaiss/npu/NpuOPQ.h/.cpp、faiss/npu/impl/OPQ.h/.cppfaiss/npu/test/TestNpuOPQ.cpp术语表
ksub = 2^nbits = 256dsub = d2 / MaclnnMm)aclnnSvd/ CPUsgesvd)文档更新计划
欢迎加入社区,感谢您对社区的贡献 🎉!