Concurrency Sweep · 第二轮 · RTX 4090
同样的 1024 / 256 负载,同样扫到 64 并发,但这次采样器全程跟跑了 917 秒。 第一轮只能说"TTFT 涨到了 5.8 秒",这一轮能说清楚它涨在哪一段。
把 TTFT 拆成"排队"和"真算"两段之后,结论很干脆:从 C=1 到 C=64, prefill 只涨了 5 倍,而排队从 0 涨到 379 毫秒。 并发越高,用户等的越不是计算,而是别人。
核心分解结果 · C=64
排队 379 ms + prefill 461 ms + 前端 136 ms
三段加起来 976 ms,正是客户端观测到的平均 TTFT。 其中排队从 C=1 的 0 ms 一路涨到 379 ms,是唯一一个近乎从零起步的分量。
两轮吞吐复现性
<0.2 %
7 个点全部落在 +0.02%~+0.19%,方法可靠
C=64 排队占 TTFT
38.8 %
C=1 时这一项是 0
C=64 持续功耗
440 W
峰值 461 W,贴着 450 W 功耗墙
每次迭代 token 数
321 tok
C=64 时,其中仅约 62 个是 decode
服务端的 request_queue_time 和 request_prefill_time 两个 histogram,
差分后得到每个并发点的平均排队时间与平均 prefill 时间。
两者之和与客户端观测的 TTFT 之间还差一截,那是 vLLM 前端(API server)的处理与 IPC 开销。
TTFT 三段分解(平均值,堆叠)
柱顶数字为该并发点的平均 TTFT。前端那一段由客户端 TTFT 减去服务端两段得出, 因此也吸收了窗口对齐带来的误差,不宜逐毫秒解读 —— 但它随并发从 12 ms 增长到 136 ms 这个趋势是真实的。
排队 vs prefill:谁在推高 TTFT
纵轴对数。两条线在本轮测到的范围内始终没有交叉 —— 排队一直在 prefill 下方。值得看的是斜率而不是高低: prefill 从 92 ms 涨到 461 ms,只有 5.0 倍; 排队从 C=1/C=2 的 0(低于计数器精度)、C=4 的 1.0 ms,一路涨到 379 ms —— 从第一个可测点算起是 379 倍。
两者的差距因此一路收窄:排队占 prefill 的比例从 C=4 的 0.6% 涨到 C=16 的 10.8%、C=32 的 30.4%,到 C=64 已经追到 82%。 按最后两档的对数斜率(排队 1.60 / prefill 0.16)外推,交叉会发生在 C ≈ 73 —— 这是外推,不是实测,本轮最高只压到 64。
这解释了第一轮的一个悬念
第一轮看到 C=64 的 p99 TTFT 高达 5.77 秒而 p50 只有 782 ms,当时只能推测"少数请求被排到了后面"。 现在有了直接证据:C=64 时有 13% 的采样点观测到排队,waiting 队列峰值达 49 条。 而 running 的中位数是 62,低于设定的 64 —— KV 池确实开始限制并发了。
iteration_tokens_total 记录每次 engine 迭代处理了多少 token。
如果只有 decode,这个数应该等于 batch size。实测是 batch 的 5 倍 ——
多出来的部分就是被 chunked prefill 塞进来的 prefill 块。
每次迭代的 token 构成
decode 部分按窗口内 running 的中位数估计。比例几乎不随并发变化, 恒定在 1 : 4 上下 —— 这正是输入输出比(1024 : 256)的直接反映。
这是 ITL p99 尖峰最直接的物理解释:一个正在解码的请求,每一步都要等同批次里的 prefill 块算完。C=64 时每次迭代要处理 321 个 token,其中约 259 个是 prefill, 于是解码方向的每一步都被拖长,尾部就出现了 200 ms 量级的停顿。
这是第一轮完全没有的视角。七个并发点在时间轴上依次排开, 可以直接看到队列什么时候堆起来、KV 什么时候冲顶。
运行数 / 等待数 / KV 占用
灰色分区为各并发点的测量窗口。waiting 在 C≤8 时全程为 0, 从 C=16 开始出现零星尖峰,到 C=64 变成常态。
功耗与温度
C=64 期间功耗中位数 440 W、峰值 461 W,已经贴住 4090 的默认 450 W 限制。 温度最高 72 °C,不构成瓶颈。
功耗墙可能是效率衰减的一部分原因
C=64 时 GPU 持续运行在 440 W,而 C=1 只有 314 W。撞上功耗限制后 GPU 会降频, 这会直接压低实际可用带宽和算力 —— 而 vLLM 的任何指标都看不到这件事。 第一轮把 C=64 效率只有 35.7% 全部归因于 prefill 抢算力,现在看至少还要算上功耗墙这一份。
这是原计划里的实验 3-A。KV 占用不该是个测出来才知道的黑箱 —— 它应该能从并发数、序列长度、每 token 的 KV 体积三个数直接算出来。 拿稳态实测值去对,误差有多大就说明这本账理解得有多准。
原公式写的是 并发 × (in_len + out_len) × 56 KiB ÷ 池大小。
但 in + out = 1280 是请求结束那一刻的占用,
稳态下大量请求正生成到一半,平均只占 in + out/2 = 1152。
峰值要对峰值,均值要对均值,混用会系统性高估 11%。
# 池容量(服务端日志) GPU KV cache size = 86,896 tokens = 5,431 blocks × 16 # 单请求占用(block_size = 16,两个数都整除,无对齐损耗) 峰值 = ceil(1280/16) = 80 blocks = 1,280 tokens 均值 = ceil(1152/16) = 72 blocks = 1,152 tokens # 预期占用率 峰值% = C × 1280 ÷ 86,896 均值% = C × 1152 ÷ 86,896
KV 占用:稳态实测 vs 理论预期
实线为实测,虚线为理论值,两两几乎完全重合。稳态段取每个并发点窗口的中间 60%, 去掉了档位切换时的爬升与回落。
| 并发 | running p50 | 实测 p50 | 预期均值 | 误差 | 实测峰值 | 预期峰值 | 误差 |
|---|
账算平了
峰值口径误差 0.0 ~ −1.0 个百分点,七个点全部落在 1 pp 以内。 均值口径在低并发下同样贴合,只有 C=64 出现 −2.7 pp 的偏差。
而这个偏差不是模型错了,是"名义并发 ≠ 实际并发"。
C=64 时 KV 池已经限制了调度,running 中位数只有 62。
用 62 代入公式:预期 82.2%,实测 82.1% —— 误差 0.1 个百分点。
剩下 C=8~32 那 −0.3 ~ −1.6 pp 的小幅负偏差,来自请求生命周期错开: 理论均值假设输出进度均匀分布在 0~256 之间,但请求完成到新请求 prefill 完成之间 有短暂空隙,实际占用略低于理想值。这个方向和量级都合理。
一个这轮没暴露的坑
本例的 1280 和 1152 都能被 block_size=16 整除,所以对齐损耗恰好为零。
换成 in_len=1000 就不一样了:ceil(1256/16)=79 blocks = 1264 tokens,
每请求白占 8 个 token。并发 64 时就是 512 个 token 的浪费 ——
量不大,但做容量规划时要记得按 block 向上取整,而不是按 token 数直接算。
两轮相隔两天,重启过实例,配置相同。这是检验测量方法可靠性的机会。
吞吐复现性:第一轮 vs 第二轮
7 个点的吞吐差异全部落在 +0.02% ~ +0.19% 之间,p99 TTFT 差异在 -1.5% ~ 0% 之间。
这个吻合度说明脚本里那些固定变量的措施(--seed、--ignore-eos、
关闭 prefix caching)确实起了作用。
| 并发 | 输出 tok/s | queue | prefill | 前端 | TTFT 均值 | TTFT p99 | ITL p50 | ITL p99 | running | wait 峰值 | KV% | 功耗 W |
|---|
num_preemptions_total
始终为 0,比第一轮只查日志更有说服力。in + out 是请求结束那一刻的占用,稳态均值应该用 in + out/2,
两者差 11%。混用会以为模型算错了,实际上只是口径不对。一个没做成的验证
本轮原计划用 estimated_flops_per_gpu_total ÷
estimated_read_bytes_per_gpu_total 实测算术强度,验证理论推导的 1.0 FLOP/byte。
但这两个 counter 全程为 0 —— 它们需要启动时加
--enable-mfu-metrics 才会计数。下一轮补上即可。
--enable-mfu-metrics 重跑 C=1,
拿 ΔFLOP ÷ Δbytes 验证算术强度 = 1.0。这是理论推导唯一还缺的实测对照。--kv-cache-memory=6569114112 扩池到 6.12 GiB,
并发上限从 67 提到 89,然后补测 C=96。现在已知 C=64 时 waiting 峰值 49、
running 卡在 62,扩池后应该能看到明显变化。--max-num-batched-tokens 再跑一轮,
验证"prefill 块变小 → ITL 尖峰变矮、TTFT 变差"这个对换关系。
现在有了 iteration_tokens 指标,可以直接量化块大小的变化。