当前位置: 首页 > news >正文

推理引擎debug记(控制变量法)

很久之前就想复刻一下 vLLM 中的 paged Attention,相关的文章还有论文也基本上都看过一遍,苦于本人很懒,一直处于想做,但又没那么想做的状态,直至上周才下定决心把这个玩意弄完,遂去看了 nano-vLLM 的源码,仔细学习一下相关的架构和设计,然后编写了适配个人推理引擎项目的 cpp 实现。

struct Block;
using block_t = std::shared_ptr<Block>;class BlockManager;
using BlockManager_t = std::shared_ptr<BlockManager>;// kv cache block
struct Block{int _block_id;                          // 块号long long _hash = -1;                   // 哈希值 前缀+当前块信息std::vector<int64_t> _token_ids;        // 当前块缓存的token信息// std::atomic<size_t> _ref_count = 0;
};// kv cache block manager
class BlockManager{
private:tensor_t _k_cache;                                      // 底层连续缓存张量tensor_t _v_cache;size_t _token_num;                                      // 单个block所包含token缓存个数size_t _block_num;                                      // 维护的总kv cache块数std::vector<block_t> _blocks;                           // 维护所有块的信息std::unordered_map<long long, int> _hash_2_block;       // 哈希值转块号std::queue<int> _free_block_ids;                        // 可用的block的块号std::unordered_set<int> _used_block_ids;                // 已经使用的block的块号std::stack<int> _last_used_block;                       // TODO: 后续实现为 LRU cache DataType_t _dtype;                                      // 缓存数据类型DeviceType_t _device_type;                              // 缓存设备类型int _device_id;                                         // 缓存所在设备private:// 自定义哈希函数求解: 计算 token_ids[l: r + 1) 及其前缀信息 prefix 的哈希值long long _compute_hash(const int64_t *token_ids, size_t l, size_t r, long long prefix = -1);// 重新初始化blockvoid _reset(block_t block);// 更新blockvoid _update(block_t block, long long hash, const int64_t *token_ids, size_t l, size_t r);BlockManager(const size_t token_dim,        // 单个token的k/v cache占用dtype个数const size_t token_num = 32,   // 单个block所包含token总数 const size_t block_num = 8,    // block总数DataType_t dtype= LLAISYS_DTYPE_F32,                // cache的数据类型DeviceType_t device_type = LLAISYS_DEVICE_NVIDIA,   // cache所处设备类型int device_id = 0);                                 // cache所处设备号public:static BlockManager_t create(const size_t token_dim,       const size_t token_num = 128,  const size_t block_num = 8, DataType_t dtype= DTYPE_F32,               DeviceType_t device_type = DEVICE_NVIDIA, int device_id = 0); ~BlockManager() = default;// Prevent copyBlockManager(const BlockManager &) = delete;BlockManager &operator=(const BlockManager &) = delete;// Prevent moveBlockManager(BlockManager &&) = delete;BlockManager &operator=(BlockManager &&) = delete;// 根据token_ids和现有缓存信息分配block块表(对应设备上) 返回匹配前缀块总长size_t allocate(const int64_t *token_ids,       // 要分配的序列的信息const size_t ntoken,              // 序列总长std::vector<int> &block_ids);     // 分配的 block 信息 块表/总块数 (输出)// 根据id获取对应的块的指针block_t block(int block_id) { ASSERT(block_id >= 0 && static_cast<size_t>(block_id) < this->_block_num, "BlockManager_block: block_id out range.");return this->_blocks[block_id]; }// 获取底层缓存的张量tensor_t k_cache() { return this->_k_cache; };tensor_t v_cache() { return this->_v_cache; };// 返回总块数size_t block_num() { return this->_block_num; }// 返回单个block所占token数size_t token_num() { return this->_token_num; }
};

其实还没写完,因为 LRU 缓存我还没写,并且还只是单线程的情况。虽然我没去深究 nano-vLLM 中具体是怎么实现的,但凭借着对整体功能和架构的理解,我基于自己的推理引擎搞了这么个缓存管理的玩意,然后就改进了原有的 flash attention,非常好改,加个块表,计算逻辑不变,就缓存加载时改一下。

由于之前的架构非常不优雅,所以我就对一些部分进行了重构,并且在新的前向链路中增添了 kv cache 管理和 paged attention,再对 paged attention 做过单元测试后,我本以为整个链路会非常顺利的完成测试,当然模型有输出,只是全是乱码(没崩住),成功了一半。

然后就开始分析,正如题目所描述,由于这里我一下引入了分块 kv cache 和 paged attention,再加上整个系统好久没碰了,之前的修改忘记做没做测试了,所以我也不知道哪个部分错了,并且模型前向过程中的张量大的要死,直接打印又看不出什么信息,直接开始控制变量。

