Qwen3-ASR 课程 · 第 12 课 · 插入专题(工作场景)

ASR 性能压测与容量规划 —— 3 个实例能接多少路电话?

⏱ 约 40 分钟 · 场景:通话流式转写,A40 48GB(压测机),0.6B 多实例部署 · 所有外部数据来自文末来源(2026-09 核实),推导值均已标注

暂停课程路线,处理一个直接的工作问题:「显存只够起 3 个 ASR 实例,这 3 个实例最多能接多少路并发通话?怎么科学地测出来?」本报告给你:指标体系 → 话务量数学 → GPU 供给模型 → 可运行的压测代码 → 容量判定流程。

已决策 3 实例拓扑?配套的可执行压测方案(SLO、11 档阶梯、6 个测试用例、记录模板、时间表)见 loadtest-plan-a40-3inst.md,照着跑即可。

问题的一句话翻译 容量 = 需求侧(你的话务量,呼叫统计决定)与供给侧(GPU 处理能力,压测决定)的交点。「3 个实例」只是供给侧的一个参数——真正的变量是每个实例能同时扛几路流,这必须实测,但方法有业界标准答案。
通话流 8k G.711 → 16k 每路 = 1 条实时流 负载均衡 呼叫级粘性路由 轮询/最小连接 实例 1(vLLM) 内部并发:N 路流 实例 2(vLLM) 内部并发:N 路流 实例 3(vLLM) 内部并发:N 路流 总容量 = 3 × N × 安全系数 与需求对照 关键未知数: 每实例的 N = ? ← 本专题的压测目标
「3 个实例」撑起容量的机制:每个实例内部用 continuous batching 同时服务多路流(N 待实测),实例之间由负载均衡按呼叫分发。容量 = 3 × N × 安全系数,再与话务量对照。

一、指标体系:测什么

指标定义为什么重要
RTF(实时率,本方案口径)处理耗时 ÷ 音频时长,< 1 才跟得上实时(越小越好)> 1 意味着延迟无限堆积,流必死。SLO 取 0.5 = 用半秒算一秒话音,留一倍余量
RTFx(Riva 口径)音频时长 ÷ 纯推理处理时长("x"表不含 I/O 等待),与 RTF 互为倒数读外部数据先看方向:> 1 即快于实时(越大越好)。报告 Table 2 的"吞吐"列(秒音频/秒)就是聚合 RTFx
TTFT(首字延迟)音频开始 → 第一个转写结果返回通话体验的第一指标;随并发恶化最快(排队)
中间/最终延迟Riva 定义:中间结果(is_final=false)与最终结果(is_final=true)的延迟分开统计流式结果会回改,两者意义不同,必须分开报
并发数同时活跃的音频流路数容量的横轴;注意「并发」≠「注册用户数」
吞吐每秒能转写多少秒音频(秒音频/秒)0.6B 官方报告:并发 1 时 106,并发 128 时 2000
WER-负载曲线压力下的转写质量是否劣化防止「扛住了但都转错了」的隐性劣化
两条约束,取更紧的那个 容量受两条线夹击:吞吐约束(所有流的 RTF ≤ SLO)和延迟约束(TTFT p95 ≤ 目标)。小模型(0.6B)算得飞快,RTF 往往不先撞线——先撞线的几乎总是 TTFT(排队延迟)。压测时两条都要记。

二、需求侧:你的话务量(Erlang 数学)

电信业的百年标准:话务量单位 Erlang = 平均并发呼叫数(1 Erlang = 一路呼叫占满一小时)。提供话务量 = 呼叫到达率 × 平均通话时长;由它可用 Erlang B(无排队,呼损概率)与 Erlang C(无限排队,等待概率)算出需要多少条"线路"。

手算示例:每小时 50 通、平均 3 分钟 → 话务量 = 50 × 3/60 = 2.5 Erlang,即平均 2.5 路并发。下面是可运行的计算器(改参数重跑):

