几周前,我照着那篇著名的 PagedAttention 论文,手搓了一个迷你版 KV 缓存管理器。块管理器、空闲列表、写时复制,该有的都有。它在模拟里跑得挺欢,但那个 copy_kv_data 从头到尾都只是个占坑的空函数——我压根没让它真正碰过 GPU 内存。
这周我终于翻开了 vLLM 的源码,想看看自己离真实世界有多远。结果发现:核心思路大差不差,但细节早已天翻地覆。而且我头一次测量内存时还搞错了,从错误里学到的东西比想象的多。
![]()
我第一件事就是去找 BlockManager 这个类——论文里的核心模块,也是我从头复刻的那一个。很遗憾,它已经没了。取而代之的是一堆拆分开的文件,各自管各自的缓存。为什么要拆分?因为现在的 vLLM 允许一个模型里塞进好几种注意力机制:全注意力、滑动窗口、甚至 Mamba 层。不同机制对缓存块的形状和淘汰规则要求都不一样,所以每个缓存类型分到一个独立的管理器。我当初写的那个只有一种统一的缓存类型,和论文一模一样,自然没理由搞这一套。
如果把前缀缓存、滑动窗口、推测解码这些生产环境里的“豪华装饰”全剥掉,剩下的块计数逻辑简直跟我写的如出一辙。can_allocate 检查就是那一行:用总 token 数除以块大小,算算需要多少块。分配也好、释放也罢,都是从空闲结构里拽出一个块,把引用计数设成 1;释放的时候就减掉计数,等它归零才真正放回空闲池。
但我栽在一个小地方:我的空闲列表就是个普通的 Python 列表。vLLM 用的是货真价实的双向链表,每个块对象上直接挂着前驱和后继指针。原因是前缀缓存有时需要从链表中间把某个块拽出来,而不是只动头部,普通队列根本扛不住这种操作。我没做过前缀缓存,当然想不到这一层。
至于写时复制,这一块我从头到尾都是个空壳,读完源码反而踏实了。_apply_cow 本身也不做显存拷贝,它只是重新指一下块表条目,然后把真正的拷贝任务扔到 worker 层排队执行。管理器干的永远是记账,跟我的空桩逻辑没差。
真正的差别在于“什么时候触发写时复制”。论文里是为了波束搜索:两条序列共享一个块,直到分支往前走,需要私有副本才分家。现在的 vLLM 里,这个逻辑几乎全被前缀缓存接管了。一个请求在半路命中别人缓存的块,从那个分岔点开始需要私有副本,于是就触发了写时复制。波束搜索反而不再是这条路径的常客。
论文的一大卖点是分页能砍掉浪费的内存。我也试着实际量一下。第一次实验,我先按测试批大小精确分配了一个 GPU 张量,再跟自己的公式一比对,当然分毫不差——torch.zeros() 要多少给多少,这不叫实验,只是证明自己的算术和 PyTorch 保持一致。这套操作唯一的收获是让我意识到:如果你从不犯错,就永远发现不了自己哪里不懂。
所以这一趟下来,我认清了一件挺有意思的事:论文里的设计像是一副干净的骨架,而 vLLM 的生产实现已经在骨架上贴满了适配真实场景的肌肉。什么混合注意力、前缀缓存、双向链表,要不是为了应付越来越奇怪的模型结构和越来越苛刻的延迟要求,这些“额外细节”根本不会冒出来。当初我觉得自己的迷你版只要把空函数填上就能干活,现在看来,光是把那个空壳补全,距离一个能扛住线上洪流的 KV 缓存引擎,中间还隔着好几个“为什么”——为什么拆、为什么改成双向链表、为什么写时复制的触发源头变了。这些问题不撞一次源码,根本不会出现在你的待办列表里。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.