<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>模型推理加速 &#8211; Blog of Code</title>
	<atom:link href="https://www.cztcode.com/tag/%E6%A8%A1%E5%9E%8B%E6%8E%A8%E7%90%86%E5%8A%A0%E9%80%9F/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.cztcode.com</link>
	<description>模型推理加速与编程实践</description>
	<lastBuildDate>Sat, 12 Sep 2026 01:00:05 +0000</lastBuildDate>
	<language>zh-Hans</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://www.cztcode.com/wp-content/uploads/2024/02/cropped-logo-32x32.webp</url>
	<title>模型推理加速 &#8211; Blog of Code</title>
	<link>https://www.cztcode.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">217219486</site>	<item>
		<title>张量并行该开几卡：从每层两次 All-Reduce 算清加速边界</title>
		<link>https://www.cztcode.com/2026/5377/</link>
					<comments>https://www.cztcode.com/2026/5377/#respond</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Sat, 12 Sep 2026 01:00:05 +0000</pubDate>
				<category><![CDATA[模型推理加速]]></category>
		<category><![CDATA[All-Reduce]]></category>
		<category><![CDATA[NCCL]]></category>
		<category><![CDATA[分布式推理]]></category>
		<category><![CDATA[张量并行]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/2026/5377/</guid>

					<description><![CDATA[从活跃 token 数、隐藏维度和实测集合通信延迟出发，判断张量并行该开几卡，并比较单卡多副本与流水线并行的代价，附标准库验证和排障顺序。]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><blockquote>
<p><strong>Jellow 编辑发布，AI 辅助生成并完成技术核对。</strong> 计算示例和实测结果会在文中分别说明。</p>
</blockquote>
<p>模型刚好能放进一张 GPU，但 Decode 的每 token 延迟没有达标。把 <code>tensor_parallel_size</code> 从 1 改成 4，会接近四倍快吗？另一种情况更直接：模型单卡放不下，究竟该用几路张量并行（Tensor Parallelism，TP），还是沿层切成流水线并行？</p>
<p>答案不能只看 GPU 数量。张量并行把矩阵乘法分给多张卡，同时把原来留在卡内的数据依赖变成集合通信。本文从一次调度步骤中的激活形状 <code>[T,H]</code> 出发，算出标准 Megatron 式稠密 Transformer 的通信账，再给出可测量的加速条件。</p>
<p>核对日期为 2026-09-12。讨论范围是采用一维列并行/行并行的稠密 Decoder 推理；MoE、Sequence Parallel、Context Parallel 和特殊融合实现放在失效边界中处理。</p>
<h2>先把“放得下”和“跑得快”拆开</h2>
<p>对每个候选 TP 度数 <code>p</code>，先检查容量约束：</p>
<p>[<br />
M<em>{rank}(p)=M</em>{weight}(p)+M<em>{KV}(p)+M</em>{runtime}(p)\le M_{usable}<br />
]</p>
<p><code>M_usable</code> 是扣除安全余量后的单卡可用显存。权重通常可按 <code>p</code> 分片，但 KV Cache、词表、临时工作区和框架保留内存是否等比例缩小取决于实现，不能直接把总显存除以 <code>p</code>。</p>
<p>容量检查只产生“可运行的候选集合”，不证明更多卡更快。如果模型能在单卡运行，以吞吐为目标时，合理基线不是一组 TP=<code>p</code>，而是相同 <code>p</code> 张卡上的 <code>p</code> 个 TP=1 副本。vLLM 的官方部署指南也把“模型能放入单卡”作为优先采用单卡推理的起点；节点内模型过大时才优先 TP，缺少 NVLink 的节点则建议考虑流水线并行以降低通信压力。<a href="https://docs.vllm.ai/en/latest/serving/parallelism_scaling/" target="_blank" rel="noopener">vLLM Parallelism and Scaling</a></p>
<p>如果模型单卡放不下，最小可行 <code>p</code> 是容量基线。此时“成功”可能只是让模型能够运行，不能把它表述成相对单卡的实测加速。</p>
<h2>为什么一个 MLP 只在末尾归并</h2>
<p>设当前调度步骤有 <code>T</code> 个活跃 token，隐藏维度为 <code>H</code>，SwiGLU 中间维度为 <code>I</code>：</p>
<p>[<br />
X\in\mathbb{R}^{T\times H},\quad<br />
W_g,W_u\in\mathbb{R}^{H\times I},\quad<br />
W_d\in\mathbb{R}^{I\times H}<br />
]</p>
<p>Decode 每条活跃序列通常贡献一个新 token，因此此时 <code>T</code> 近似活跃批大小；Prefill 或 Chunked Prefill 中，<code>T</code> 是本步骤实际调度的 prompt token 总数，不等于请求数。</p>
<p>把前两个权重沿输出维切成 <code>p</code> 份，把下降投影沿输入维切成对应的 <code>p</code> 份：</p>
<p>[<br />
W<em>g=[W</em>{g,1},\ldots,W_{g,p}],\quad<br />
W<em>u=[W</em>{u,1},\ldots,W_{u,p}],\quad<br />
W<em>d=\begin{bmatrix}W</em>{d,1}\ \vdots \ W_{d,p}\end{bmatrix}<br />
]</p>
<p>第 <code>r</code> 张卡计算：</p>
<p>[<br />
Z<em>r=\operatorname{SiLU}(XW</em>{g,r})\odot(XW_{u,r})<br />
\in\mathbb{R}^{T\times I/p}<br />
]</p>
<p>[<br />
P_r=Z<em>rW</em>{d,r}\in\mathbb{R}^{T\times H},\qquad<br />
Y=\sum_{r=1}^{p}P_r<br />
]</p>
<p>激活函数和逐元素乘法都留在本地分片内，只有最后的 <code>[T,H]</code> 部分和需要 All-Reduce。Megatron Core 的官方 API 仍以 <code>ColumnParallelLinear</code> 和 <code>RowParallelLinear</code> 表达这两种切分。<a href="https://docs.nvidia.com/megatron-core/developer-guide/latest/apidocs/core/core.tensor_parallel.layers.html" target="_blank" rel="noopener">Megatron Core 张量并行层文档</a></p>
<p>自注意力采用相同配对：Q/K/V 投影按头或输出列切分，注意力在各卡本地计算，输出投影按行切分后归并。原始 Megatron-LM 论文由此得到一个标准 Transformer 层前向两次 All-Reduce：注意力一次，MLP 一次。<a href="https://arxiv.org/abs/1909.08053" target="_blank" rel="noopener">Megatron-LM 论文</a></p>
<p>这里的“两次”是本文成本模型的关键假设，不是所有推理引擎和模型结构的常数。</p>
<h2>从 <code>[T,H]</code> 得到通信账</h2>
<p>设激活通信数据类型每元素占 <code>e_a</code> 字节。一次 All-Reduce 的逻辑载荷为：</p>
<p>[<br />
S=THe_a\quad\text{bytes}<br />
]</p>
<p>对 <code>p</code> 个 rank，NVIDIA <code>nccl-tests</code> 给出的 All-Reduce 总线带宽修正因子是 <code>2(p-1)/p</code>。按每个 rank 的出口带宽折算，一次集合通信的数据项为：</p>
<p>[<br />
V_{AR}=\frac{2(p-1)}{p}S<br />
]</p>
<p>一个有 <code>L</code> 层的步骤包含 <code>2L</code> 次这样的归并，所以总线等价通信量为：</p>
<p>[<br />
V_{step}=4L\frac{p-1}{p}THe_a<br />
]</p>
<p><a href="https://github.com/NVIDIA/nccl-tests/blob/master/doc/PERFORMANCE.md" target="_blank" rel="noopener">NVIDIA 对该因子的推导</a>同时提醒：小消息应直接观察操作时间；消息足够大之后，时间才近似为固定开销加字节数除以带宽。因此可用下式做初筛：</p>
<p>[<br />
\tau_{AR}(S,p)\approx \alpha_p+<br />
\frac{2(p-1)S}{pB_p}<br />
]</p>
<p><code>α_p</code> 是一次集合通信的固定延迟，<code>B_p</code> 是指定 rank 布局下的有效总线带宽。NCCL 可能选择树、环、NVLS 或其他路径，这个式子不是替代实测的硬件承诺；生产判断应直接代入相同拓扑、相同载荷下测得的 <code>τ_AR</code>。</p>
<p>看一个纯计算示例。假定 <code>H=8192</code>、<code>L=80</code>、<code>T=32</code>，激活以 BF16 通信，<code>p=4</code>：</p>
<ul>
<li>每次载荷 <code>S=32×8192×2=524288</code> 字节，即 512 KiB；</li>
<li>每步有 <code>2L=160</code> 次集合通信；</li>
<li>每个 rank 的总线等价通信量为 <code>4×80×3/4×512 KiB=120 MiB</code>。</li>
</ul>
<p>120 MiB 看起来不大，但它被拆成 160 次有依赖关系的调用，固定延迟可能比峰值带宽更重要。这也是小 Decode 批量经常无法按卡数线性加速的原因。</p>
<h2>加速成立需要满足什么不等式</h2>
<p>令 GQA 的 KV 投影总宽度为 <code>H_KV</code>。忽略 bias 后，注意力投影 Q、K、V、O 的主要 FLOPs 为：</p>
<p>[<br />
F<em>{attn}=4T(H^2+HH</em>{KV})<br />
]</p>
<p>SwiGLU 三个线性层的 FLOPs 为：</p>
<p>[<br />
F_{mlp}=6THI<br />
]</p>
<p>因此每层主要投影计算量近似为：</p>
<p>[<br />
F<em>{proj}=4T(H^2+HH</em>{KV})+6THI<br />
]</p>
<p>这些式子按一次乘加计 2 FLOPs。它们没有包括注意力分数、归一化、采样和调度开销；长上下文会增加注意力计算，从而改变计算与通信的比例。</p>
<p>先作一个乐观假设：分片后的 GEMM 效率不下降，投影计算可按 <code>p</code> 等分。则：</p>
<p>[<br />
t<em>p\approx \frac{t</em>{proj,1}}{p}+2L\tau_{AR}(THe<em>a,p)+t</em>{other,p}<br />
]</p>
<p>TP 比单卡快的必要条件是：</p>
<p>[<br />
\left(1-\frac{1}{p}\right)t_{proj,1}</p>
<blockquote>
<p>2L\tau<em>{AR}+(t</em>{other,p}-t_{other,1})<br />
]</p>
</blockquote>
<p>左侧是理想情况下省下的计算时间，右侧是新增通信及其他开销。代价并没有消失，而是从单卡 GEMM 转移到了集合通信、更小的本地 GEMM、同步等待和多卡资源占用。</p>
<p>还能得到一个活跃 token 数阈值。若单卡全部投影时间写成 <code>t_proj,1=aT</code>，并暂时忽略 <code>t_other</code> 的差值，则：</p>
<p>[<br />
T&gt;<br />
\frac{2L\alpha_p}<br />
{a(1-1/p)-4L\frac{p-1}{p}\frac{He_a}{B_p}}<br />
]</p>
<p>分母必须为正；否则仅带宽项就已经吃掉理想计算收益，在这个简化模型中不存在能获益的 <code>T</code>。<code>a</code> 不应从 GPU 标称峰值推测，最好由 TP=1 的应用 trace 按 <code>T</code> 分桶拟合。</p>
<h2>用纯 Python 核对分片恒等式和阈值直觉</h2>
<p>下面程序只用 Python 标准语法。前半段以 ReLU 代替 SiLU，使整数结果可以精确比较；任何逐元素激活都不改变分片恒等式。后半段枚举一个明确标注的成本模型，结果不是 GPU 实测。</p>
<pre><code class="language-python">def mm(a, b):
    return [[sum(x*y for x, y in zip(row, col))
             for col in zip(*b)] for row in a]

def elem(a, b, fn):
    return [[fn(x, y) for x, y in zip(ar, br)]
            for ar, br in zip(a, b)]

def add(a, b):
    return elem(a, b, lambda x, y: x + y)

def col_shard(a, rank, p):
    n = len(a[0])
    assert n % p == 0
    q = n // p
    return [row[rank*q:(rank+1)*q] for row in a]

def row_shard(a, rank, p):
    n = len(a)
    assert n % p == 0
    q = n // p
    return a[rank*q:(rank+1)*q]

X = [[1, -1, 2, 0], [0, 2, -1, 1]]  # [T=2, H=4]
Wg = [[(r + 2*c) % 5 - 2 for c in range(6)] for r in range(4)]
Wu = [[(2*r + c) % 7 - 3 for c in range(6)] for r in range(4)]
Wd = [[(r - c) % 5 - 2 for c in range(4)] for r in range(6)]

gate = mm(X, Wg)
up = mm(X, Wu)
z = elem(gate, up, lambda g, u: max(0, g) * u)
dense = mm(z, Wd)

p = 2
partial = []
for rank in range(p):
    g = mm(X, col_shard(Wg, rank, p))
    u = mm(X, col_shard(Wu, rank, p))
    z_rank = elem(g, u, lambda x, y: max(0, x) * y)
    partial.append(mm(z_rank, row_shard(Wd, rank, p)))
reduced = add(partial[0], partial[1])
assert reduced == dense
print("algebra: OK", dense)

# 以下全是示例假设，不是硬件实测。
H, I, H_KV, L = 8192, 28672, 1024, 80
ACT_BYTES, P_EFF = 2, 120e12       # BF16；单卡投影有效 120 TFLOP/s
ALPHA, BUS_BW = 8e-6, 150e9        # 每次 8 us；总线带宽 150 GB/s

payload = 32*H*ACT_BYTES
traffic = 4*L*(4-1)*payload//4
assert payload == 512*1024
assert traffic == 120*1024**2
print("TP=4,T=32:", payload//1024, "KiB/call,",
      traffic//1024**2, "MiB/step/rank")

def estimate(T, p):
    f_layer = 4*T*(H*H + H*H_KV) + 6*T*H*I
    t1 = L*f_layer/P_EFF
    if p == 1:
        return t1, 1.0
    payload = T*H*ACT_BYTES
    t_ar = ALPHA + payload*2*(p-1)/p/BUS_BW
    tp = t1/p + 2*L*t_ar
    return tp, t1/tp

print("T  " + "  ".join(f"TP={p}" for p in (2, 4, 8)))
for T in (1, 4, 8, 16, 32):
    print(f"{T:&lt;2} &quot; + &quot;  &quot;.join(
        f&quot;{estimate(T, p)[1]:.2f}x&quot; for p in (2, 4, 8)))</code></pre>
<p>执行输出：</p>
<pre><code class="language-text">algebra: OK [[-14, -20, 4, 38], [86, 40, -6, -52]]
TP=4,T=32: 512 KiB/call, 120 MiB/step/rank
T  TP=2  TP=4  TP=8
1  0.61x  0.72x  0.79x
4  1.26x  1.81x  2.31x
8  1.53x  2.42x  3.42x
16 1.71x  2.91x  4.51x
32 1.82x  3.25x  5.35x</code></pre>
<p>这个枚举故意给分片 GEMM 理想的 <code>1/p</code> 加速，<code>T=1</code> 仍然变慢。真实小矩阵还可能因每卡的 <code>I/p</code> 过窄而降低计算效率，所以模型通常偏乐观。改变 <code>ALPHA</code>、<code>BUS_BW</code> 和 <code>P_EFF</code>，可以先判断哪些 TP 度数值得进入 GPU 压测，而不能据此宣布性能结果。</p>
<h2>公平基线不是只有 TP=1</h2>
<table>
<thead>
<tr>
<th>方案</th>
<th>回答的问题</th>
<th>必须观察的代价</th>
</tr>
</thead>
<tbody>
<tr>
<td>单个 TP=1 副本</td>
<td>单请求或单批的本地基线</td>
<td>是否满足容量与延迟 SLO</td>
</tr>
<tr>
<td><code>p</code> 个 TP=1 副本</td>
<td>相同 GPU 数下的吞吐基线</td>
<td>路由、各副本批量与负载均衡</td>
</tr>
<tr>
<td>一个 TP=<code>p</code> 副本</td>
<td>TP 是否降低单步延迟或解决显存不足</td>
<td>All-Reduce、GEMM 变窄、同步等待</td>
</tr>
<tr>
<td>PP=<code>p</code> 或节点内 TP+跨节点 PP</td>
<td>弱互连、切分不整齐或跨节点时的替代方案</td>
<td>流水线气泡、阶段不均衡、激活点对点传输</td>
</tr>
</tbody>
</table>
<p>线上比较应报告卡数归一化 goodput：</p>
<p>[<br />
G_{card}=\frac{\text{满足延迟 SLO 的输出 token 数}}<br />
{\text{时间}\times\text{GPU 数}}<br />
]</p>
<p>TP 可能让一个请求更快，却让每卡 goodput 下降；如果模型原本能单卡运行，这正是多副本基线不可省略的原因。</p>
<p>权重量化也不会自动等比例降低这里的通信量。公式中的 <code>e_a</code> 是归并激活的数据类型，不是权重位宽。4 bit 权重配 BF16 激活时，权重读取减少了，而 <code>[T,H]</code> 的 All-Reduce 仍可能按每元素 2 字节进行；必须从 trace 核对实际通信 dtype。</p>
<h2>可复现的压测设计</h2>
<p>先固定模型、权重与 KV 精度、框架提交版本、CUDA/NCCL 版本、GPU 型号与 rank 布局。对 TP=1、2、4、8 中满足容量和可整除条件的候选执行两阶段测试。</p>
<p>第一阶段隔离组件。记录真实 <code>T</code> 分布，并在目标拓扑上扫描覆盖 <code>THe_a</code> 的消息大小。例如单节点 4 卡可使用：</p>
<pre><code class="language-bash">./build/all_reduce_perf -b 16K -e 4M -f 2 -g 4 \
  -d half -n 200 -w 50 -I 1 -K 20</code></pre>
<p><code>nccl-tests</code> 官方仓库说明了消息范围、预热、逐迭代时延和正确性检查参数；跨节点测试需改用与部署相同的 MPI rank 布局。<a href="https://github.com/NVIDIA/nccl-tests" target="_blank" rel="noopener">NVIDIA nccl-tests</a></p>
<p>同时在完整引擎 trace 中记录：</p>
<ul>
<li>每步活跃 token 数 <code>T</code>，并区分 Prefill 与 Decode；</li>
<li>每次 collective 的载荷、dtype、耗时和每层调用数；</li>
<li>collective 位于关键路径上的时间，而不只是所有 kernel 时间之和；</li>
<li>分片 GEMM 的形状、耗时、SM 利用率与显存带宽；</li>
<li>TPOT/ITL、TTFT、端到端延迟的 p50、p95、p99；</li>
<li>输出 token/s、满足 SLO 的 goodput，以及每卡归一化结果。</li>
</ul>
<p>第二阶段回放相同到达过程与输入、输出长度分布。先保持调度参数一致以隔离 TP，再允许每个候选独立调优批量上限，比较各自能达到的最佳 SLO—goodput 前沿。只报离线大批吞吐会掩盖小 <code>T</code> 时的集合通信固定开销。</p>
<h2>什么时候公式会失效</h2>
<p>以下任一条件成立，都应从 trace 重新建立通信账：</p>
<ul>
<li><code>p</code> 不能整除注意力头数、KV 头数或 MLP 中间维度，框架发生 padding、复制 KV 头或拒绝加载；</li>
<li>GQA/MQA 的 KV Cache 分片策略与假设不同，尤其 <code>p</code> 大于 KV 头数；</li>
<li>开启 Sequence Parallel 后，部分 All-Reduce 被 All-Gather 与 Reduce-Scatter 替代；</li>
<li>MoE 引入 token dispatch 和 All-to-All，负载不均衡也进入关键路径；</li>
<li>引擎融合了归并、残差或归一化，或者通过自定义 collective 改变调用次数；</li>
<li>跨节点链路、并发通信或 rank 放置使 <code>α_p</code>、<code>B_p</code> 与微基准不同；</li>
<li>长上下文的注意力计算占主导，此时只计算投影 FLOPs 会低估可并行计算；</li>
<li>本地 GEMM 过窄，实际计算时间明显高于 <code>t_proj,1/p</code>。</li>
</ul>
<p>最直接的校验是：若 trace 中每步 collective 数不接近 <code>2L</code>，或载荷不是约 <code>THe_a</code>，就停止套用本文的简化公式。</p>
<h2>排障要按因果链走</h2>
<ol>
<li><strong>先核对正确性与形状。</strong> 确认实际 <code>p</code>、rank 数、dtype、头数和中间维度切分；检查是否有 padding、KV 复制或意外降级。</li>
<li><strong>再核对物理路径。</strong> 运行 <code>NCCL_RUN_DIAGNOSTICS=1</code>，查看 <code>nvidia-smi topo -m</code> 和 P2P 状态。NCCL 2.31 的官方诊断可在通信器初始化时验证 GPU 间实际路径。<a href="https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/troubleshooting/diagnostics.html" target="_blank" rel="noopener">NCCL Diagnostics</a></li>
<li><strong>用相同载荷测 collective。</strong> 如果 <code>nccl-tests</code> 本身就慢，先处理 NVLink、PCIe、NIC、NUMA 或 GPUDirect RDMA；不要先怪模型。</li>
<li><strong>比较微基准与应用 trace。</strong> 微基准正常而应用中的 All-Reduce 慢，检查 rank 放置、并发 stream、CPU 亲和性、框架同步及其他通信争用。</li>
<li><strong>通信正常再看 GEMM。</strong> 若 collective 关键路径占比不高但加速仍差，检查 <code>I/p</code>、每卡头数和实际 kernel 效率，修正理想 <code>1/p</code> 假设。</li>
<li><strong>最后回到调度与排队。</strong> 离线步骤变快而线上 p99 或 goodput 没改善，通常要检查 <code>T</code> 分布、排队、批量形成速度和副本负载，而不是继续增加 TP。</li>
</ol>
<p>NCCL 的官方性能排障文档也建议先区分硬件路径、NCCL 和应用问题，并提醒不要因单一微基准收益就把环境变量强制带入生产。<a href="https://docs.nvidia.com/deeplearning/nccl/archives/nccl_2312/user-guide/docs/troubleshooting/performance_and_tuning.html" target="_blank" rel="noopener">NCCL 2.31.2 Performance and Tuning</a></p>
<h2>把卡数选择落成一条规则</h2>
<p>先选出能放下模型且形状可分的 <code>p</code>。对每个候选，用真实 <code>T</code> 分桶测量 <code>τ_AR(THe_a,p)</code>，再检查：</p>
<p>[<br />
\text{实测计算节省} &gt; 2L\tau_{AR}+\text{其他新增开销}<br />
]</p>
<p>模型能单卡运行时，只有 TP 在目标延迟下的每卡 goodput 优于多副本基线，或多副本无法满足单请求延迟 SLO，才值得占用多张卡。模型不能单卡运行时，默认选择满足容量和 SLO 的最小 TP 度数；跨越弱链路之前，先比较节点内 TP 加跨节点 PP。</p>
<p>卡数不是越多越好。真正的分界由活跃 token 数、每层 <code>[T,H]</code> 的归并延迟、本地 GEMM 退化和服务 SLO 一起决定。</p>
<h2>继续阅读</h2>
<ul>
<li><a href="https://www.cztcode.com/2026/5375/">在线推理能扛多少请求：别用固定并发替代到达率压测</a></li>
<li><a href="https://www.cztcode.com/2026/5372/">Codex 怎么和子 agent 协作：从一次任务分派看消息、上下文与状态管理</a></li>
<li><a href="https://www.cztcode.com/2026/5370/">大模型推理加速（四）：推测解码为什么不改变分布，什么时候反而更慢</a></li>
</ul>
<hr />
<p><a href="https://www.cztcode.com/ai-content-policy/">本站的内容生成、审核与纠错说明</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2026/5377/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5377</post-id>	</item>
		<item>
		<title>在线推理能扛多少请求：别用固定并发替代到达率压测</title>
		<link>https://www.cztcode.com/2026/5375/</link>
					<comments>https://www.cztcode.com/2026/5375/#comments</comments>
		
		<dc:creator><![CDATA[Jellow]]></dc:creator>
		<pubDate>Fri, 11 Sep 2026 01:00:04 +0000</pubDate>
				<category><![CDATA[模型推理加速]]></category>
		<category><![CDATA[SLO]]></category>
		<category><![CDATA[vLLM]]></category>
		<category><![CDATA[在线推理]]></category>
		<category><![CDATA[性能基准]]></category>
		<guid isPermaLink="false">https://www.cztcode.com/2026/5375/</guid>

					<description><![CDATA[固定并发能测饱和吞吐，却回答不了线上安全 RPS。本文从张量工作量、开放环排队和请求级联合 SLO 出发，用多个独立重复运行的保守下包络判定单副本容量。]]></description>
										<content:encoded><![CDATA[<div id="bsf_rt_marker"></div><blockquote>
<p><strong>Jellow 编辑发布，AI 辅助生成并完成技术核对。</strong> 计算示例和实测结果会在文中分别说明。</p>
</blockquote>
<p>假设固定并发压测显示一个副本能完成 19 req/s，而线上流量是 6 req/s，能否据此断定容量充足？不能。固定并发中的新请求由旧请求完成触发：服务越慢，客户端发得越慢；真实用户却不会因为 GPU 忙就自动停止到达。</p>
<p>本文要解决的工程问题是：给定请求长度分布、到达模式和延迟 SLO，单副本可以安全接收多大的外生到达率。相关算子、缓存与服务文章汇总在<a href="https://www.cztcode.com/llm-inference-acceleration/">模型推理加速专题页</a>；本文只讨论容量测量。资料和接口核对日期为 2026-09-11，实际复现仍应固定 vLLM 版本或提交哈希。</p>
<h2>从张量形状看“一个请求”有多重</h2>
<p>先假设普通稠密因果注意力、单请求、<code>h</code> 个注意力头，每头维度为 <code>d_h</code>；输入长度 <code>L≥1</code>，实际生成长度 <code>O≥1</code>。</p>
<p>Prefill 中，<code>Q</code> 和 <code>K</code> 的形状都是 <code>[h,L,d_h]</code>。由缩放点积注意力 <code>softmax(QKᵀ/√d_h)V</code> 和因果遮罩可知，每层每头需要处理一个三角形。<a href="https://arxiv.org/abs/1706.03762" target="_blank" rel="noopener">Transformer 原论文</a>给出这一注意力定义。有效 query-key 点积数为：</p>
<p><code>A_prefill = h × L(L+1)/2</code></p>
<p>第一个输出 token 可以从 prefill 的 logits 取得。为了得到其余 <code>O-1</code> 个 token，decode 依次让形状 <code>[h,1,d_h]</code> 的 query 读取长度为 <code>L+1</code> 到 <code>L+O-1</code> 的 KV Cache：</p>
<p><code>A_decode = h × [(O-1)L + O(O-1)/2]</code></p>
<p>所以每层有效点积总数为：</p>
<p><code>A(L,O) = h × [L(L+1)/2 + (O-1)L + O(O-1)/2]</code></p>
<p>每个点积约含 <code>2d_h</code> 次浮点运算。这个式子没有计入投影、MLP、内核填充、批处理和调度，因此不是实际延迟模型；它只解释为什么容量实验必须保留 <code>(L,O)</code> 的联合分布。</p>
<p>一个反例是 <code>(L,O)=(2048,128)</code> 与 <code>(512,1664)</code>。两者都有 <code>L+O-1=2175</code> 个实际参与注意力的位置，每头总计都是 <code>2175×2176/2=2,366,400</code> 个有效点积；但前者的 prefill/decode 分别为 <code>2,098,176/268,224</code>，后者则为 <code>131,328/2,235,072</code>。总工作量相同，不代表 TTFT、TPOT 或批处理效率相同。</p>
<p>NVIDIA 的<a href="https://docs.nvidia.com/nim/benchmarking/llm/latest/parameters.html" target="_blank" rel="noopener">参数说明</a>也要求按业务保留输入和输出长度分布：长输入主要增加 prefill 压力与 TTFT，长输出增加生成阶段压力与 ITL。固定长度测试可以用 <code>ignore_eos</code> 控制输出长度，但容量验收还应回放生产停止条件。若对 TTFT、TPOT、ITL 的口径不熟悉，可先看站内的<a href="https://www.cztcode.com/2026/5348/">推理指标入门</a>。</p>
<h2>固定并发把等待移到了哪里</h2>
<p>用单服务器 FCFS 队列隔离这个问题。第 <code>i</code> 个请求的预定到达时间、服务时间、开始时间和完成时间分别为 <code>a_i、s_i、b_i、f_i</code>：</p>
<p><code>b_i = max(a_i, f_{i-1})</code></p>
<p><code>f_i = b_i + s_i</code></p>
<p><code>w_i = b_i - a_i</code></p>
<p>开放环压测先产生与完成事件无关的 <code>a_i</code>。在未条件化的泊松过程中，间隔独立服从 <code>Exp(λ)</code>，其中 <code>λ</code> 是目标 RPS。闭环并发为 1 时，下一个请求恰在前一个完成后发出，因此 <code>a_i=f_{i-1}</code>、<code>w_i=0</code>：它测到的是 <code>1/E[s]</code>，没有测到外生 <code>λ</code> 下的排队尾延迟。</p>
<p>并发上限大于 1 时同样可能发生代价转移。信号量、网关或连接池会把多余请求留在客户端；若计时从真正发往模型服务时才开始，用户等待就从指标中消失。vLLM 将 <code>--request-rate</code> 与 <code>--max-concurrency</code> 分开，并明确指出并发帽可能使实际执行率低于设定值。<a href="https://docs.vllm.ai/en/latest/cli/bench/serve/" target="_blank" rel="noopener">CLI 参数定义</a>还提供 <code>client_queue_time</code> 和 <code>e2el_including_client_queue</code> 来显示这部分等待。</p>
<p>MLPerf LoadGen 的 Server 场景同样采用随机泊松到达，允许请求保持 outstanding，并按延迟阈值判定；Offline 场景报告吞吐。<a href="https://github.com/mlcommons/inference/blob/master/loadgen/test_settings.h" target="_blank" rel="noopener">MLPerf 官方实现说明</a>描述的是两个不同问题，不能互相替代。</p>
<h2>vLLM 的有限速率不是未条件化泊松过程</h2>
<p>截至 2026-09-11，vLLM 文档把 <code>--burstiness=1</code> 描述为泊松到达，其他正值对应 Gamma 间隔；但源码还有一个必须说明的实现细节。</p>
<p>在固定有限 <code>request-rate</code>、没有 ramp-up、且没有使用 self-timed trace 的路径中，vLLM 会先采样 Gamma 间隔；当 <code>burstiness=1</code> 时，这一步确实是指数间隔。随后程序累加所有间隔，并统一乘以：</p>
<p><code>normalize_factor = (num_requests/request_rate) / last_cumulative_delay</code></p>
<p>因此最后一个请求的计划偏移会精确对齐 <code>num_requests/request_rate</code>。<a href="https://github.com/vllm-project/vllm/blob/main/vllm/benchmarks/serve.py" target="_blank" rel="noopener">当前 serve.py</a>直接展示了采样与缩放逻辑。缩放以后，各间隔共同受总时长约束，不再是相互独立的未条件化指数变量；它与未条件化泊松过程存在实现差异。长时间测试中差异可能很小，但本文研究的正是到达过程，不能省略这个边界。</p>
<p>若生产突发和相关性很重要，最终验收应使用带真实时间戳的 <code>timed_trace --self-timed</code>，或使用能够按原始时间戳发送的外部负载发生器。本文后面的标准库例子使用未缩放的独立指数间隔，验证的是未条件化泊松队列，不是 vLLM 调度器的逐行复刻。</p>
<h2>容量应由请求级联合 SLO 定义</h2>
<p>令第 <code>r</code> 个独立重复运行中，第 <code>i</code> 个请求的联合指示量为：</p>
<p><code>I_{r,i} = 1[请求成功 且 TTFT≤B_f 且 TPOT≤B_d 且 E2E≤B_e]</code></p>
<p>未设置的约束可以删去，错误和超时必须记为 0。该次运行的达标率和合格请求吞吐为：</p>
<p><code>Â_r(λ) = Σ_i I_{r,i}/N_r</code></p>
<p><code>Ĝ_r(λ) = Σ_i I_{r,i}/T_r</code></p>
<p>分别查看 TTFT p99 和 TPOT p99 不等价于联合达标。两类违规若发生在不同请求上，各自有 99% 的请求通过时，联合通过率仍可能只有 98%。vLLM 对同一成功请求的全部 <code>--goodput</code> 条件执行逻辑 AND，再计算 <code>good_completed/duration</code>；它输出的是 request goodput，不是把错误纳入分母后的联合达标率。<a href="https://github.com/vllm-project/vllm/blob/main/vllm/benchmarks/serve.py" target="_blank" rel="noopener">官方源码</a>可以核对这一区别。DistServe 论文则把 per-GPU goodput 定义为达到 SLO attainment 目标时可承载的最大请求率。<a href="https://www.usenix.org/system/files/osdi24-zhong-yinmin.pdf" target="_blank" rel="noopener">论文原文</a></p>
<p>FCFS 等待时间会向后传播，所以同一次运行中的 <code>I_{r,i}</code> 通常存在序列相关性。不能把几万个请求直接视为独立 Bernoulli 样本，再用逐请求 IID Wilson 区间决定 PASS/FAIL。</p>
<p>本文采用多个相互独立的完整运行。预先指定 <code>R</code> 组 seed，每次重新建立负载随机过程并按实验策略重置服务状态，以整次运行作为实验单位。定义保守下包络：</p>
<p><code>A_floor(λ) = min_{r=1..R} Â_r(λ)</code></p>
<p>只有每次运行都满足全部验收条件，候选 RPS 才通过：</p>
<p><code>λ* = max{λ | 对所有 r：Â_r≥q，错误率_r≤ε，实际发送率合格，且队列无持续上升}</code></p>
<p>这条 <code>min</code> 规则是预先声明的保守验收规则，不是“95% 置信下界”，也不声称具有某个未证明的覆盖率。如果无法做独立重启，可以按时间分块报告最差块，但块长度必须覆盖业务突发与队列相关时间；相邻小块不能被冒充为独立样本。</p>
<h2>一个验证排队效应和重复判定的标准库例子</h2>
<p>下面不是 GPU 实测。它运行 12 个相互独立的单服务器队列：80% 请求服务 20 ms，20% 服务 180 ms；E2E SLO 为 250 ms，目标达标率为 95%。不同重复分别生成服务序列与未缩放的泊松到达；不同 RPS 使用配对 seed，便于把负载随机性对齐后观察变化。</p>
<pre><code class="language-python">import math
import random

N, WARM, RUNS = 50_000, 5_000, 12
SLO, TARGET = 0.250, 0.95
SEEDS = tuple(range(101, 101 + RUNS))

def percentile(xs, p):
    ys = sorted(xs)
    return ys[max(0, math.ceil(p / 100 * len(ys)) - 1)]

def one_run(rate, seed):
    service_rng = random.Random(seed)
    arrival_rng = random.Random(seed + 10_000)
    service = [0.020 if service_rng.random() = WARM:
            waits.append(start - arrival)
            passed += (finish - arrival &lt;= SLO)
    measured = N - WARM
    actual_rate = (measured - 1) / (arrival - first_measured_arrival)
    return percentile(waits, 95), passed / measured, actual_rate

closed_rates = []
for seed in SEEDS:
    rng = random.Random(seed)
    service = [0.020 if rng.random() = TARGET else "FAIL"
    print(f"{rate:&gt;4} {rate*0.052:&gt;4.2f} "
          f"{min(actuals):.2f}-{max(actuals):.2f} "
          f"{max(q95s)*1000:&gt;14.1f} "
          f"{min(attains):.2%}-{max(attains):.2%} "
          f"{min(attains):&gt;7.2%} {check}")</code></pre>
<p>Python 3 标准库的实际输出为：</p>
<pre><code class="language-text">closed-loop: rate_range=19.07-19.43 req/s, min_attain=100.00%
rate rho actual_rps_range worst_p95q_ms attain_range       floor   slo_check
   2 0.10 1.99-2.01           64.2 98.85%-99.00%  98.85% PASS
   4 0.21 3.98-4.02          137.7 97.22%-97.50%  97.22% PASS
   6 0.31 5.96-6.03          170.7 94.70%-95.26%  94.70% FAIL
   8 0.42 7.95-8.04          232.7 90.84%-91.78%  90.84% FAIL
  10 0.52 9.94-10.05          314.9 85.52%-86.58%  85.52% FAIL
  14 0.73 13.91-14.07          673.9 64.74%-67.65%  64.74% FAIL
  18 0.94 17.89-18.10         3889.7 19.30%-26.52%  19.30% FAIL</code></pre>
<p>固定并发基线在 12 次运行中得到 19.07—19.43 req/s，并且因为单次服务时间都小于 250 ms，最差达标率仍是 100%。开放环中，4 RPS 的 12 次运行全部超过 95%，而 6 RPS 的保守下包络只有 94.70%，因此这里只得到 4—6 RPS 的 SLO 夹点，不能把 4 RPS 杜撰成精确容量。</p>
<p><code>rho</code> 使用理论均值 <code>E[s]=0.8×0.020+0.2×0.180=0.052 s</code>。即使平均利用率只有约 31%，服务时间方差和到达重叠也已让 6 RPS 的部分运行失守。这个模型没有模拟 GPU、网络或<a href="https://www.cztcode.com/2026/5360/">连续批处理</a>；实际服务时间会随批次和调度变化，示例阈值不能搬到生产。</p>
<h2>可复现的 vLLM 容量实验</h2>
<p>先固定模型与软件版本、权重精度、并行度、调度参数、GPU 与驱动、客户端机器、网络、采样参数和缓存状态。数据集至少保留 <code>(输入 token 数,实际输出 token 数,缓存命中条件)</code> 的联合分布。</p>
<table>
<thead>
<tr>
<th>层次</th>
<th>负载</th>
<th>回答的问题</th>
</tr>
</thead>
<tbody>
<tr>
<td>隔离基线</td>
<td>并发 1、低到达率</td>
<td>无排队时各长度桶的服务时间</td>
</tr>
<tr>
<td>饱和基线</td>
<td><code>request-rate=inf</code>，扫描固定并发</td>
<td>峰值吞吐和批处理拐点</td>
</tr>
<tr>
<td>容量扫描</td>
<td>有限外生 RPS，先不设并发帽</td>
<td>队列和联合 SLO 在哪里失守</td>
</tr>
<tr>
<td>生产验收</td>
<td>真实时间戳、网关限制和缓存策略</td>
<td>突发、相关性及代价转移后的体验</td>
</tr>
</tbody>
</table>
<p>截至核对日，下面命令可作为固定 <code>(1024,128)</code> 长度桶的模板。<code>800/50 ms</code> 是占位 SLO，必须替换；它没有在本文环境中连接模型服务，也不是 GPU 实测。</p>
<pre><code class="language-bash">vllm bench serve \
  --backend openai \
  --base-url http://127.0.0.1:8000 \
  --model MODEL_ID \
  --dataset-name random \
  --input-len 1024 \
  --output-len 128 \
  --ignore-eos \
  --temperature 0 \
  --request-rate 8 \
  --burstiness 1.0 \
  --num-prompts 10000 \
  --num-warmups 32 \
  --seed 11 \
  --percentile-metrics ttft,tpot,e2el,client_queue_time,e2el_including_client_queue \
  --metric-percentiles 50,95,99 \
  --goodput ttft:800 tpot:50 \
  --save-result \
  --save-detailed</code></pre>
<p>当前 vLLM 的一个 <code>--seed</code> 不是独立的“到达种子”。<code>serve.py</code> 用它设置 Python <code>random</code> 和 NumPy RNG；数据集代码又把同一个值用于随机请求生成、抽样或顺序洗牌，具体作用取决于数据集和 <code>--disable-shuffle</code>；NumPy RNG 还用于到达间隔调度。<a href="https://github.com/vllm-project/vllm/blob/main/vllm/benchmarks/serve.py" target="_blank" rel="noopener">serve.py</a>与<a href="https://github.com/vllm-project/vllm/blob/main/vllm/benchmarks/datasets/datasets.py" target="_blank" rel="noopener">数据集源码</a>共同给出了这一语义。</p>
<p>因此比较配置 A/B 时应使用配对设计：重复 1 的 A 和 B 都用 seed 11，重复 2 都用 seed 23，之后继续使用多组不同 seed。每一组 seed 是一个新的完整重复，同时改变该重复的请求随机性、顺序和到达调度；同一组中的 A/B 则共享这些随机条件。不能把换 seed 描述成只换“到达种子”。若必须固定请求序列而只改变到达时间，应准备固定顺序的数据或 timed trace，并由外部调度器控制时间戳。</p>
<p>不同重复之间还要按实验目标重置状态。vLLM 的<a href="https://docs.vllm.ai/en/latest/benchmarking/cli/" target="_blank" rel="noopener">基准说明</a>提醒，重复请求可能命中上一次留下的前缀缓存并虚增吞吐；生产不依赖这种复用时，应重置缓存、重启服务或使用会重置缓存的 sweep 流程。</p>
<p>先粗扫得到一过一败的 RPS 夹点，再在夹点中加密。边界候选建议增加到至少 10—12 个预先指定的独立重复，并用更长生产时间窗复验。每次都要单独保存实际发送率、错误、联合达标率和队列时间序列，不能先合并所有请求再计算一个 IID 区间。</p>
<p>若生产网关限制并发，再增加相同的 <code>--max-concurrency</code>，同时报告客户端排队、用户视角 E2E、拒绝和超时。当前 <code>--goodput</code> 只接受 <code>ttft、tpot、e2el</code>，不接受客户端排队指标；有外部背压时必须从详细记录重新计算联合指示量，并把失败请求放回分母。</p>
<h2>指标和排障顺序</h2>
<p>一次容量扫描至少保存：设定与实际发送 RPS、成功/错误/超时数、客户端排队、TTFT/TPOT/E2E 的 p50/p95/p99、逐运行联合达标率及其最小值，并按 <code>(L,O)</code> 桶分解。</p>
<p>NVIDIA 的<a href="https://docs.nvidia.com/nim/benchmarking/llm/latest/metrics.html" target="_blank" rel="noopener">指标定义</a>中，TTFT 通常包含排队、prefill 和网络，E2E 还覆盖 batching。数学上的 TPOT 只在实际输出 token 数大于 1 时定义，公式为 <code>(E2E-TTFT)/(输出 token 数-1)</code>；当 <code>O=1</code> 时分母为零，应从 TPOT 统计中排除或明确采用工具约定。当前 vLLM 在 goodput 检查中把这类请求的 TPOT 置为 0，属于实现约定，不能当作数学定义。工具实现仍可能不同。根据 <a href="https://docs.vllm.ai/en/latest/benchmarking/cli/" target="_blank" rel="noopener">vLLM Benchmark CLI</a>，推测解码一次流式返回多个 token 时，ITL 记录的是流式事件间隔，而 TPOT 会把时间摊到 token 上，两者尤其不能混用。</p>
<p>排障按请求生命周期进行：</p>
<ol>
<li><strong>负载发生器</strong>：实际发送率是否达到设定值；客户端 CPU、连接数或网络是否先饱和；排队是否藏在信号量之前。</li>
<li><strong>工作量</strong>：核对实际 <code>(L,O)</code>、停止原因、采样参数、前缀命中率和失败请求，先排除两次实验负载不同。</li>
<li><strong>服务队列</strong>：观察等待请求数和 queue time 是否持续上升。若上升，先降低 RPS 或增加副本，不要先调算子。</li>
<li><strong>阶段拆分</strong>：队列平稳但 TTFT 变差，检查 prompt 长度、缓存命中和 prefill；TPOT 变差则检查 decode、活动序列数和长输出干扰。</li>
<li><strong>资源证据</strong>：最后结合 KV Cache 使用率、抢占次数、GPU/CPU、显存带宽和网络判断瓶颈。vLLM 的<a href="https://docs.vllm.ai/en/latest/usage/metrics/" target="_blank" rel="noopener">生产指标清单</a>提供等待请求数、queue time、prefill/decode 时间、KV Cache 使用率和抢占计数。</li>
</ol>
<p>安全余量不是固定的 70% 或 80%。它取决于长度方差、突发程度、SLO、缓存和扩容反应时间。固定并发仍是有价值的饱和基线；只有开放环到达率、多个独立重复的联合 SLO、稳定队列和真实背压同时进入验收，测出的 RPS 才能用于容量决策。</p>
<h2>继续阅读</h2>
<ul>
<li><a href="https://www.cztcode.com/2026/5372/">Codex 怎么和子 agent 协作：从一次任务分派看消息、上下文与状态管理</a></li>
<li><a href="https://www.cztcode.com/2026/5370/">大模型推理加速（四）：推测解码为什么不改变分布，什么时候反而更慢</a></li>
<li><a href="https://www.cztcode.com/2026/5368/">大模型推理加速（三）：FlashAttention 为什么更快，又没有省掉哪些计算</a></li>
</ul>
<hr />
<p><a href="https://www.cztcode.com/ai-content-policy/">本站的内容生成、审核与纠错说明</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.cztcode.com/2026/5375/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">5375</post-id>	</item>
	</channel>
</rss>