import math def erlang_b(A, N): """呼损率:全部线路忙时新呼叫被拒的概率(递推式)""" b = 1.0 for n in range(1, N + 1): b = A * b / (n + A * b) return b def erlang_c(A, N): """等待概率:呼叫到达时必须排队的概率(要求 A < N)""" if A >= N: return 1.0 s = sum(A**k / math.factorial(k) for k in range(N)) top = A**N / (math.factorial(N) * (1 - A/N)) return top / (s + top) calls_per_hour = 50 # 改我:高峰小时呼叫量 avg_call_min = 3 # 改我:平均通话时长(分钟) lines = 10 # 改我:可用线路(=ASR 并发容量) A = calls_per_hour * avg_call_min / 60 print(f"话务量 A = {A:.1f} Erlang(平均并发 {A:.1f} 路)") print(f"{lines} 线:呼损率 B = {erlang_b(A, lines):.4f} | 需排队概率 C = {erlang_c(A, lines):.4f}")
致命陷阱:均值 ≠ 峰值 呼叫到达是随机的(泊松过程),峰值并发远高于均值。均值 6 路的话务,瞬时峰值打到 12 路很正常——容量必须按峰值 + 余量规划,下面的模拟让你亲眼看到:
import numpy as np def simulate(hours, calls_per_hour, avg_call_min, capacity, seed=7): """泊松到达 + 指数时长:统计峰值并发与过载时间占比""" rng = np.random.default_rng(seed) T = hours * 3600.0 gaps = rng.exponential(3600.0 / calls_per_hour, int(calls_per_hour * hours * 2)) arrivals = np.cumsum(gaps) arrivals = arrivals[arrivals < T] durations = np.minimum(rng.exponential(avg_call_min * 60, len(arrivals)), T - arrivals) events = [] for a, d in zip(arrivals, durations): events.append((a, 1)); events.append((a + d, -1)) events.sort(key=lambda e: e[0]) cur = peak = over = 0; prev = 0.0 for t, e in events: if cur > capacity: over += t - prev cur += e; prev = t; peak = max(peak, cur) return peak, over / T hours, cph, avg = 2, 120, 3 # 高峰:120 通/小时 × 3 分钟(均值并发 6 路) peak, over = simulate(hours, cph, avg, capacity=8) print(f"均值并发 {cph*avg/60:.0f} 路 | 容量 8 路: 峰值 {peak} 路, 过载时间 {over:.1%}") peak, over = simulate(hours, cph, avg, capacity=12) print(f"均值并发 {cph*avg/60:.0f} 路 | 容量 12 路: 峰值 {peak} 路, 过载时间 {over:.1%}") print("练习:把 calls_per_hour 改成你的真实高峰话务,找过载恰好归零的容量")

三、供给侧:官方基准告诉我们什么

Qwen3-ASR 技术报告 §2.4 Table 2(vLLM v0.14.0、BF16、约 2 分钟音频/请求、GPU 型号未公开)——这是唯一官方数据,完整抄录:

并发0.6B1.7B
RTF吞吐(秒音频/秒)TTFT 均值/p95 (ms)RTF吞吐TTFT 均值/p95 (ms)
10.009410692 / 1050.014867102 / 113
20.0111181103 / 1680.0153131117 / 170
40.0122327132 / 2030.0169237135 / 192
80.0147543228 / 4170.0200400224 / 382
160.0194826459 / 8820.0264606443 / 791
320.02911099820 / 15750.0397806847 / 1570
640.043514711631 / 31960.062110311597 / 2942
1280.064020003210 / 61950.105012203392 / 6227

读这张表的三个要点(都是能从数字直接看出来的):

  1. RTF 随并发增长很慢(批处理摊销:并发 ×128,RTF 只 ×6.8)——0.6B 的算力根本不是瓶颈;
  2. TTFT 随并发增长极快(×128 并发,p95 从 105ms 涨到 6195ms,×59)——排队延迟先撞线;
  3. 吞吐上界 ≈ 2000 秒音频/秒(0.6B 单实例)——这是「延迟不管的话」的理论天花板:2000 路通话 × 1x 实时。
  4. 顺带一个口径关系(防读混):「吞吐」列就是聚合 RTFx;「RTF」列 = 并发数 ÷ 吞吐(验证:128 ÷ 2000 = 0.064,正是表中 RTF@128)——前者聚合、后者每路。
