Concurrency Sweep · 第二轮 · RTX 4090

TTFT 解剖

同样的 1024 / 256 负载,同样扫到 64 并发,但这次采样器全程跟跑了 917 秒。 第一轮只能说"TTFT 涨到了 5.8 秒",这一轮能说清楚它涨在哪一段。

GPU
RTX 4090 · 24 GB
vLLM
0.27.1 · V1
KV 池
4.64 GiB · 86,896 tok
In / Out
1024 / 256 tok
采样
913 点 · 917 s
采样误差
p50 1.006 s
失败请求
0 / 1332
抢占次数
0

结论TTFT 的增长几乎全部来自排队

把 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

分解TTFT 拆成三段

服务端的 request_queue_time 和 request_prefill_time 两个 histogram, 差分后得到每个并发点的平均排队时间与平均 prefill 时间。 两者之和与客户端观测的 TTFT 之间还差一截,那是 vLLM 前端(API server)的处理与 IPC 开销。

Figure 1

TTFT 三段分解(平均值,堆叠)

排队 queue prefill 真算 前端 + IPC

柱顶数字为该并发点的平均 TTFT。前端那一段由客户端 TTFT 减去服务端两段得出, 因此也吸收了窗口对齐带来的误差,不宜逐毫秒解读 —— 但它随并发从 12 ms 增长到 136 ms 这个趋势是真实的。

两个分量的增长速度完全不同

Figure 2

排队 vs prefill:谁在推高 TTFT

排队 queue 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 池确实开始限制并发了。

混批每次迭代里,八成 token 属于别人的 prefill

iteration_tokens_total 记录每次 engine 迭代处理了多少 token。 如果只有 decode,这个数应该等于 batch size。实测是 batch 的 5 倍 —— 多出来的部分就是被 chunked prefill 塞进来的 prefill 块。

Figure 3

每次迭代的 token 构成

decode(≈ 并发数) 混入的 prefill 块

decode 部分按窗口内 running 的中位数估计。比例几乎不随并发变化, 恒定在 1 : 4 上下 —— 这正是输入输出比(1024 : 256)的直接反映。

这是 ITL p99 尖峰最直接的物理解释:一个正在解码的请求,每一步都要等同批次里的 prefill 块算完。C=64 时每次迭代要处理 321 个 token,其中约 259 个是 prefill, 于是解码方向的每一步都被拖长,尾部就出现了 200 ms 量级的停顿。

波形917 秒全程实况

这是第一轮完全没有的视角。七个并发点在时间轴上依次排开, 可以直接看到队列什么时候堆起来、KV 什么时候冲顶。

Figure 4

运行数 / 等待数 / KV 占用

running waiting KV 占用 %

灰色分区为各并发点的测量窗口。waiting 在 C≤8 时全程为 0, 从 C=16 开始出现零星尖峰,到 C=64 变成常态。

Figure 5

功耗与温度

功耗 W 450 W 功耗墙 温度 °C

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 抢算力,现在看至少还要算上功耗墙这一份。

对账PagedAttention 的账,算得平不平

这是原计划里的实验 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
Figure 7

KV 占用:稳态实测 vs 理论预期

实测峰值 预期峰值 实测 p50(稳态) 预期均值

实线为实测,虚线为理论值,两两几乎完全重合。稳态段取每个并发点窗口的中间 60%, 去掉了档位切换时的爬升与回落。

并发running p50 实测 p50预期均值误差 实测峰值预期峰值误差
单位为占池百分比,误差单位为百分点(pp)。

账算平了

峰值口径误差 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 数直接算。

复现两轮独立测试的吻合度

两轮相隔两天,重启过实例,配置相同。这是检验测量方法可靠性的机会。

Figure 6

吞吐复现性:第一轮 vs 第二轮

7 个点的吞吐差异全部落在 +0.02% ~ +0.19% 之间,p99 TTFT 差异在 -1.5% ~ 0% 之间。 这个吻合度说明脚本里那些固定变量的措施(--seed、--ignore-eos、 关闭 prefix caching)确实起了作用。

数据完整测量结果

并发输出 tok/squeueprefill前端 TTFT 均值TTFT p99ITL p50ITL p99 runningwait 峰值KV%功耗 W
延迟单位 ms。queue / prefill 来自服务端 histogram 差分,前端 = TTFT 均值 − queue − prefill。

对照第一轮的结论,哪些站住了

被证实的

被修正的

新发现

一个没做成的验证

本轮原计划用 estimated_flops_per_gpu_total ÷ estimated_read_bytes_per_gpu_total 实测算术强度,验证理论推导的 1.0 FLOP/byte。 但这两个 counter 全程为 0 —— 它们需要启动时加 --enable-mfu-metrics 才会计数。下一轮补上即可。

下一步三件值得做的事