已关闭
[RFC]: OPQ 算法 NPU 支持技术方案设计 #38
Qanly创建于  8月10日关闭于  8月12日
Qanly
Qanly成员
8月10日 创建

状态 (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),覆盖训练、变换、入库、检索的完整生命周期。

主要工作包括:

  • 开发 NPU 版本的 NpuOPQMatrix(接口层),继承 CPU faiss::OPQMatrix,调用方持有 OPQMatrix* 即可直接使用,API 完全兼容
  • 开发实现层 OPQfaiss/npu/impl/OPQ.cpp),将训练主循环中的 GEMM / SVD / 子空间 K-means 距离计算下沉到 NPU 算子
  • 支持 d2 < d(降维)、d2 == dd2 > d(输入补零到 d2)三种维度配置,与 CPU 行为一致
  • 训练结果与 CPU OPQMatrix::train 对齐(旋转矩阵相对误差 < 1e-2),可无缝替换 CPU OPQ 前置变换
  • 端到端验证 OPQ + IVFPQ 检索:L1 粗量化分配一致率 ≥ 70%,recall@10 ≥ 50%

1.2 动机

背景

OPQ 通过训练一个旋转矩阵,对原始向量做旋转后再进行乘积量化,可减小各子空间之间的方差相关性,显著提升 PQ / IVFPQ 在大规模向量检索场景下的召回精度。开源 Faiss 中 OPQ 仅有 CPU(OPQMatrix,BLAS/LAPACK 实现)与 GPU(旋转在 CPU 端训练,用户手动管理)两种路径,NPU 平台缺少对应实现。

痛点

  • NPU 适配缺失:faiss-npu 已支持 NpuIndexIVFPQ,但 OPQ 旋转矩阵的训练仍停留在 CPU(BLAS/LAPACK),无法发挥 NPU 算力
  • 训练耗时瓶颈:OPQ 训练包含 niter 轮外层迭代,每轮含多次 GEMM、一次 SVD 与 M 组子空间 K-means,在高维(如 1024 维)场景下 CPU 训练耗时显著
  • 精度损失:旋转矩阵若在 CPU 训练、数据变换在 NPU 执行,两端数值路径不一致,容易引入不可控的精度偏差

价值

  • 补齐 NPU 平台 OPQ 能力,OPQ 训练与应用全流程在 NPU 上完成,形成 OPQ-IVFPQ 完整链路
  • 与开源 Faiss 保持精度与 API 一致(旋转矩阵与 CPU 训练对齐),降低迁移成本
  • 复用 IVFPQ 已有的 NPU 距离/argmin 算子,实现层代码量小、可维护性好

1.3 目标

目标

  • 支持 OPQ 旋转矩阵在 NPU 上的训练应用NpuOPQMatrix::train / apply_noalloc
  • 训练精度与 CPU OPQMatrix 对齐:旋转矩阵 A 的相对误差 < 1e-2,变换后数据相对误差 < 1e-1
  • 支持 d2 < dd2 == dd2 > d 三种维度配置
  • API 与 faiss 标准 API 保持一致(NpuOPQMatrix 继承 OPQMatrix,对标 GPU 方案中用户在 CPU 端管理 OPQ 的使用方式)
  • 支持与 NpuIndexIVFPQ 端到端搭配(OPQ-IVFPQ),检索精度与 CPU 基线对齐

2. 用例分析

2.1 功能需求

核心功能

功能 说明
OPQ 训练 在 NPU 上完成旋转矩阵训练:降采样 → 中心化 → 随机正交初始化 → niter 轮外层迭代(投影 GEMM + 子空间 K-means + 编码解码 + 重构 GEMM + SVD + 旋转更新)
OPQ 应用 在 NPU 上完成旋转变换:xt = x @ A^T(GEMM)
OPQ-IVFPQ 搭配 OPQ 训练/变换结果作为 NpuIndexIVFPQ 的输入,支持训练、入库、检索全流程

参数规格

