上周组会导师提前撤了,说去开个会,让我们自由活动。

我寻思着模型还在跑,反正也没事干,就打开了nanoVLLM的源码翻了翻。
两千行,在ai的辅助下,使用一个小时读完,感觉还行——大概摸清楚是怎么回事了。然后顺手点开了vLLM的GitHub仓库。

我当时就愣在那了。
十几万行。

我盯着那个数字看了一会儿,然后关掉了,去食堂吃饭了。
(咳咳,可恶,才不是看不懂,是需要先缓一缓!)

后来煮啵认真想了想,这两个东西之间到底差了什么呢?
后来越想越觉得这个问题值得认真说一遍——因为nanoVLLM很容易就会给你一种"LLM推理我懂了"的感觉,这个感觉是真实的,但也是不完整的。
从nanoVLLM到vLLM,中间其实差了好几座山哦。

第一座山:KV Cache怎么管

先解释一下KV Cache是什么,不然后面说不清楚。

我们可以知道,LLM生成文字,是一个token一个token往外蹦的。每生成一个新token,都要和前面所有token做注意力计算,很贵。
所以大家想了个办法:把前面token的计算结果缓存起来,下次直接用,不重新算。这个缓存叫KV Cache。
有了KV Cache之后,推理快多了。但有个新问题——这个缓存要占多少显存?
这个问题,你不知道。用户可能三句话就结束,也可能聊一个小时。

nanoVLLM的做法:PagedAttention,动态分配。

它借鉴了vLLM的PagedAttention思想,把显存切成固定大小的block,用block table记录逻辑序列到物理块的映射,并且是按需动态分配的——每当序列长度到达256的倍数加一(比如257、513……)时,才分配一个新block。感兴趣可以在attention.py的forward函数里打印一下context.block_table,亲眼验证一下。

和vLLM的差距不在于"静态 vs 动态",而在于显存不够用时怎么办。vLLM有完整的抢占和swap机制:显存紧张时可以把KV Cache换出到CPU内存,等资源充裕了再换回来继续跑。nanoVLLM虽然也有基本的抢占逻辑,但没有swap机制,在显存极度紧张的场景下表现会差很多。

vLLM的做法:PagedAttention。

这个设计第一次看到的时候,煮啵在心里骂了一句可恶,太聪明了。(果然计算机技术强者辈出)
它借鉴了操作系统的虚拟内存分页——操作系统不给每个进程分配大块连续内存,而是把内存切成固定大小的"页",用多少给多少,用完还回来,其他进程可以用。
PagedAttention把这个思路搬到KV Cache:
显存切成固定大小的block,请求来了按需分配,结束了block还回去,下一个请求接着用。不同请求的KV Cache可以存在不连续的block里,用一张block table记录映射关系。
结果:显存利用率大幅提升。
同样的卡,能同时跑的请求多了好几倍。

而nanoVLLM虽然也叫PagedAttention,但它缺少了vLLM中内存抢占和交换(swap)等复杂机制,因此在高并发和内存紧张场景下的表现要差很多。

第二座山:多个请求怎么一起跑

nanoVLLM也是连续批处理——某个请求完成后会直接退出batch,新请求可以立刻插进来,而不需要等整批跑完。

那差距在哪里?差在调度器的精细程度。vLLM有Zero-Overhead Scheduler的设计目标,通过异步引擎让CPU调度和GPU计算尽量重叠,互不等待。nanoVLLM的调度逻辑相对简单,是同步的,在真实高并发场景下吞吐量差距会体现出来。另外vLLM还有Chunked Prefill——把超长Prefill切成小块和Decode请求混合处理,避免长Prefill把GPU独占太久。nanoVLLM没有这个。

在真实的高并发流量下,吞吐量差距能有好几倍。

这个连续批处理的技术最早是Orca那篇论文提出来的,发表在OSDI 2022,vLLM把它工程化落地了。(感兴趣的uu可以去看看)

第三座山:注意力计算怎么做

nanoVLLM默认使用的是FlashAttention v2(FA2),而不是PyTorch自带的标准注意力实现。
它在代码中会优先调用FlashAttention v2的高效内核,只有在未安装FlashAttention库时才会回退到标准注意力。这是因为标准注意力存在严重的性能问题。

标准注意力在计算过程中,要在GPU的HBM(显存,速度慢)和SRAM(片上缓存,速度快)之间来回搬数据——搬数据花的时间,比真正做计算花的时间还长。这在GPU优化里叫做访存瓶颈。
FlashAttention重新设计了计算顺序,让数据尽量留在SRAM里算完,减少去HBM取数据的次数。
结果:注意力计算速度提升2-4倍,显存占用降低5-20倍(因为不用存中间的大矩阵了)。

vLLM深度集成了FlashAttention-2,并且针对Prefill(处理输入prompt)和Decode(逐步生成token)两个不同阶段,分别使用了不同的优化内核。nanoVLLM虽然也用了FlashAttention v2,但它的集成和优化要简单得多,没有vLLM那么精细。

第四座山:推测解码

这个技术煮啵觉得是这几个里面最有意思的,所以说来单独说说。
LLM推理慢的根本原因是串行——每次只能生成一个token,没有办法并行。

推测解码的思路是:
先用一个小模型(比如7B的draft模型)快速猜接下来5个token。
然后用目标大模型(比如70B)一次性验证这5个猜测对不对。
如果都对,5个token一口气接受,大模型相当于一步走了5步。
如果某个位置猜错了,接受那个位置之前的,从那里重新生成。

