<?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/%E5%9C%A8%E7%BA%BF%E6%8E%A8%E7%90%86/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.cztcode.com</link>
	<description>模型推理加速与编程实践</description>
	<lastBuildDate>Fri, 11 Sep 2026 01:00:04 +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>在线推理能扛多少请求：别用固定并发替代到达率压测</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>