参数 取值 备注
输入维度 d (d_in) 任意正整数 如 128 / 1024
输出维度 d2 (d_out) -1(默认等于 d)或正整数 支持降维 d2 < d、升维 d2 > d(输入补零)
子量化器个数 M ≥ 1,须满足 d2 % M == 0 建议与 IVFPQ 的 M 一致
外层迭代次数 niter 默认 50 与 CPU OPQMatrix::niter 一致
首个外层迭代 PQ 迭代数 niter_pq_0 默认 40 与 CPU 一致
后续外层迭代 PQ 迭代数 niter_pq 默认 4 与 CPU 一致
训练采样上限 max_train_points 默认 65536(256²) 超过时均匀采样,CPU/NPU 行为一致
PQ 每子空间编码位数 8(ksub=256) 与 CPU ProductQuantizer(d2, M, 8) 对齐
距离度量 L2 OPQ 训练内部使用 L2 距离 K-means

功能要求

  • 支持完整的 OPQ 训练 → 应用流程,训练结果直接写入公开成员 A(d2 × d,行主序)
  • 支持与 IVFPQ 端到端搭配(OPQ-IVFPQ),训练、入库、检索时对数据先旋转再使用
  • train / apply 内部自动设置设备上下文(DeviceScope),并对每个 NPU 算子执行 aclrtSynchronizeStream 同步,返回即完成,调用端无需手动同步
  • NpuOPQMatrix::train 前将外部可能修改的 M 同步到实现层,并校验 d2 % M == 0
  • 复用 IVFPQ 已有的 NPU 距离/argmin 算子,不新增算子开发

2.2 性能需求

精度验收标准

精度测试方法(对齐 faiss 基准方法):

  1. 使用同一份随机数据(如 128 维 × 10000 条)分别在 CPU(OPQMatrix)与 NPU(NpuOPQMatrix)上训练,参数完全一致(niterniter_pq_0 等)
  2. 逐元素对比旋转矩阵 A,计算最大绝对/相对误差
  3. 端到端 OPQ + IVFPQ:两端 IVFPQ 使用同一份(NPU 变换的)数据训练/入库/检索,对比 L1 粗量化分配与检索 top-k 结果

验收指标

验收项 指标
旋转矩阵 A 最大相对误差 < 1e-2
变换后数据最大相对误差 < 1e-1
L1 粗量化分配一致率 ≥ 70%
检索 recall@10 ≥ 50%

性能验收标准

性能测试方法

对比 CPU 与 NPU 的 OPQ 训练耗时、旋转耗时(同一数据、同一参数)。

验收指标

验收项 指标
NPU OPQ 训练耗时 ≤ CPU 训练耗时(预期随维度/迭代数提升更明显)
NPU 旋转耗时 ≤ CPU 旋转耗时

2.3 DFX 要求

可靠性

  • 算法计算结果正确性保证(与 CPU 基线对齐)
  • 异常输入检查:resources 为空、维度非正、d2 % M != 0、算子调用失败(ACL 返回非 0)时抛出明确的 FaissException
  • 每个 NPU 算子(aclnnMm / aclnnSvd / 距离 / topk)调用后检查返回码,失败即报错而非静默

