本文基于昇腾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 以内。