首先对整个链路去除 kv cache、paged attention 确保其他算子和流程的正确性,非常 amazing,输出是对的,那么问题就在我们新引入的分块 kv cache 或者 paged attention 上。推理一下由于现在还是单请求,并且 kv cache 就是连续的块,所以分块搬等价于连续搬。所以我构造了一种情况:直接拿现成的 kv cache 构造成可被 flash attention 可用的情况,验证了链路在连续搬运 kv cache 且 无 paged attention 的情况下是正确的。然后替换为 paged attention 发现也可以跑通了,所以 paged attention 也是对的!

        // 更新 KV cache: slice 返回视图, rearrange 将新数据写入对应的位置tensor_t k_layer = k_cache->slice(0, layer, layer + 1)->reshape({block_num, token_num, nkvh, dh});tensor_t v_layer = v_cache->slice(0, layer, layer + 1)->reshape({block_num, token_num, nkvh, dh});// 按块加载到 kv cachefor(size_t i = start; i < ntoken; i += token_num){size_t b = i / token_num;size_t l = std::max(start, b * token_num);size_t r = std::min(ntoken - 1, (b + 1) * token_num - 1);// 缓存块切分int block_id = block_ids[b];tensor_t k_block = k_layer->slice(0, block_id, block_id + 1)->reshape({token_num, nkvh, dh});tensor_t v_block = v_layer->slice(0, block_id, block_id + 1)->reshape({token_num, nkvh, dh});k_block = k_block->slice(0, l % token_num, r % token_num + 1)->reshape({r - l + 1, nkvh, dh});v_block = v_block->slice(0, l % token_num, r % token_num + 1)->reshape({r - l + 1, nkvh, dh});// 张量切分tensor_t k_slice = k_rope->slice(0, l - start, r - start + 1);tensor_t v_slice = v_view->slice(0, l - start, r - start + 1);ops::rearrange(k_block, k_slice);ops::rearrange(v_block, v_slice);} // 去掉 layer 维度// k_layer = k_layer->reshape({block_num * token_num, nkvh, dh});// v_layer = v_layer->reshape({block_num * token_num, nkvh, dh});// tensor_t k_slice = k_layer->slice(0, start, ntoken);// tensor_t v_slice = v_layer->slice(0, start, ntoken);// ops::rearrange(k_slice, k_rope);// ops::rearrange(v_slice, v_view);// k_layer = k_layer->slice(0, 0, ntoken);// v_layer = v_layer->slice(0, 0, ntoken);// 自注意力: paged_attention(flash v2)ops::paged_attention(attn_val, q_rope, k_layer, v_layer, dev_block_ids, block_ids.size(), ntoken, scale);// ops::self_attention(attn_val, q_rope, k_layer, v_layer, scale);

所以定位问题在分块搬运的过程中。最后发现 tensor 的 reshape 实现要求张量要连续 isContigous 连续会返回视图,复用内存,否则会调用 contigous 创建临时张量(这里我原本想利用前一种情况),而分块搬运连续两次 slice 导致张量步长不再连续,并且在搬之前还 slice 了一次(slice 只改变了 offset 并没有改变 stride),导致张量直接 reshape 就会调用 contigous 创建临时张量,而不是写入块中。

罪魁祸首:
image

解决方案:每次 tensor 每次 slice 了,就 reshape 一下,让步长变连续。

非常 amazing!

http://www.jsqmd.com/news/887134/

相关文章:

  • 嵌入式SQLite数据库实验
  • Shopify 分销和独立站分销有什么区别?完整对比指南
  • Meta Llama 4全系列深度解析:Scout/Maverick双剑合璧,原生多模态刷新开源纪录
  • 2026年Q2浙江无缝通用锁企业怎么选择?这三大趋势与一个标杆给出答案 - 2026年企业推荐榜
  • 婚介所管理系统选型指南:红娘系统/婚介小程序/婚介所小程序/婚介所管理系统/婚介管理小程序/婚介管理系统/婚介管理软件/选择指南 - 优质品牌商家
  • AI编程端到端生成前后端分离代码的完整指南
  • 35岁程序员转项目管理,PMP真能破解年龄焦虑?专业导师分点答疑
  • 第9章:AI辅助Layer2与跨链开发——Arbitrum、Optimism与跨链桥
  • STM32嵌入式视频监控及智能识别系统
  • 防水RJ45连接器全解析:IP67/IP68工业以太网接口的密封设计与选型实战
  • 2026年Q2北京正规收二手车机构排行实测对比:北京正规收车/北京淘汰车回收/北京私家车回收/北京诚信收车/北京闲置车回收/选择指南 - 优质品牌商家
  • 源码版UE5工程关联断裂修复指南:Target.cs、UBT与BuildConfiguration深度解析
  • 13456
  • 2026年权威榜单揭晓,北斗水库变形监测系统好用的三款传感器推荐
  • Product Hunt 每日热榜 | 2026-05-25
  • 20252805 2025-2026-2 《网络攻防实践》第9次作业 实践九 软件安全攻防--缓冲区溢出和shellcode
  • 2026年婚恋小程序技术实测:婚介所小程序、婚介所管理系统、婚介管理小程序、婚介管理系统、婚介管理软件、婚介系统选择指南 - 优质品牌商家
  • 2026年青岛系统门窗品牌排行:上海阳台封窗/北京断桥铝门窗/北京窗纱一体窗/北京铝合金门窗/北京门窗/合肥断桥铝门窗/选择指南 - 优质品牌商家
  • 发现一个免费的AI创作平台,一句话就能做出上线应用
  • Unity编辑器黑屏崩溃?Windows TDR超时机制详解与安全调优
  • ARIMA与LSTM双模型实战:构建金融时间序列预测系统
  • Visual C++运行库合集:一劳永逸解决Windows应用兼容性难题的完整指南
  • 2026财务分析师能力提升培训推荐课程:大学生如何打造“财务+数据+决策”高薪竞争力?
  • 2026年5月新发布好的分体空气锤平台:服务商深度解析与选型指南 - 2026年企业推荐榜
  • SSH工具对比:新手用户和熟练运维,选型逻辑有什么不同
  • 别再手动备份代码了!一文带你走进Git与GitHub的世界
  • STM32+FreeRTOS移植完整教程(基于CubeMX),从配置到验证一步到位
  • 从零到量产:DeepSeek测试用例生成落地全链路(模型微调→领域知识注入→结果可信度分级→自动化验收)
  • 森优时铁锌维发根养黑用三个月真实效果实测:内服营养养黑的客观测评
  • Claude Code 费用突然飙升怎么查?7 个缓存失效和错模型配置的常见坑