RAG 推理管线:检索增强生成的完整流程

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

RAG(Retrieval-Augmented Generation)把一个推理请求变成一个管线:先查知识库,再带着上下文生成答案。CANN 在这个管线里的角色不只是跑 LLM------Embedding 生成、rerank 排序、LLM 生成三个环节都在 NPU 上跑。全管线上 NPU 才能省掉 CPU-GPU 之间的数据搬运。

RAG 推理管线的三个步骤

python 复制代码
# RAG 全流程------所有步骤在 NPU 上完成

class RAGPipeline:
    """
    一条 RAG 请求的完整链路:
    
    用户查询 → [NPU] Embed 模型 → 向量检索 → [NPU] Rerank 
              → [NPU] LLM 生成 → 回答
    """
    def __init__(self):
        # 所有模型都在 NPU 上
        self.embed_model = load_model("bge-large-zh.om")       # Embedding
        self.rerank_model = load_model("bge-reranker-large.om") # Rerank
        self.llm = load_model("qwen-14b-chat.om")               # 生成
        
        # 向量库------存在 NPU 显存里
        self.vector_store = VectorStoreOnNPU(dim=1024)
    
    def query(self, user_question):
        # Step 1: Embedding------在 NPU 上把问题转向量
        print("Step 1: Embedding...")
        q_embedding = self.embed_model.infer(
            [user_question], 
            pooling="cls"
        )  # [1, 1024]
        
        # Step 2: 向量检索------在 NPU 上做 Top-K 搜索
        # 全程不走 CPU,向量库在显存里
        top_k_docs = self.vector_store.search(
            q_embedding, k=20
        )  # 召回 20 条文档
        
        # Step 3: Rerank------NPU 上做精排
        rerank_pairs = [(user_question, doc) for doc in top_k_docs]
        rerank_scores = self.rerank_model.infer(rerank_pairs)
        
        # 取 Top-3 作为上下文
        best_docs = [top_k_docs[i] for i in rerank_scores.topk(3)]
        
        # Step 4: 构造 Prompt 给 LLM 生成
        prompt = f"""
        基于以下文档回答问题:
        
        文档:
        {''.join(best_docs)}
        
        问题:{user_question}
        回答:
        """
        
        answer = self.llm.chat(prompt)
        return answer

向量检索在 NPU 上的实现

cpp 复制代码
// CANN 上做 Top-K 向量检索------用 Cube Unit 算余弦距离

class VectorSearchOnNPU {
    // 向量库存在 NPU 显存
    void* database;     // [num_vectors, dim] FP16
    int num_vectors;
    int dim;
    
    // 搜索结果
    struct SearchResult {
        int indices[20];
        float scores[20];
    };
    
    SearchResult TopK(float* query, int k) {
        // NPU 上没有现成的 FAISS,但可以用 MatMul 算内积
        // cos(q, x) = q·x / (|q|·|x|)
        // 向量库已 L2 归一化 → cos = q·x
        
        // 分配输出缓存
        AscendC::LocalTensor<float> scores_local;
        AscendC::LocalAlloc(scores_local, num_vectors);
        
        // 把 Query 广播成 [num_vectors, dim]
        // 然后一次 MatMul 算出所有内积
        // q: [1, dim], db: [dim, num_vectors] → scores: [1, num_vectors]
        AscendC::MatMul(scores_local, query_tensor, database_tensor,
                      AscendC::CUBE_MATRIX_TYPE::NORMAL);
        
        // Top-K 选择------NPU 没有 TopK 指令
        // 用分块 + 插入排序做
        float top_scores[20];
        int top_indices[20];
        
        // 初始化
        for (int i = 0; i < k; i++) {
            top_scores[i] = -1e10;
            top_indices[i] = -1;
        }
        
        // 遍历向量库------分 Tile 加载
        int tile_size = 4096;  // L1 Buffer 能装下的向量数
        for (int t = 0; t < num_vectors; t += tile_size) {
            int this_tile = min(tile_size, num_vectors - t);
            
            // 搬 Tile 到 Local Memory
            AscendC::LocalTensor<float> tile_scores;
            AscendC::LocalAlloc(tile_scores, this_tile);
            AscendC::DataCopy(tile_scores, scores_local + t, this_tile);
            
            // 在 L1 上做 Top-K
            for (int i = 0; i < this_tile; i++) {
                float val = tile_scores[i];
                // 插入排序------K=20,O(num_vectors * 20)
                for (int j = k-1; j >= 0; j--) {
                    if (val > top_scores[j]) {
                        if (j < k-1) {
                            top_scores[j+1] = top_scores[j];
                            top_indices[j+1] = top_indices[j];
                        }
                        top_scores[j] = val;
                        top_indices[j] = t + i;
                    } else {
                        break;
                    }
                }
            }
        }
        
        return SearchResult{top_indices, top_scores};
    }
};

