
四个知识点,看懂本地部署的瓶颈
欢迎来到我的小铺,今天继续聊聊模型本地部署。
前面写过几篇,硬件怎么选、软件怎么装、模型怎么跑等等。也聊过一些基础概念。但真正跑起来之后,我深刻地发现,有些概念不搞明白,优化就无从谈起。
这里我聊一聊我认为比较重要的知识点,也算是我的学习笔记,供学习参考。
一、内存带宽(Memory Bandwidth):为什么算力够强还是慢
想象一个厨房。
厨房里灶台很多,二十个、三十个,一排排摆开。厨师也够,每个灶台前都站着人。但食材的传送带只有一条。传送带一次只能送一盘菜,厨师们就得等。灶台空着,厨师闲着,但菜就是出不来——因为食材送不过来。
这个画面,就是大模型推理在解码阶段的真实写照。
大模型生成文本,是一个token一个token往外蹦的。每蹦一个token,就要把整个模型的全部权重,从显存搬到计算单元,跑一遍。注意,是每生成一个token,就要搬一遍全部权重。权重就是食材,显存到计算单元的通道就是那条传送带。这条传送带的吞吐量,就是内存带宽(memory bandwidth)。
推理过程分两个阶段。prefill阶段——处理输入的时候——一次处理大量token,计算密集,灶台全开,GPU确实满负荷运转。但到了decode阶段——一个一个往外生成token的时候——一次只生成一个token,计算量骤减,可还是要把全部权重搬一遍。这时候,搬数据的时间远大于算数据的时间。灶台空着,厨师闲着,就等传送带把食材送过来。
这就是为什么很多人买了算力不错但跑起来速度跟预期差很远的机器。不是灶台不够多,是传送带太窄。大模型推理的解码阶段,天生就是“喂不饱”的。
在常见的本地部署配置下,单流推理速度通常在62到83 tok/s之间。这个数字跟GPU标称的算力参数完全不成比例——一张算力动辄几百TFLOPS的卡,每秒只蹦出几十个token。原因就是那些算力在解码阶段根本用不满,真正卡脖子的是memory bandwidth。
所以如果是为了跑大模型选硬件,不能只看算力,memory bandwidth同样关键。这是我在这个过程中学到的第一件事。
二、KV缓存(KV Cache):为什么显存够大但上下文不够
想象一个班级,不断有新同学加入。
每来一个新同学,要跟在座每个同学互相认识一次。来了10个人,互相认识45对关系。来了20个人,不是90对,是190对。人越多,认识的关系不是按人头涨,是越涨越快。
这个画面,就是KV缓存(KV cache)的真实写照。每生成一个token,模型都要跟前面所有token建立关联——就是互相认识一次。存下来的这些关联就是KV cache,对应故事里那些“认识关系”。
关键在于,KV cache的占用跟上下文长度不是线性关系,是平方级增长的。
为什么是平方级?因为注意力机制要求每个token都要跟前面所有token计算关联——就像每来一个新同学都要跟在座所有人互相认识一次。上下文每多一个token,KV cache不只是多存一份,而是要跟前面所有token都产生一对关系。所以增长是平方级的,不是线性的。
拿70B模型来说,BF16精度下,128K上下文的KV cache大约要40GB。这个数字什么概念?相当于一个4-bit量化的整模型的大小。
也就是说,模型权重塞进去了,显存还剩不少,但上下文一开长,KV cache这个隐形的大胃王就把剩余显存吃光了。模型本身占的显存是固定的,KV cache是随着上下文长度和并发数动态膨胀的。
而且KV cache不是一个人用的,是共享的池子。就像学校不可能给每个班都配无限大的教室——所有班级的“认识关系”加起来,不能超过教室的总容量。推理引擎会给所有并发请求分配一个总的KV cache池,所有请求的上下文加起来,不能超过这个池子的容量。
举个例子。KV cache池的容量假设是2.33M个token。6个请求,每个占50K,总共300K,轻松。6个请求,每个占1M,总共6M——不可能,超出池子容量了。
max_model_len和max_num_seqs这两个参数,是“上限”不是“预留”。不是说设了128K的上下文、6个并发,引擎就给留好6×128K的空间。真正限制是:所有正在跑的请求的token总和,不能超过KV cache池的容量。
所以上下文长度和并发数是一对trade-off。想开长上下文,并发就得砍;想多接几个请求,上下文就得缩短。受限于KV cache池的容量,不是想开多大就开多大。这是第二件事。
三、prefill与decode:推理的两个阶段,两副面孔
想象一个食堂。
你走进食堂,点了一大桌菜。后厨收到单子,开始处理——这一步是“读菜单”,得把整张单子从头到尾看一遍,理清每道菜的做法、配料、火候。单子越长,这一步越耗时。但这一步是并行处理的,十口锅同时开火,灶台全开,效率拉满。
菜单读完了,开始一道一道往外端菜。每端一道,都要把对应的锅、配料、调料重新过一遍。这一步是一盘一盘来的,没法并行——因为只有一个出菜口。
前半段,灶台全开,忙得热火朝天。后半段,灶台闲着,出菜口堵着。同一个食堂,前后两段,瓶颈完全不一样。
这个画面,就是大模型推理两个阶段的真实写照。前半段叫prefill(预填充),后半段叫decode(解码)。prefill就是你给模型一段输入,模型一次性“读”进去,算出所有token之间的注意力关系,蹦出第一个回答token。decode就是从第一个token开始,一个一个往外蹦,直到生成完毕。
prefill对应“读菜单”,是计算密集型的——大量矩阵运算并行处理,GPU的算力能吃满。decode对应“一道一道端菜”,是内存带宽密集型的——每生成一个token都要把全部权重搬一遍,计算量小但数据搬运量大,瓶颈在带宽不在算力。
这个区别影响极大。
首先,用户等第一个字等多久,基本取决于prefill的速度。这个指标叫TTFT(Time To First Token,首token延迟)。输入越长——比如喂进去一篇长文档、一大段RAG检索结果——prefill的计算量就越大,TTFT就越长。用户看到模型“想了好久才开口”,不是模型在犹豫,是prefill在“读菜单”。
其次,prefill和decode共享同一套计算资源,它们会争抢。一个大prefill进来——比如128K的冷启动——就像有人端了一整桌的菜来结算,后面排队的全得等着。正在decode的其他请求被挤到一边,速度就要严重下跌。解决办法就是限制每人每次最多结算几样——对应到推理引擎,就是把prefill分块——chunked prefill——每步最多处理一定数量的token,给decode留出执行空间。
还有一层更深的:这两个阶段连适合的并行策略都不一样。prefill计算量大,适合多卡张量并行,加卡就能加速;decode因为每步计算量小,多卡之间的通信开销会吃掉一部分并行收益。同一个模型,prefill阶段多卡加速明显,decode阶段多卡的收益可能不如prefill那么大。
所以prefill不是一个可以略过的中间步骤,它直接决定了首token延迟、decode会不会被饿死、多卡部署的效率。理解prefill和decode的区别,是理解推理性能的钥匙。这是第三件事。
四、参数联动:为什么参数合理但系统出问题
想象一支乐队。
每个乐手都在正常演奏,音准没问题,节奏没问题,各自的段落都没问题。但合在一起就是灾难——因为没有人指挥谁先谁后、谁强谁弱。每个声部单独听都合理,组合在一起就突破了整体的边界。
每个声部就是每个参数,合在一起演奏就是参数组合运行。没有人指挥谁先谁后,就是没有考虑参数之间的联动关系。temperature、max_tokens、上下文长度、量化精度——每个参数单独看都在合理范围,但组合在一起,就可能出问题。参数不是一个个独立的旋钮,它们之间有耦合关系。
举两个例子。
推理模型的思考与回答共享预算。
现在的推理模型有thinking模式,生成回答之前会先“思考”一轮——也就是先生成一段reasoning过程,再给出正式回答。这段思考也是要消耗token的,而且量不小。在中等难度的问题上,thinking模式能生成大约12500个reasoning token。
关键在于,思考预算和回答预算共享同一个max_tokens上限。如果max_tokens设了512——这在普通对话场景下完全合理——reasoning把512个token的预算全部吃光,留给正式回答的预算是0。返回结果:content: null。
从用户角度看,就是“模型无响应了”。但实际上模型不是没响应,是思考预算把输出预算吃光了。它想完了,但没额度说了。
量化(Quantization)省空间但可能赔速度。
想象你要搬家。
家里东西太多,搬家公司车不够大。怎么办?把东西压缩——衣服用真空袋抽瘪,书拆掉书架摞起来,桌子椅子腿卸下来。原本三辆车才能装下的,现在一辆车就塞进去了。
但压缩有代价。到了新家,你得把真空袋打开、书架重新装、桌子腿拧回去——这个过程比直接搬过去慢。短途搬家还好,多花几分钟。但如果你搬进去之后天天要翻箱子找东西,每次都要先拆真空袋再装回去,这个慢就是累积的。
这个画面,就是量化(quantization)的真实写照。量化就是用更低的精度存储权重,省显存——比如把BF16压缩到4-bit,同样的模型占的显存少了一大截。压缩对应故事里的真空袋和拆装,省空间对应少装一车。
但精度降低之后,某些计算操作缺少对应的优化实现,只能退回到更低效的计算路径上跑——就像每次找东西都要先拆真空袋。短上下文时影响不大,因为计算量小,慢一点也看不出来。但长上下文时,每个token的生成都要处理前面所有的注意力状态,计算量随上下文长度增长,慢路径的劣势就被放大了——就像天天翻箱子,拆装的慢是累积的。实测中,长上下文场景下解码速度会出现数量级的下降。省了显存,但在特定场景下赔了速度。
这两个例子,每一个都是参数单独看合理,组合起来出问题。thinking是功能、max_tokens是限制、quantization是压缩——它们各管各的,但拼在一起跑,就得考虑它们之间的联动。
五、本地部署是一个持续性的工程
指望一次部署,终身受用,这个是不太现实的。不同的模型,不同的量化方式,不同的使用场合,就对应着不同的调优方案。
算力、memory bandwidth、显存、KV cache、采样参数、引擎策略,这些变量之间有耦合关系。动一个,牵一发动全身。
越学习,越折腾,需要了解的知识就越多。写下来,是为了记住,也给同样在折腾的人一个参考。
最后几个知识点划划重点:
- 推理慢不一定是算力不够,可能是memory bandwidth卡脖子。解码阶段GPU大部分时间在等数据搬过来,不是在算。选硬件不能只看TFLOPS。
- 上下文长度和并发数是trade-off。KV cache跟上下文长度是平方级增长,且所有并发请求共享一个池子——想开长上下文就得砍并发,想多接请求就得缩短上下文。
- prefill和decode是两副面孔。prefill计算密集决定首token延迟,decode带宽密集决定生成速度。两者共享资源会争抢,连适合的并行策略都不一样。
- 参数之间有联动,不能各调各的。thinking吃光max_tokens、quantization在特定场景赔速度——每个参数单独合理,组合起来可能突破系统边界。