为什么大模型验证5个token和验证1个token速度差不多?因为Transformer本来就是并行计算的,一次看5个token和看1个token,时间差不大。
实际效果:推理速度在很多任务上能提升2-3倍。
vLLM支持,nanoVLLM不支持。

还有几个,煮啵就不细说了,让我们快速过一遍

  • 多卡并行——nanoVLLM其实也实现了张量并行(TP),可以把模型切开分到多张卡上。vLLM在多卡调度的工程完成度和协同上更成熟,但这个核心机制nanoVLLM是有的。
  • 量化——把模型权重从float16压到int8甚至int4,显存占用直接砍半。vLLM支持AWQ、GPTQ、INT8。nanoVLLM不支持。
  • 生产级工程能力——vLLM有完整的异步引擎、OpenAI兼容的API服务器、Prometheus监控指标、优雅的错误处理,支持几十种模型架构。nanoVLLM能做什么:跑起来,打印出生成的文字。

对比汇总表

nanoVLLM vLLM
代码量 ~2000行 ~10万行+
KV Cache PagedAttention(无swap) PagedAttention + 抢占/Swap
批处理 连续批处理(无Chunked Prefill) 连续批处理 + Zero-Overhead调度
注意力实现 FlashAttention-2 FlashAttention-2(分阶段深度优化)
推测解码 不行 可以
多卡并行 张量并行(基础版) 张量并行(更完整)
量化支持 不行 AWQ/GPTQ/INT8
生产可用 不行 可以
学习价值 极高 直接读太难

那读nanoVLLM有没有价值呢?
煮啵认为,有,但要清楚价值在哪里。
我之前有个师兄,想搞清楚vLLM,上来直接clone代码,在里面翻了两个礼拜,还是一团雾。煮啵认为,不是他技术差,而是选择错误——vLLM的代码是为了工程正确性和性能写的,不是为了教学写的,大量的抽象层把核心逻辑藏得很深。
相当于,nanoVLLM是地图。
有了地图再进vLLM,看到的是一个个在解决具体问题的设计决策;没有地图直接进,看到的是密密麻麻的建筑,不知道哪里是入口。

从nanoVLLM到vLLM,路线怎么走

说点实操的。

第一步:把nanoVLLM真正搞懂,不只是读懂(1-2周)

不只是读,是改。
跑起来,把采样策略改一改,把KV Cache预分配大小改小,看看什么时候OOM,在关键位置加计时,看看每一步到底花了多少时间。
改的过程比读的过程理解深多了,跑过实验的人估计都知道这个感觉。
然后画一张数据流图——一个请求进来,从文字到token id,到embedding,到注意力计算,到KV Cache,到采样,到输出。这张图画出来,推理流程就真正刻到脑子里了,而不是记在纸上。

第二步:读三篇论文(2-3周)

这三篇是理解vLLM的地基,跳过任何一篇,读源码都会卡:

  • PagedAttention论文——Efficient Memory Management for Large Language Model Serving with PagedAttention,2023年SOSP。重点看Section 3和4,搞清楚block怎么管,block table怎么维护,显存不够时怎么抢占。
  • FlashAttention论文——FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness,2022年NeurIPS。数学推导可以跳,重点理解为什么重设计计算顺序能减少HBM访问。
  • Orca论文——A Distributed Serving System for Transformer-Based Generative Models,2022年OSDI。重点看Section 3,连续批处理的调度设计。

读完这三篇,再看vLLM的代码,相信家人们的感觉会完全不一样。
如果感觉没有变化,那么。。。

第三步:读vLLM源码,按模块来(3-4周)

不要从头到尾读,按模块来:
先看core/scheduler.py——vLLM的心脏,管理waiting、running、swapped三个队列,决定哪些请求进入下一步,显存不够时怎么抢占。对比nanoVLLM的简单调度逻辑,差距一下子就清晰了。
再看core/block_manager.py——PagedAttention的工程实现,block怎么分配,block table怎么维护。
然后看attention/——这里有个很多人读源码会错过的细节:Prefill阶段(处理输入prompt)和Decode阶段(逐步生成token)用的是不同的注意力kernel。前者计算密集,一次处理很多token;后者访存密集,每次只生成一个token但要读大量KV Cache。针对两个阶段分别优化。
(另外可以使用ai辅助哈哈哈,这里煮啵推荐字节的trae,毕竟不要钱哈哈哈哈)

第四步:自己实现一个关键特性(1-2个月)

光读不够,要自己写。
三个练手项目按难度排:
给nanoVLLM加连续批处理——能做到"某个请求完成立刻加入新请求"就算成功。
实现简单的PagedAttention——把静态预分配改成分块管理,能按需分配block,用block table记录映射。
给nanoVLLM加推测解码——加载两个模型,实现完整的草稿生成和验证流程,对比速度差异。
做完任何一个,理解都会比只读源码深很多。

结语

读nanoVLLM的那天下午,煮啵读完的时候好像有一种很清晰的感觉——LLM推理的本质我知道了,骨架在脑子里了。
然后打开vLLM,那种获得感碎了哈哈哈哈哈。

但后来我想通了。
vLLM多出来的那十几万行,每一行背后都是一个nanoVLLM没有处理的问题——显存不够用了,多个请求堵着了,一张卡放不下了,速度还不够快……
每一个问题被解决,系统就长大一点。

nanoVLLM让你看到这个系统最开始的样子。vLLM让你看到它被问题逼着、一步步成长成现在这个样子。
两个都值得看。
顺序别搞反了就好。

加油加油哈哈哈哈,下课下课!

Logo

免费领 200 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