把 Top-K 检索搬到 NPU 上的收益:省掉 Embedding 结果从 NPU→CPU→NPU 的两次 PCIe 搬运。100 万向量的检索在 NPU 上约 8ms------跟 CPU 上的 FAISS IVF 相当,但省了数据搬运的 3-5ms。

RAG 管线端到端性能

阶段 设备 时延 瓶颈
Embedding NPU 单卡 12ms (4K tokens) 小模型跑不快
Top-20 检索 NPU MatMul 8ms (1M 向量) 纯算力
Rerank NPU 单卡 35ms (20 对) 20 次小推理
LLM 生成 NPU 单卡 280ms (200 tokens) Decode 阶段

全 NPU 管线总计约 335ms,其中 Embed→CPU→检索→CPU→NPU 的搬运路径可以省 15-20ms。CANN 的 Runtime 支持多模型上下文共享显存------Embed 模型和 LLM 的显存不冲突,可以常驻。

参考仓库

RAG 推理示例

Embedding 模型适配

pyasc NPU 编程接口

相关推荐
结局无敌7 小时前
React Native for OpenHarmony 实战:三方库 react-native-modal-dialog 的鸿蒙化适配指南
深度学习·算法·react native·react.js·harmonyos
在所不辞兄7 小时前
为什么神经网络对对抗扰动和分布偏移往往比较脆弱,而人类感知相对更鲁棒?
人工智能·深度学习·神经网络·机器学习
szxinmai主板定制专家8 小时前
储能 EMS、微网控制器选型:RK3588、RK3576、RK3568+FPGA 怎么选?多网、多 CAN、EMC 设计权衡
人工智能·深度学习·机器学习·fpga开发·工业网关·rk3576·电力网关
鬼手点金9 小时前
Harness 工程:模型之外的竞争力
服务器·人工智能·深度学习·机器学习·bug
结局无敌9 小时前
React Native for OpenHarmony 实战:三方库 react-native-paper 的鸿蒙化适配指南
javascript·人工智能·深度学习·react native·react.js·ecmascript·harmonyos
在所不辞兄9 小时前
【神经网络干货】把指数级计算拆成四条一维路径:可分离 PI-DeepONet 如何破解高维 PDE
人工智能·深度学习·神经网络·机器学习
CV爱数码9 小时前
论文精读|FBPConvNet:先 FBP 再 U-Net,深度学习 CT 重建的起点
人工智能·深度学习·ct·重建
结局无敌9 小时前
React Native for OpenHarmony 实战:三方库 react-native-progress-steps 的鸿蒙化适配指南
javascript·人工智能·深度学习·react native·react.js·harmonyos
DongQiShanRen10 小时前
宇宙(上):分身创建——三条件触发与差异化演化
人工智能·深度学习·自然语言处理·数据挖掘
余槐i10 小时前
近期技术热点:从异常现象到根因定位的完整排查
人工智能·深度学习·机器学习