import warnings; warnings.filterwarnings("ignore") import numpy as np import matplotlib.pyplot as plt conc = np.array([1,2,4,8,16,32,64,128]) rtf = np.array([0.00940,0.01108,0.01224,0.01472,0.01936,0.02912,0.04352,0.06400]) ttft = np.array([105,168,203,417,882,1575,3196,6195]) # 两点拟合幂律模型(方法演示):RTF(N)=RTF1·N^a,TTFT(N)=TTFT1·N^b a = np.log(rtf[-1]/rtf[0]) / np.log(conc[-1]) b = np.log(ttft[-1]/ttft[0]) / np.log(conc[-1]) N = np.logspace(0, np.log10(256), 100) fig, ax1 = plt.subplots(figsize=(8, 4)) ax1.loglog(N, rtf[0]*N**a, "-", label=f"RTF model (a={a:.2f})") ax1.loglog(conc, rtf, "o", label="RTF: Table 2 (0.6B)") ax1.axhline(0.5, color="g", ls="--", alpha=0.6); ax1.text(1.1, 0.55, "RTF SLO 0.5", color="g", fontsize=8) ax1.set_xlabel("concurrent streams"); ax1.set_ylabel("RTF", color="C0") ax2 = ax1.twinx() ax2.loglog(N, ttft[0]*N**b, "-", color="C3", alpha=0.7, label=f"TTFT model (b={b:.2f})") ax2.loglog(conc, ttft, "s", color="C3", label="TTFT p95 (ms)") ax2.axhline(1000, color="r", ls="--", alpha=0.6); ax2.text(1.1, 1150, "TTFT SLO 1s", color="r", fontsize=8) ax2.set_ylabel("TTFT p95 (ms)", color="C3") fig.legend(loc="upper center", ncol=4, fontsize=8, bbox_to_anchor=(0.5, 0.98)) plt.title("Two constraints vs concurrency (log-log) - which line hits SLO first?") plt.tight_layout(); plt.show() n_rtf = (0.5/rtf[0])**(1/a) n_ttft = (1000/ttft[0])**(1/b) print(f"RTF 约束: SLO 0.5 -> 约 {n_rtf:.0f} 路(不绑定)") print(f"TTFT 约束: SLO 1s -> 约 {n_ttft:.0f} 路(绑定!)") print(f"单实例容量 = min(两者) ≈ {min(n_rtf, n_ttft):.0f} 路;3 实例 x 0.7 安全系数 ≈ {3*min(n_rtf,n_ttft)*0.7:.0f} 路") print("注意:上表 TTFT 基于『整段 2 分钟音频提交』;通话流式(2 秒块)延迟结构不同——这正是必须自测的原因")
A40 部署要点(相对报告基准的异同) ① 好消息:A40 是 Ampere(cc 8.6),数据手册原生列出 BF16 Tensor 149.7 TFLOPS——报告的 BF16 权重直接可跑,--dtype auto/bfloat16 即可,无需 fp16 转换。② 报告基准 GPU 型号未公开,A40(48GB、696GB/s)与其差距未知——数字仍是参照,不是结论。③ 流式模式官方注明 vLLM 专用、无批处理/无时间戳。结论:上表 = 数量级参照与方法示范;你的 A40 + 通话音频的真实数字,必须用下面的流程自己测。

四、A40 上的最大实例数(0.6B)—— 算给你看

48GB 显存能塞几个 0.6B 实例?先立一个反直觉的事实:实例不是越多越好——每个实例都要完整复制一份权重和固定开销,KV 池总量随之被蚕食,显存侧容量随实例数单调下降。所以「最大实例数」有三档答案,由三笔账决定(权重为推导值,进程开销无公开数字、须实测):

账一:权重。0.6B 版总参数约 0.9B(含编码器),BF16 下 ≈ 1.8GB。

账二:每实例固定开销。CUDA context + CUDA graph 等进程级开销,vLLM 无公开数字(已核实;vLLM 启动时自行探测)——按 ~2GB 估,以 nvidia-smi 差分实测为准。

账三:每路通话的 KV 开销(推导)。0.6B 文本配置(28 层、GQA 8 KV 头、head_dim 128、bf16)下,每个 token 的 KV = 2×28×8×128×2B = 112 KB;一路通话占的 token = 音频嵌入(12.5/秒)+ 生成文本(约 5/秒):

