中级农民
- 积分
- 253
- 大米
- 颗
- 鳄梨
- 个
- 水井
- 尺
- 蓝莓
- 颗
- 萝卜
- 根
- 小米
- 粒
- 学分
- 个
- 注册时间
- 2015-10-6
- 最后登录
- 1970-1-1
|
注册一亩三分地论坛,查看更多干货!
您需要 登录 才可以下载或查看附件。没有帐号?注册账号 
x
下面全是手写+AI Format润色为了流畅和连贯性
接着之前 FlashAttention 那篇,聊点 serving 相关的。
我现在准备这块,会拿一个问题往下追:一张卡放得下一个 8B 模型,那它能同时服务多少人?
这个问题看着简单,真要算就会发现条件不够。输入多长,输出多长,能等多久,都是影响结果的。100 个请求进了队列,也能叫“支持 100 并发”,但最后一个等半分钟才出字,这个数字就没什么意思。
先算 KV。
假设一个普通全注意力模型,32 层,32 个 query heads,8 个 KV heads,每个 head 的维度是 128,KV 用 BF16。先按单卡算,不考虑前缀共享。
每个 token 要存的 KV:
2 × 32 × 8 × 128 × 2 bytes
= 128 KiB
前面的 2 是 K 和 V,后面的 2 是 BF16。
这里别顺手把 32 个 query heads 填进去。GQA 存的是 8 个 KV heads,填错直接差四倍。另外,权重用了 INT4,不代表 KV 也跟着变成 INT4,这个得看实际配置。
按这个例子,一个请求的 KV 占用是:
4K tokens → 512 MiB
8K tokens → 1 GiB
32K tokens → 4 GiB
这个长度包括输入和已经生成的 token。8K 输入再生成 2K,最后就是大约 1.25 GiB,不能只算 prompt。
假设扣完权重、运行时开销和预留空间,剩下 24 GiB 给 KV,那么总预算是:
24 GiB / 128 KiB = 196,608 tokens
所以我会先说,能放下大约 19.6 万个 cached tokens。至于多少请求,看它们各自多长。
比如 4 条 32K,加上 8 条 8K,刚好用完。才 12 条请求。还没给它们继续生成留空间。
这时候就能看出来,光配一个 max_num_seqs=64,不会凭空多出 64 条请求的容量。
那 PagedAttention 呢?
它把 KV 按 block 分配,不要求每个请求占一整段连续显存。长一点就多分一个 block,结束了再还回去。这样不用一开始就按最大长度给所有请求留空间。
假设一个 block 放 16 个 token,来了一个 8,193-token 的请求:
ceil(8193 / 16) = 513 blocks
最后一个 block 只用一个位置,浪费 15 个位置。按上面的配置算,不到 2 MiB。
这个浪费已经挺小了。但前面那 8,193 个 token 对应的 K/V,还是得老老实实存着。分页改善的是分配和共享,不会把真实数据压没。
PagedAttention 论文:https://arxiv.org/abs/2309.06180
还有一笔账,我觉得比“最多占多少显存”更容易漏:占多久。
两个请求峰值都用 1 GiB,一个很快结束,一个慢慢生成几十秒,对后面的请求影响肯定不一样。
做个简化计算。输入长度是 P,输出长度是 O,生成速度先假设固定为 r,每个 token 的 KV 大小是 k。
decode 期间,KV 从 kP 慢慢长到 k(P+O)。忽略 prefill 和共享,这段时间占用的“显存 × 时间”大概是:
k × (P + O/2) × O/r
还是上面的配置,8K 输入、2K 输出,假设每秒生成 50 个 token:
生成时间:2048 / 50 ≈ 41 秒
平均 KV:
(8192 + 1024) × 128 KiB
= 1.125 GiB
显存 × 时间:
约 46 GiB·秒
实际生成速度当然不会一直固定,这里就是算个量级。
长输出麻烦的地方也在这儿:它一边继续长 KV,一边迟迟不释放。只限制输入长度,解决不了这部分压力。
然后看计算。
prefill 一次处理很多输入 token,线性层比较容易形成大的矩阵乘法。普通 decode 每条请求每步只有一个新 token,小 batch 时很容易卡在带宽上。
batching 为什么有用,也可以粗算。
假设 dense 模型有 N 个参数,每个权重 s 字节,一个 decode batch 有 B 条请求。只算权重相关的线性层,暂时不管 attention、通信这些:
计算量约:2NB FLOPs
读权重约:Ns bytes
算术强度约:2B/s FLOPs/byte
BF16 的 s=2,结果就约等于 B。
batch=1 时,约 1 FLOP/byte。batch 上来,同一份权重有机会服务更多 token,读进来的数据就能多算几下。这是 batching 能提高利用率的一个原因。
但别接着推成“batch 越大越好”。
batch 大了,KV 也多。上下文越长,attention 要读取的历史 K/V 越多。算到后面可能换了瓶颈,不能一直拿小 batch 时的结论往上套。
用户体验也可能变差。拿一组假设数字:
8 条请求,一轮 20ms:
8 / 0.020 = 400 output tokens/s
16 条请求,一轮 30ms:
16 / 0.030 ≈ 533 output tokens/s
整机吞吐确实涨了。但单个用户等下一个字,从 20ms 变成了 30ms。
离线跑数据可能觉得不错,语音场景可能不接受。面试官问“怎么提高吞吐”,我会先确认延迟要求能不能动,不然优化目标都没对齐。
再往里加长 prompt。
假设已经有几十条请求在 decode,这时候进来一段很长的输入。把 prefill 整块塞进一次迭代,正在出字的人就可能一起卡一下。
Chunked prefill 会把这段输入拆开,让 decode 穿插执行。这里几个参数得分清:
max_model_len
一条请求允许的上下文长度上限
max_num_seqs
一次迭代最多调度多少条序列
max_num_batched_tokens
一次迭代最多处理多少 token
都不是 KV 总容量。
比如 token budget 是 2,048,当前有 64 条普通 decode,每条这一步处理一个 token,先用掉约 64,剩下约 1,984 才给 prefill。
所以一个 8K prompt 要分几次处理,期间还可能有其他请求进来。
当前 vLLM 文档描述的 chunked prefill 策略是优先 decode,再安排 prefill。budget 调小,单次迭代可能更短,但长 prompt 得等更多次;调大,则要看看正在生成的请求有没有被拖慢。这个参数不能脱离 workload 抄。
vLLM 调优说明:https://docs.vllm.ai/en/stable/configuration/optimization/
如果面试官继续问,P95 首字延迟突然涨了,怎么查?
我会先看时间花在哪。排队和 prefill 执行得分开量。
如果请求等了好几秒,真正 prefill 只花一小段时间,那先查到达速率、KV 压力、长短请求混跑和抢占。直接去优化 kernel,可能忙半天也没解决主要问题。
压测本身也得看。
客户端固定保持 16 个在途请求,服务器越慢,客户端发新请求的速度就越低。这种测试有用,但它会自己收油门。
线上流量不一定会。
所以我会再测固定到达速率,逐渐往上加,看看队列从哪里开始持续增长。超时和被拒绝的请求也要算进去,只看成功请求的 P95,很容易把最惨的那批漏掉。
最后是 Agent。这里还有个挺实际的问题:等工具返回的几秒钟,KV 留不留?
留着,后面继续用可能省掉重复计算,但工具没返回的时候,显存还占着。释放或者迁出去,能腾地方,下次又得付重算或传输成本。具体还要看缓存能不能命中,以及服务到底怎么实现。
而且 Agent 往往不是只调用一次模型。工具结果往上下文里一加,后面几轮的输入长度就变了。第一轮短 prompt 跑得很快,不代表整个任务都能保持这个速度。
所以我会把这道题最后落到两个数上:KV 允许多少请求驻留,以及延迟要求下每秒能稳定接多少请求。前者靠配置和长度分布估,后者要压测。再讲清楚长请求怎么限、过载怎么排队或拒绝,基本就能顺着往下聊了。 |
上一篇: OpenAI 系统设计题为什么总偏基础设施?四个高频题的拆解框架下一篇: 挂经看多了,发现大多数人不是输在不会做题
|