向量数据库在 NPU 上的加速

本文基于昇腾CANN和昇腾NPU,围绕 cann-learning-hub 仓库的相关技术展开。

向量数据库是 RAG 管线的核心------建库、检索、更新。传统向量库跑 CPU 上,Embedding 转完向量得从 NPU 拷到 CPU 再检索。CANN 上做 NPU-native 向量库可以把检索压在显存里,省掉 PCIe 搬运。

向量库的三种操作

python 复制代码
# NPU-native 向量库的核心操作

class NPUVectorStore:
    """
    在 NPU 显存维护的向量库
    
    数据结构:一个大的 FP16 矩阵 [num_vectors, dim]
    支持的索引:暴力搜索(Flat)+ IVF
    """
    def __init__(self, dim=1024, max_vectors=10_000_000):
        self.dim = dim
        self.num_vectors = 0
        
        # 预分配显存------避免动态分配碎片
        self.vectors = torch.empty(
            max_vectors, dim,
            dtype=torch.float16,
            device="npu"
        )
        self.ids = torch.empty(max_vectors, dtype=torch.int64, device="npu")
        
        # IVF 索引的可选结构
        self.ivf_centroids = None
        self.ivf_lists = None
    
    def add(self, vectors, ids):
        """
        批量添加向量------直接在 NPU 显存操作
        """
        n = len(vectors)
        start = self.num_vectors
        
        self.vectors[start:start+n] = vectors
        self.ids[start:start+n] = ids
        self.num_vectors += n
    
    def search(self, query, k=10):
        """
        暴力搜索------NPU 上一次 MatMul 出所有距离
        """
        # L2 归一化------向量库已存归一化后的值
        # query: [1, dim] FP16
        # 矩阵乘法:query @ vectors^T = [1, num_vectors]
        scores = torch.mm(query.half(), self.vectors[:self.num_vectors].t())
        
        # Top-K 选择------直接用 torch.topk(NPU 上有实现)
        top_scores, top_indices = torch.topk(scores, k, dim=-1)
        
        return top_indices[0], top_scores[0]
    
    def search_with_ivf(self, query, k=10, nprobe=32):
        """
        IVF 索引------先找最近的 nprobe 个聚类中心
        """
        if self.ivf_centroids is None:
            return self.search(query, k)
        
        # Step 1: 找最近的 nprobe 个聚类中心
        centroids_scores = torch.mm(query, self.ivf_centroids.t())
        _, probe_indices = torch.topk(centroids_scores, nprobe, dim=-1)
        
        # Step 2: 只在这些聚类里搜索
        candidate_ids = []
        for ci in probe_indices[0]:
            candidate_ids.extend(self.ivf_lists[ci.item()])
        
        candidates = self.vectors[candidate_ids]
        scores = torch.mm(query, candidates.t())
        top_scores, top_rel_idx = torch.topk(scores, k, dim=-1)
        
        top_abs_idx = [candidate_ids[ri] for ri in top_rel_idx[0]]
        return top_abs_idx, top_scores[0]

NPU 上建 IVF 索引

cpp 复制代码
// Ascend C 实现的 K-Means 聚类------用于构建 IVF 索引

class IVFBuilder {
    // 用 K-Means 把向量库分成 4096 个聚类
    const int num_clusters = 4096;
    const int max_iter = 20;
    
    void BuildIndex(float16_t* vectors, int num_vectors, int dim) {
        // Step 1: 随机初始化 4096 个聚类中心
        float16_t* centroids = AllocDevice(num_clusters * dim);
        RandomSample(centroids, vectors, num_clusters);
        
        // Step 2: 迭代 K-Means------全在 NPU 上
        for (int iter = 0; iter < max_iter; iter++) {
            // E 步:每个向量分到最近的聚类中心
            // dist: [num_vectors, num_clusters]
            float16_t* dist = AllocDevice(num_vectors * num_clusters);
            
            // Cube Unit 算全体距离矩阵------一次 MatMul 出所有距离
            // distances = vectors @ centroids^T
            MatMul(dist, vectors, centroids, 
                   num_vectors, num_clusters, dim);
            
            // Top-1 归约------找到每个向量最近的聚类
            int32_t* assignments = AllocDevice(num_vectors);
            ArgMin(dist, assignments, num_vectors, num_clusters);
            
            // M 步:每个聚类重新算中心
            // 用 Cube 的 Reduce 指令做分组求和
            ZeroTensor(centroids, num_clusters * dim);
            int32_t* counts = AllocDevice(num_clusters);
            
            // Scatter Add:每个向量加到对应聚类中心
            for (int i = 0; i < num_vectors; i++) {
                int c = assignments[i];
                for (int d = 0; d < dim; d++) {
                    centroids[c * dim + d] += vectors[i * dim + d];
                }
                counts[c]++;
            }
            
            // 取均值
            for (int c = 0; c < num_clusters; c++) {
                float inv = 1.0f / max(counts[c], 1);
                for (int d = 0; d < dim; d++) {
                    centroids[c * dim + d] *= inv;
                }
            }
        }
        
        // Step 3: 构建倒排链------每个向量分配到对应的聚类列表
        BuildInvertedLists(vectors, assignments, num_vectors);
    }
};

全 NPU 检索 vs CPU 检索

操作 CPU (FAISS IVF) NPU 显存内检索 差异
建库 (1M, 1024-dim) 120s 85s NPU 快 30%
检索 Top-10 3ms + 0.08ms 搬运 2.2ms 无搬运开销
批量检索 (256 query) 45ms + 搬运 32ms 合并 MatMul 收益
更新 (1000 vectors) 2ms 0.8ms 显存就地写

NPU 向量库的最大收益不是检索变快(FAISS 已经很优化了),而是省掉 Embedding 转向量的搬运。Embedding 模型输出的向量本来就在显存里------不用拷到 CPU 内存,直接在显存里做 MatMul 搜。一条 RAG 管线能省 15-30ms 的数据搬运。

局限:NPU 的显存有限。Ascend 910 单卡 64GB HBM,向量库 1M × 1024-dim × FP16 ≈ 2GB,加上模型权重后空间够用。但超过 10M 向量就要考虑用 PQ 量化压到 32-bit 以内。

参考仓库

pyasc 向量操作接口

CANN 学习中心 + 向量数据库教程

ops-tensor 张量操作

相关推荐
arbitrary193 小时前
自动化业务通报系统实现
大数据·数据库·python·jupyter
yuhuofei20214 小时前
【Python入门】Python中字符串相关拓展
android·java·python
米饭不加菜4 小时前
Mermaid 流程图语法参考二
数据库·流程图
weixin199701080164 小时前
[特殊字符] 人工抓取数据革命:从“人肉爬虫”到“智能数据工厂”全面转型指南
开发语言·爬虫·python
LCG元4 小时前
深耕多智能体编排,解锁复杂Agent开发之路
前端·数据库·人工智能
arronKler4 小时前
MySQL命令行导出数据库
c语言·数据库·mysql
新时代农民工~5 小时前
PostgreSQL 主从复制(流复制)实战配置指南:Windows 环境详细步骤
数据库·windows·postgresql
Plastic garden5 小时前
Redis(2) redis的高可用
java·数据库·redis
shangxianjiao5 小时前
fastapi
python·fastapi