通话时长token/路KV/路
1 分钟≈1,050≈0.12 GB
5 分钟≈5,250≈0.59 GB
10 分钟≈10,500≈1.18 GB

拓扑对比。vLLM 官方文档明确 gpu_memory_utilization 是每实例独立限额(默认已改为 0.92),并给出"同卡两实例各设 0.5"的官方示例。把 0.92 按实例数均分:

# A40 48GB · 0.6B(BF16)多实例显存推算 # 已核实:vLLM gpu_memory_utilization 为每实例独立限额(默认 0.92) # 推导:权重 ~1.8GB;KV 112KB/token(28层 x 8KV头 x 128维 x 2B x 2) # 估计(务必实测):执行器内激活 ~1.0GB;每进程 CUDA context ~0.4GB TOTAL, WEIGHTS, ACT, CTX, RESERVE = 48.0, 1.8, 1.0, 0.4, 0.5 KV = {"1min": 0.12, "3min": 0.35, "5min": 0.59, "10min": 1.18} # 每路并发通话 print(f"{'N实例':>4} {'每实例预算':>9} {'KV池/实例':>9} | {'5min总':>6} {'1min总':>6}") for N in [1, 2, 3, 4, 6, 8, 10, 12]: budget_i = (TOTAL - RESERVE - N * CTX) / N # 每实例执行器预算 kv_pool_i = budget_i - WEIGHTS - ACT tot5 = N * kv_pool_i / KV["5min"] tot1 = N * kv_pool_i / KV["1min"] print(f"{N:>4} {budget_i:>7.1f}GB {kv_pool_i:>7.2f}GB | {tot5:>5.0f}路 {tot1:>5.0f}路") print("\n三档结论(推导,激活/开销两项以 nvidia-smi 差分实测校正):") print("① 纯显存上限 ~12-14 个:每实例只容 1 路呼叫,权重重复占 22GB+,纯浪费") print("② 有意义上限 ~8-10 个:每实例至少容 4 路典型呼叫,但总 KV 容量已掉到单实例 1/2") print("③ 推荐 1-3 个:显存侧容量随实例数单调下降(75->70->64->59->48->37->26->15),") print(" 多实例只买『TTFT 延迟 + 故障隔离』——见拓扑扫描")

读表得出两条规则:

你的通话分布推荐拓扑理由
以短呼叫为主(<5 分钟)2–3 实例TTFT 是瓶颈,拆分近线性扩容
以长呼叫为主(≥10 分钟)1–2 实例大池KV 是瓶颈,拆分缩容
混合/未知拓扑扫描实测(见第五节第⑥步)同总压对比 1×0.92 / 2×0.46 / 3×0.31
要求故障隔离每实例本就是独立进程kill 一个不影响另两个
「最大实例数」的直接回答(三档) ① 纯显存上限 ≈ 12–14 个:每实例最低足迹 = 权重 1.8 + 激活 ~1.0 + 1 路呼叫 KV(1 分钟 0.12 / 5 分钟 0.59)≈ 2.9–3.4GB → 47.5 ÷ 3.4 ≈ 14。但此时每实例只容得下 1 路呼叫、权重重复占用 22GB——纯浪费,没有运营意义。② 有意义上限 ≈ 8–10 个:要求每实例至少容 4 路典型呼叫(5 分钟 → 足迹 ≥ 5.2GB → ~9 个;1 分钟 → ~14 个,但总 KV 容量已掉到单实例的 1/2)。③ 推荐 1–3 个:上表显示显存侧容量随实例数单调下降(权重 + 固定开销被线性重复占用,KV 池总量被蚕食),多实例只在「TTFT 延迟 + 故障隔离」上收益。A40 不支持 MIG(数据手册原文 "MIG support: No"),进程级多实例是唯一分割手段;所有估计中的激活/context 两项,用 nvidia-smi 差分实测后回来重算。

五、压测协议:业界标准六步法