可测试性

  • 开发 faiss/npu/test/TestNpuOPQ.cpp 单元测试(Google Test):
    • TestCpuNpuTrainProduceSameMatrix:CPU/NPU 训练旋转矩阵一致性,覆盖 d2 < d / d2 == d / d2 > d 三种情形
    • TestEndToEndOpqIvfpqVsCpu:OPQ + IVFPQ 端到端,对比 L1 分配一致率与 recall@10
  • 无 NPU 设备时测试自动跳过(FAISS_NPU_SKIP_IF_NO_DEVICE

兼容性

  • API 设计与 faiss 标准 API 保持一致(NpuOPQMatrix 继承 CPU faiss::OPQMatrix,对标 GpuIndexIVFPQ 方案中 OPQ 的使用方式)
  • 不影响 faiss 主干代码及其他算法,所有 NPU 实现位于 faiss/npu/ 目录
  • 仅依赖现有 CANN 算子(aclnnMm / aclnnSvd)与 IVFPQ 已有的自定义算子,无新增算子依赖

3. 方案设计

3.1 总体方案

设计思路

参考 faiss CPU OPQMatrix::train 的训练流程与 faiss GPU 的分层架构(用户层 → 实现层 → 算子层 → 资源层),在 NPU 平台上开发 NpuOPQMatrix(接口层)+ OPQ(实现层)。核心设计要点:

  1. 训练主循环镜像 CPU:降采样、中心化、随机正交初始化、niter 轮外层迭代(投影 → PQ K-means → 编码/解码 → 重构 → SVD → 旋转更新)与 CPU OPQMatrix::train 一一对应,保证训练结果对齐。
  2. 数值路径在 NPU:训练中的 GEMM(投影、重构、旋转更新)用 aclnnMm,SVD 用 aclnnSvd,子空间 K-means 的距离/argmin 复用 IVFPQ 自定义算子(aclnnDistanceFlatL2MinsAtFp32 + aclnnTopkFlatFp32Cpu);质心更新、空簇分裂、编码/解码等轻量逻辑留在 host 端,与 CPU 语义完全一致。
  3. 与 CPU 布局对齐aclnnSvd 输出按 CPU LAPACK sgesvd 的列主序布局填充(vt 存左奇异向量 U、u 存右奇异向量转置 VT),旋转更新 rotation = V @ U^T 的读取方式与 CPU 完全一致,避免布局转换引入误差。
  4. 接口零改动NpuOPQMatrix 继承 CPU OPQMatrix,持有 OPQMatrix* 的调用方(如 IndexPreTransform / 用户代码)可直接替换使用。

技术架构

┌──────────────────────────────────────────────────────────┐
│                   用户应用层                              │
│  NpuOPQMatrix (继承 CPU OPQMatrix, 接口零改动)           │
│  IndexPreTransform / 用户代码 (OPQMatrix*)               │
└──────────────────────────────────────────────────────────┘
                       ↓
┌──────────────────────────────────────────────────────────┐
│                API 层 (NpuOPQMatrix)                     │
│  - 构造: 持有 NpuResources (shared_ptr)                  │
│  - train: DeviceScope + 同步 M → impl->train → A         │
│  - apply_noalloc: DeviceScope → impl->apply              │
└──────────────────────────────────────────────────────────┘
                       ↓
┌──────────────────────────────────────────────────────────┐
│                   实现层 (OPQ)                           │
│  ├── matmul_: host↔device GEMM (aclnnMm)                 │
│  ├── svd_:    host↔device SVD  (aclnnSvd, 列主序对齐)    │
│  ├── kmeansNpu_: 距离/argmin 上 NPU + host 质心更新       │
│  ├── distAssign_: L2 距离矩阵 + argmin (自定义算子)      │
│  └── train / apply: 训练主循环与旋转变换                 │
└──────────────────────────────────────────────────────────┘
                       ↓
┌──────────────────────────────────────────────────────────┐
│                   算子层 (ops)                           │
│  aclnnMm                      (GEMM, AICORE)             │
│  aclnnSvd                     (SVD,  AICORE)             │
│  aclnnDistanceFlatL2MinsAtFp32 (L2 距离, 自定义)         │
│  aclnnTopkFlatFp32Cpu          (topk=1 argmin, AICPU)    │
└──────────────────────────────────────────────────────────┘
                       ↓
┌──────────────────────────────────────────────────────────┐
│              ACL Runtime + NPU                           │
└──────────────────────────────────────────────────────────┘

核心流程

训练阶段

输入训练数据 x (n × d_in)
  │
  ├─ 1. 降采样: n > max_train_points 时 rand_perm 均匀采样 (seed=1234)
  ├─ 2. 中心化: 按 d_in 维统计均值并减均值, 其余 d - d_in 列补零 (d = max(d_in, d2))
  ├─ 3. 旋转初始化: A 为空 → float_randn(d*d, seed=1234) + matrix_qr(d, d), 截取前 d*d2;
  │                  A 非空 → 校验尺寸后直接复用 (与 CPU 一致)
  │
  └─ 4. 外层迭代 iter in 0..niter:
        a. 投影:   xproj = xtrain @ rotation^T                    (aclnnMm)
        b. PQ 训练: 对 M 个子空间独立 K-means (ksub=256)
                    - 距离/argmin: 自定义算子 (distAssign_)
                    - 质心更新/空簇分裂: host 端 (与 CPU compute_centroids 等价)
                    - 首个外层迭代随机初始化, 之后热启动 (Train_hot_start)
        c. 编码/解码: codes = argmin 索引, pq_recons = 对应质心     (host, 轻量)
        d. 重构:   xxr = pq_recons^T @ xtrain                     (aclnnMm)
        e. SVD:    xxr = U · diag(S) · VT^T                       (aclnnSvd)
        f. 旋转:   rotation = V @ U^T                             (aclnnMm)
  │
  └─ 5. d2 > d_in 时压缩 A: 每行只保留前 d_in 列, resize 为 d2 × d_in (与 CPU revert 一致)
graph TD
    A[训练数据 x] --> B[1. 降采样 max_train_points]
    B --> C[2. 中心化 xtrain<br/>减均值 + 补零到 d]
    C --> D{3. A 是否为空?}
    D -->|是| E[随机正交初始化<br/>float_randn + matrix_qr]
    D -->|否| F[复用外部 A]
    E --> G[4. 外层迭代 iter 0..niter]
    F --> G
    G --> H[a. 投影 xproj = xtrain @ rotation^T<br/>aclnnMm]
    H --> I[b. 子空间 PQ K-means × M<br/>自定义距离/topk 算子 + host 质心更新]
    I --> J[c. 编码 + 解码 → pq_recons]
    J --> K[d. xxr = pq_recons^T @ xtrain<br/>aclnnMm]
    K --> L[e. SVD(xxr) → U, VT<br/>aclnnSvd]
    L --> M[f. rotation = V @ U^T<br/>aclnnMm]
    M --> G
    G -->|niter 轮结束| N[5. d2 > d_in 时压缩 A<br/>输出旋转矩阵 A d2 × d_in]

应用阶段

xt (n × d2) = x (n × d) @ A^T        (aclnnMm)

apply 构造 A^T 的转置视图(at[k][j] = A[j][k],行主序),一次 aclnnMm 完成变换,与 CPU apply_noallocxt[i][j] = sum_k A[j][k] · x[i][k] 语义一致。

3.2 技术选型

选型 说明
NPU 编程框架 ACL Runtime + aclnn 算子 aclnnMm(GEMM)、aclnnSvd(SVD)
距离/topk 算子 复用 IVFPQ 自定义算子 aclnnDistanceFlatL2MinsAtFp32 + aclnnTopkFlatFp32Cpu,零新增算子
算子加载方式 函数指针延迟加载 getOpApiFuncAddr 运行时获取,与 IVFPQ 路径一致
数值对齐 与 CPU BLAS/LAPACK 语义对齐 SVD 输出列主序布局、GEMM 转置语义、随机种子(1234)与 CPU 一致
资源管理 NpuResources(shared_ptr) 资源生命周期与变换对象绑定,调用端无需保活

3.3 功能与性能设计

功能实现方案

NpuOPQMatrix(接口层,faiss/npu/NpuOPQ.h/.cpp

  • 继承 CPU faiss::OPQMatrix,持有 std::shared_ptr<OPQ> impl 与公开成员 device(默认 0)
  • 构造:NpuOPQMatrix(provider, d, M, d2 = -1),校验 provider 非空,d2 == -1 时取 d
  • train(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 = trueis_orthonormal = true
  • apply_noalloc(n, x, xt):校验 is_trainedDeviceScopeimpl->apply(n, x, A.data(), xt)

OPQ(实现层,faiss/npu/impl/OPQ.h/.cpp

函数 实现要点
matmul_ host↔device GEMM:aclrtMalloc + aclrtMemcpy 上传 → aclnnMm(fp32,cubeMathType=1)→ aclrtSynchronizeStream → 下载结果;统一封装、统一异常检查
svd_ host↔device SVD:aclnnSvd 分解 xxr = U·diag(S)·VT^T;输出按 CPU LAPACK sgesvd 列主序布局填充(vt 存左奇异向量 U(d2×d2)、u 存右奇异向量转置 VT(d×d)、sing_val 存奇异值),后续旋转更新读取方式与 CPU 完全一致
distAssign_ 计算 x(n×dim)到质心(nlist×dim)的全量 L2 距离矩阵并取最近质心(argmin):复用 IVFPQ 自定义算子 aclnnDistanceFlatL2MinsAtFp32(距离 + 每块最小距离)与 aclnnTopkFlatFp32Cpu(topk=1 → argmin),分块参数(blockNum/blockSize/coreNum、sizes/attr/flag 缓冲)与 IVFPQ 路径对齐
kmeansNpu_ 一次 K-means 完整流程:niter 轮「distAssign_ + host 质心更新」;质心更新与 CPU compute_centroids 等价(按归属累加后除以计数);空簇处理与 CPU Clustering 对齐(按概率轮询分裂大簇,EPS=1/1024,seed=1234);结束后对最终质心补一次分配,与 CPU PQ 训练后 compute_codes 语义一致
train 完整 OPQ 训练主循环(见 3.1 核心流程),跨外层迭代保留 PQ 质心(热启动)
apply xt = x @ A^T(一次 aclnnMm

与 CPU 对齐的关键点

环节 CPU 实现 NPU 实现对齐方式
降采样种子 fvecs_maybe_subsample(rand_perm) rand_perm(perm, n, 1234)
训练维度 d = max(d_in, d_out),输入补零到 d 相同,均值只统计 d_in 维
旋转初始化 float_randn(d*d, 1234) + matrix_qr(d, d) 相同
投影 GEMM sgemm("T","N"):rotation^T × xtrain matmul_(n, d, d2, xtrain, rotationT)
PQ 训练 ProductQuantizer::traincp.niter = iter==0 ? niter_pq_0 : niter_pq 每子空间独立 K-means,迭代次数相同
热启动 Train_hot_start(保留上轮质心) 质心跨外层迭代保留
重构 GEMM sgemm("N","T"):pq_recons^T × xtrain matmul_(d2, n, d, reconsT, xtrain)
SVD sgesvd(列主序输出 u/vt) aclnnSvd 输出按列主序填充,读取方式一致
旋转更新 sgemm("T","T"):rotation = VT^T × U^T matmul_(d2, d2, d, Umat, VTmat) 等价的 rotation = V @ U^T
矩阵压缩 d > d_in 时每行保留前 d_in 列 memmove 压缩,resize 为 d2 × d_in

影响范围

文件/模块 开发类型 说明
faiss/npu/NpuOPQ.h 新增 NPU OPQ 公共 API(继承 CPU OPQMatrix
faiss/npu/NpuOPQ.cpp 新增 接口层实现(train / apply_noalloc)
faiss/npu/impl/OPQ.h 新增 实现层头文件(matmul_/svd_/kmeansNpu_/distAssign_/train/apply)
faiss/npu/impl/OPQ.cpp 新增 实现层实现
faiss/npu/test/TestNpuOPQ.cpp 新增 单元测试(旋转矩阵一致性 + OPQ+IVFPQ 端到端)
faiss/npu/ops/ 复用 distance_flat_l2_mins_at_fp32 / topk_flat_fp32_cpu 已有算子

3.4 安全隐私与DFX设计

安全隐私

  • 不涉及用户敏感数据处理
  • 算子调用遵循 CANN 安全编码规范,所有 ACL 返回码均显式检查

兼容性

  • API 设计与 faiss 标准 API 保持一致(NpuOPQMatrix 继承 CPU OPQMatrix,调用方持有 OPQMatrix* 即可直接使用,如 IndexPreTransform
  • 不影响 faiss 主干代码及其他算法,所有 NPU 实现位于 faiss/npu/ 目录
  • 仅依赖现有 CANN 算子与 IVFPQ 已有自定义算子,无新增算子

可维护性

  • 分层设计(API 层 → 实现层 → 算子层),职责清晰
  • 训练主循环与 CPU OPQMatrix::train 结构一一对应,便于后续跟进 faiss 主干演进
  • matmul_ / svd_ / makeDeviceTensor 等公共工具统一封装,避免重复实现

可测试性

  • Google Test 单元测试框架
  • 测试用例覆盖:d2 < d / d2 == d / d2 > d 三种维度配置、OPQ+IVFPQ 端到端、L1 分配一致率、recall@10
  • 无 NPU 设备自动跳过(FAISS_NPU_SKIP_IF_NO_DEVICE

可靠性

  • 算法计算结果正确性保证(与 CPU 基线对齐)
  • 异常情况处理:
    • resources 为空、维度非正、d2 % M != 0 时抛出 FaissException
    • 算子调用失败(ACL 返回非 0)时抛出明确异常并附错误码
    • is_trained 为 false 时调用 apply_noalloc 抛出异常

3.5 编程与调用设计

3.5.1 编程模型基本设计

开发环境

要求
硬件平台 Ascend NPU(Atlas 系列)
编程语言 C++
NPU 编程框架 ACL Runtime + aclnn 算子 + 自定义算子(复用 IVFPQ)
构建系统 CMake(FAISS_ENABLE_NPU option)
测试框架 Google Test

开发约束

  • 必须安装 CANN 工具链与 NPU 驱动
  • OPQ 训练依赖的 aclnnMm / aclnnSvd 算子由 CANN 提供,距离/topk 自定义算子随 IVFPQ 一同编译部署

可验收设计

验收类型 环境 标准
功能验收 NPU 单卡 单元测试全部通过
精度验收 NPU 单卡 旋转矩阵相对误差 < 1e-2,端到端 recall 对齐 CPU
性能验收 NPU 单卡 训练/旋转耗时 ≤ CPU

3.5.2 接口定义与设计

3.5.2.1 NpuOPQMatrix 构造

  • 接口描述:构造 NPU OPQ 变换对象,继承 CPU 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;         // 是否已训练
  • 参数说明
参数名称 输入/输出 类型 描述 取值范围
provider 输入 NpuResourcesProvider* NPU 资源提供者(如 StandardNpuResources 非空
d 输入 int 输入维度(d_in) 正整数
M 输入 int 子量化器个数 ≥ 1,须满足 d2 % M == 0
d2 输入 int 输出维度(d_out);-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

  • 接口描述:在 NPU 上训练 OPQ 旋转矩阵 / 对数据施加旋转。
  • 接口原型
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
  • 参数说明
参数名称 输入/输出 类型 描述 取值范围
n 输入 idx_t 向量数量 正整数
x 输入 const float* 训练/变换数据,shape [n, d] 非空
xt 输出 float* 变换结果,shape [n, d2] 非空(apply)
  • 异常处理

    • train 前校验 d2 % M == 0,不满足抛出 FaissException
    • apply_noalloc 前校验 is_trained,未训练抛出 FaissException
    • 所有 ACL 算子调用失败抛出带错误码的 FaissException
  • 调用参考代码(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 LAPACK sgesvd 的数值路径不同(分解算法、精度、收敛行为),奇异向量符号/数值可能存在微小差异;通过 SVD 输出布局按列主序对齐 + 精度验收阈值(相对误差 < 1e-2)控制
  • K-means 距离计算在 NPU(fp32 自定义算子)执行,与 CPU 浮点累加顺序不同,质心归属在边界样本上可能不同(与 IVFPQ 训练路径同类问题)

性能风险

  • 训练循环中每轮迭代涉及多次 host↔device 拷贝(GEMM/SVD 输入输出),小规模训练数据下拷贝开销可能掩盖 NPU 算力优势
  • 质心更新、编码/解码在 host 端串行执行,M 较大(如 32)时子空间循环可能成为瓶颈
  • SVD 在高维(d2=1024)场景的计算量较大,需评估 aclnnSvd 性能

4.2 负面影响

对 faiss 主干的影响

  • 不修改 faiss 主干代码,所有 NPU 实现在 faiss/npu/ 目录下独立开发
  • 不影响其他算法(Flat、IVFFlat、IVFPQ 等)

4.3 实现成本

维护成本

  • 需跟进 CANN 版本更新对 aclnnMm / aclnnSvd 的影响
  • 需跟进 faiss 主干 OPQMatrix 实现的演进(训练流程、默认参数)
  • 自定义距离/topk 算子的分块参数(blockNum/blockSize)与 SoC 核心数相关,跨芯片适配需验证

4.4 应对措施

  • 精度风险:以 CPU OPQMatrix 训练结果为基线,建立旋转矩阵与端到端 recall 的双重验收;SVD 布局严格按列主序对齐
  • 性能风险:训练耗时对比 CPU 基准;若拷贝开销明显,可评估训练数据常驻 device、减少往返拷贝的优化方向
  • 充分的单元测试与集成测试:覆盖三种维度配置与 OPQ+IVFPQ 端到端
  • 维护成本:与 IVFPQ 共用算子与工具函数,减少独立维护面

5. 现有技术

faiss 开源实现

  • faiss CPU OPQMatrixfaiss/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 → sgesvdsgemm("T","T") 旋转更新)→ d > d_in 时压缩 A
  • faiss GPU GpuIndexIVFPQ:架构参考,分层设计(用户层 → 实现层 → 算子层 → 资源层);OPQ 旋转由用户在 CPU 端手动管理

可复用的 faiss 组件

组件 来源 复用方式
OPQMatrix faiss CPU 作为 NpuOPQMatrix 的基类,公开成员(M/niter/niter_pq_0/niter_pq/max_train_points/A/is_trained)直接继承
rand_perm / float_randn / matrix_qr faiss utils 降采样与随机正交初始化,与 CPU 种子一致(1234)
RandomGenerator faiss utils 空簇分裂的概率轮询
FAISS_THROW_IF_NOT* faiss impl 异常检查体系
距离/topk 自定义算子 faiss/npu(IVFPQ) distance_flat_l2_mins_at_fp32 / topk_flat_fp32_cpu 直接复用

借鉴与差异

  • 借鉴:CPU OPQMatrix::train 的完整训练流程与数值语义、faiss GPU 的分层架构设计、IVFPQ 的 NPU 算子与资源管理方式
  • 差异:与 GPU 方案(OPQ 在 CPU 端训练)不同,本提案 OPQ 训练与应用均在 NPU 执行;算子层使用 aclnnMm / aclnnSvd 与自定义距离/topk 算子替代 BLAS/LAPACK

6. 未解决问题

待社区讨论/决策的开放问题,如硬件适配范围、参数默认值等(需在RFC通过前解决)。

附录

参考资料

  • faiss CPU OPQMatrix 实现:faiss/VectorTransform.h / faiss/VectorTransform.cpp
  • faiss 产品量化实现:faiss/impl/ProductQuantizer.h/.cpp
  • faiss GPU IVFPQ 架构:faiss/gpu/GpuIndexIVFPQ.h
  • NPU OPQ 实现:faiss/npu/NpuOPQ.h/.cppfaiss/npu/impl/OPQ.h/.cpp
  • NPU OPQ 测试:faiss/npu/test/TestNpuOPQ.cpp

术语表

术语 含义
OPQ Optimized Product Quantization,优化乘积量化(含旋转矩阵预处理)
d (d_in) 输入向量维度
d2 (d_out) 输出向量维度(旋转后)
M (m) 乘积量化子空间数量
ksub 每子空间聚类中心数,ksub = 2^nbits = 256
dsub 子向量维度,dsub = d2 / M
niter OPQ 外层训练迭代次数
niter_pq_0 / niter_pq 外层迭代中的 PQ K-means 迭代次数(首轮/后续)
max_train_points 训练点采样上限
GEMM 通用矩阵乘法(aclnnMm
SVD 奇异值分解(aclnnSvd / CPU sgesvd
A OPQ 旋转矩阵(d2 × d,行主序)
L1 Coarse Quantization 阶段(IVFPQ 粗量化分配)

文档更新计划

  • RFC 评审通过后,更新《faiss-npu 用户指南》OPQ 章节,补充设计说明与 OPQ-IVFPQ 使用示例
  • 补充性能基准数据(CPU vs NPU 训练/旋转耗时对比)

欢迎加入社区,感谢您对社区的贡献 🎉!

likedislike
QanlyQanly成员
8月10日 添加了label:rfc
xiangjie10成员
8月10日 评论:

👋 您好,感谢向 faiss 提交 Issue!
🎉 我们已收到您的反馈,感谢你对开源社区的支持!

📅 处理时效 维护团队将在工作日 24 小时内查看并回复您的问题。
🔍 自助排查(推荐优先查看) 在等待回复期间,您可以先查阅仓库README以及历史 Issue 中相似问题的解决方案,多数问题可快速解决。
💡 为了更快定位问题,请您确保 Issue 包含:

  • 清晰的问题描述
  • 可复现的操作步骤
  • 相关日志、截图或环境信息
    我们会尽快跟进,感谢您的理解与配合!
likedislike
xiangjie10成员
8月10日 评论:

/label add triaged

likedislike
ascend-robotascend-robot成员
8月10日 添加了label:triaged
QanlyQanly成员
8月10日 修改了issue 的描述
QanlyQanly成员
8月10日 修改了issue 的描述
QanlyQanly成员
8月10日 关联了pull request:【feat】实现NPU版本的OPQ part1
QanlyQanly成员
8月11日 关联了pull request:【feat】实现NPU版本的OPQ part2 doc&test
QanlyQanly成员
8月11日 关联了pull request:[WIP]【feat】实现NPU版本的OPQ
ascend-robotascend-robot成员
8月12日 关闭了 issue
ascend-robotascend-robot成员
8月12日 添加了label:resolved
QanlyQanly成员
8月12日 关联了pull request:【doc】OPQ 算法 NPU 支持技术方案设计
QanlyQanly成员
8月13日 关联了pull request:【fix】分批 GEMM 避免大底库一次性上传超显存
QanlyQanly成员
8月17日 关联了pull request:【docs】OPQ方案设计采样约束补充
Xxiangjie10成员
11 天前 issue状态由 TODO 改变为 DONE