业界事实上的参考基准是 NVIDIA Riva 的流式压测方法(非官方认证,业内普遍沿用其口径;官方开放基准 MLPerf Inference 测离线吞吐、不建模流式延迟 SLO):客户端 --simulate_realtime=true(每路流按 1x 实时回放音频)、--num_parallel_requests=N、固定块长(100/160/800ms),统计中间/最终延迟与 RTFX。FunASR/WeNet 同样以「并发 vs RTF 表」呈现。我们复刻这套方法(Qwen3-ASR 官方仓库没有自带压测脚本,已核实),协议如下:

步做什么通过/记录标准
① 数据代表性通话音频:8k μ-law → 16k 重采样(sox),含静音/双讲/噪声/方言;准备 3-5 条不同时长(30s/2min/5min)带人工标注的子集覆盖真实分布;标注子集用于压测中测 WER
② 基线单流跑通:RTF、TTFT、最终延迟、WERRTF₁ 是容量计算器的关键输入
③ 阶梯加压N = 1,2,4,8,16,32,64,128;每档稳态运行 5-10 分钟(流长 ≥ 2 分钟,错峰爬坡);同步采样 GPU 利用率/显存/功耗每档记录:RTF p50/p95/max、TTFT p95、吞吐、显存增量
④ 找拐点RTF p95 或 TTFT p95 首次违反 SLO 的最小 N,即单实例容量 N*SLO 示例:TTFT p95 ≤ 1s、RTF ≤ 0.5、最终延迟 ≤ 2s
⑤ 质量+稳定在 0.8×N* 下跑 2-4 小时:WER 与单流基线对比(不是空载——0 路没有转写结果);观察显存泄漏/碎片/重启WER 劣化 ≤ 1 个点;显存曲线平稳
⑥ 拓扑 + 故障(A)拓扑扫描:同一总压下对比 1×0.92 / 2×0.46 / 3×0.31 实例的聚合吞吐与 TTFT;(B)满载时 kill 掉 1 个实例,看流量转移的雪崩表现选出最优拓扑;验证 LB 生效;过载时间 < 30s
每路流开销:实测,不查表 权重与每路 KV 的推导见第四节(112KB/token);进程固定开销无公开数字——实测:起 1 实例 → 压 1/8/16/32 路记录 nvidia-smi --query-gpu=memory.used --format=csv -l 5 曲线,差分即每流开销,0 路基线即固定开销。多实例份额用 gpu_memory_utilization(每实例独立限额)或更细粒度的 --kv-cache-memory-bytes 控制;Triton 路线则调 instance_group count。

六、代码实例 ①:并发压测负载生成器

把 Riva 的「N 路并发 × 1x 实时回放」模式翻译成针对 Qwen3-ASR 服务(WebSocket/OpenAI 兼容端点均可适配)的可运行代码。发送/接收的消息格式按你的服务协议调整(注释已标出):

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""asr_loadgen.py — 流式 ASR 并发压测器(通话场景)

准备音频:  sox call_8k.wav -r 16000 -c 1 call_16k.wav
运行:      python3 asr_loadgen.py --url ws://gpu-host:9000/asr \
               --wav call_16k.wav --streams 16 --ramp 1.0
阶梯加压:  外层循环改 --streams(1,2,4,8,16,32,64,128),每档 5-10 分钟。
依赖:      pip install websockets numpy
"""
import asyncio, argparse, json, statistics, time, wave

import numpy as np
import websockets


def load_pcm16(path, sr=16000):
    """读 16kHz 单声道 s16le wav → 按 chunk_ms 切成字节块列表"""
    w = wave.open(path, "rb")
    assert w.getframerate() == sr and w.getnchannels() == 1, \
        "请先重采样: sox in.wav -r 16000 -c 1 out.wav"
    data = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16)
    n = int(sr * 0.1)                      # 100ms 一块
    return [data[i:i + n].tobytes() for i in range(0, len(data), n)]


async def one_stream(idx, args, chunks, results):
    await asyncio.sleep(idx * args.ramp)   # 错峰爬坡,避免建连冲击
    ttft = None
    audio_sec = len(chunks) * args.chunk_ms / 1000.0
    t0 = time.perf_counter()
    try:
        async with websockets.connect(args.url, max_size=None) as ws:
            for i, ch in enumerate(chunks):
                # 1x 实时节拍:第 i 块应在 t0 + i*0.1s 发出
                await asyncio.sleep(max(0.0, t0 + i * args.chunk_ms / 1000.0
                                        - time.perf_counter()))
                await ws.send(ch)
                try:
                    r = await asyncio.wait_for(ws.recv(), timeout=args.timeout)
                    if ttft is None:
                        ttft = time.perf_counter() - t0   # 首个部分结果
                except asyncio.TimeoutError:
                    pass                                  # 该块无返回(端点未到)
            await ws.send(json.dumps({"type": "end"}))    # 结束帧,按协议调整
            final = await asyncio.wait_for(ws.recv(), timeout=args.timeout)
        wall = time.perf_counter() - t0
        results.append({
            "stream": idx,
            "ttft_s": round(ttft, 3) if ttft else None,
            "rtf": round(wall / audio_sec, 3),            # >1 = 这路流跟不上实时
            "final_len": len(str(final)),
        })
    except Exception as e:
        results.append({"stream": idx, "error": repr(e)})


async def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--url", required=True)
    ap.add_argument("--wav", required=True)
    ap.add_argument("--streams", type=int, default=16)
    ap.add_argument("--ramp", type=float, default=1.0, help="每流错峰秒数")
    ap.add_argument("--chunk-ms", type=int, default=100)
    ap.add_argument("--timeout", type=float, default=5.0)
    args = ap.parse_args()

    chunks = load_pcm16(args.wav)
    results = []
    t0 = time.perf_counter()
    await asyncio.gather(*[one_stream(i, args, chunks, results)
                           for i in range(args.streams)])
    wall = time.perf_counter() - t0

    ok = [r for r in results if "error" not in r]
    bad = [r for r in results if "error" in r]
    ttfts = sorted(r["ttft_s"] for r in ok if r["ttft_s"] is not None)
    rtfs = sorted(r["rtf"] for r in ok)
    p = lambda a, q: a[min(len(a) - 1, int(len(a) * q))] if a else float("nan")

    print(f"\n=== 目标 {args.url} | {args.streams} 路并发 | 音频 {len(chunks)*0.1:.0f}s ===")
    print(f"成功 {len(ok)} / 失败 {len(bad)} | 整轮墙钟 {wall:.0f}s")
    if ttfts:
        print(f"TTFT  p50={p(ttfts,0.5):.2f}s  p95={p(ttfts,0.95):.2f}s  max={ttfts[-1]:.2f}s")
        print(f"RTF   p50={p(rtfs,0.5):.2f}  p95={p(rtfs,0.95):.2f}  max={rtfs[-1]:.2f}"
              f"  | RTF>1 的流: {sum(r > 1 for r in rtfs)}(超实时=延迟堆积)")
    for r in bad[:5]:
        print("错误样例:", r)
    # 判定:本档通过 = 全部流 RTF ≤ SLO 且 TTFT p95 ≤ 阈值


if __name__ == "__main__":
    asyncio.run(main())

七、代码实例 ②:压测期间的服务器侧监控

# GPU 侧:每 5 秒采样利用率/显存/功耗(压测同时开)
nvidia-smi --query-gpu=timestamp,utilization.gpu,memory.used,power.draw \
           --format=csv -l 5 > gpu_$(date +%s).csv

# 显存增量法测"每路流开销":同一实例分别在 1/8/32 路稳态下取 memory.used 差分

# 音频准备(通话 8k → 16k):
sox call_8k.wav -r 16000 -c 1 call_16k.wav

# 阶梯加压驱动(bash 伪循环,每档 300s,档间冷却 60s)
for N in 1 2 4 8 16 32 64 128; do
  python3 asr_loadgen.py --url ws://host:9000/asr --wav call_16k.wav \
      --streams $N --ramp 0.5 | tee -a ladder.log
  sleep 60
done

八、容量判定:把两条边装回你的场景

压测做完,你会得到两个数:硬上限(RTF→1 的吞吐天花板:0.6B 报告口径 ≈ 2000 路——延迟不管时的物理极限)和工程上限(TTFT SLO 撞线处,按报告数据推算 0.6B 单实例约 16–64 路,取决于你的 SLO 松紧)。真正的答案在两者之间、由你的压测收敛:

def capacity(rtf1_slo_ratio, instances, safety): """通用容量算式(把压测结果代进来)""" return rtf1_slo_ratio * instances * safety # 三个场景代入(把 measured 换成你压测出的单实例容量 N*): measured = 32 # ← 你的 A40 压测拐点(TTFT p95=1s 处的 N,来自第五节压测) for safety, tag in [(0.8, "正常运维(留 20% 余量)"), (0.5, "大促/保障期(留一半)"), (1.0, "极限(不建议)")]: print(f"{tag:24s}: 3 实例 x {measured} x {safety} = {capacity(measured, 3, safety):.0f} 路并发通话") print("\n实例数扫描(同一拐点 N*,70% 安全系数)——对照第四节显存表选拓扑:") for inst in [1, 2, 3, 4]: print(f" {inst} 实例: {capacity(measured, inst, 0.7):.0f} 路并发通话") # 需求对照:你的高峰话务(改我!) calls_per_hour, avg_min = 300, 3 erlang = calls_per_hour * avg_min / 60 print(f"\n需求侧: 高峰 {calls_per_hour} 通/小时 x {avg_min} 分钟 = {erlang:.0f} Erlang(均值 {erlang:.0f} 路并发)") print("峰值按均值 x 2~3 预估(泊松);需求峰值 vs 供给容量 -> 够/不够,一目了然")
压测结论行动
需求峰值 < 3 × N* × 0.7容量够;把 N* 与 SLO 写进值班手册,按月回归压测
需求峰值 ≈ 容量降级预案:超容量呼叫转「录音后转写」;高峰错峰;热备第 4 实例(如另一台机器)
需求峰值 > 容量扩容选项排序:① 加卡/加实例(线性)② 换 1.7B 前先看它 RTF 更差——容量反而更低,除非质量刚需 ③ 通话场景降采样或缩短 VAD 静音段降低无效音频 ④ 量化(如 int8)换 RTF
本专题的三个可迁移结论 ① 容量 = 峰值话务(Erlang/泊松)对撞双约束(吞吐+延迟)的交点,均值规划必翻车。② 小模型的瓶颈几乎总是延迟而非吞吐——RTF 0.009 的模型,先撞线的是 TTFT。③ 官方基准(GPU 型号未公开)只能当地图,不能当路标——A40 虽原生支持 BF16,但真实 RTF/TTFT 仍须自测,压测器你已经在上面拿到了。

检索练习

来源汇总(2026-09 核实)

一手来源 Qwen3-ASR 技术报告 §2.4 Table 2 / §4.5:arxiv.org/html/2601.21337v2(基准 GPU 型号未公开;流式配置 2 秒块/5 token 回退/4 块不封死) · 官方仓库(无自带压测脚本,已枚举核实):github.com/QwenLM/Qwen3-ASR · Riva 流式压测方法(--simulate_realtime/RTFX/中间与最终延迟;含 T4 数据):Riva ASR Performance(客户端:python-clients / cpp-clients) · FunASR 并发-RTF 基准方法:benchmark_onnx.md · WeNet RTF:onnxruntime README · Triton 动态批处理/实例组:batcher.md / model_configuration.md · A40 规格(Ampere/cc 8.6/48GB/696GB/s/BF16 Tensor 149.7 TFLOPS/MIG support: No):A40 Datasheet + CUDA GPU 算力表;MIG 支持列表(A40 不在列):MIG User Guide;vLLM gpu_memory_utilization 每实例语义(默认 0.92):vllm/config/cache.py · vLLM GPU 支持(cc≥7.5 含 T4)与 dtype:auto 对 BF16 权重选 BF16:vLLM GPU 安装文档 / vllm/config/model.py · Erlang 定义:Erlang (unit)。 未找到(如实标注):流式 ASR 每路流显存开销的公开量化数据;TTFT 的学术规范定义(Riva 的操作定义最严谨);LiveKit 仅测媒体面(lk load-test),非 ASR API 压测。
我是你的老师,别客气 把你的真实话务参数(高峰呼叫量/平均时长/SLO)贴回来,我帮你把容量算例算成你的数;压测跑出第一组 RTF/TTFT 曲线后带回来,我们一起找拐点。