Hy-SimulMT 同传延迟高时,先不要急着换模型或加机器。把端到端延迟拆成三段看:音频攒够一块要等多久、模型处理一块要多久、译文拼出来再发出去要多久。前两段分别对应音频分块大小和批处理设置,这两个参数在大多数同传推理脚本里都能直接改,改完也能用日志验证。判断顺序建议是:先打时间戳确认瓶颈落在哪一段,再单独调分块大小,再单独调批处理,最后用双人对话音频跑一遍整链路确认跟得上对话节奏。
Hy-SimulMT 同传延迟高,优先怀疑两个地方:音频分块太大导致首字迟迟不触发,或批处理等待凑批把单路延迟拖长。做法是先在同一份推理脚本里打印每块音频的送入时间、推理起止时间和译文首字时间,把延迟拆成音频等待、推理、输出三段;再固定其他变量,单独改分块大小、单独改批处理设置,用显存占用和单路首字延迟做对照。适用场景是自建推理服务且能改启动参数;如果分块已经很小、批处理也已关掉延迟仍高,瓶颈多半在模型或解码策略,需要另开一轮排查。
在推理脚本里打印每块音频的送入和译文输出时间戳
时间戳要打在三个位置:音频块进入队列时、调用推理前、推理返回(流式输出时取首个 token)时。字段名建议固定成一套,后面画时间线要靠这些字段对齐。下面是一个通用骨架,model.translate 换成你自己的调用即可,数值以毫秒为基准、相对脚本启动时刻计算,避免跨机时钟偏差。
import time, logging
logger = logging.getLogger("simulmt")
def now_ms():
return int(time.time() * 1000)
def run_stream(chunks, model, batch_size=1, batch_timeout_ms=0):
t0 = now_ms()
for idx, chunk in enumerate(chunks):
rec = {"chunk_id": idx, "chunk_bytes": len(chunk),
"t_chunk_ready": now_ms() - t0}
rec["t_infer_start"] = now_ms() - t0
result = model.translate(chunk, batch_size=batch_size,
batch_timeout_ms=batch_timeout_ms) # 替换为实际调用
rec["t_infer_end"] = now_ms() - t0
rec["t_first_token"] = rec["t_infer_end"] # 流式输出时在首 token 回调里取
rec["t_emit_end"] = now_ms() - t0
rec["wait_ms"] = rec["t_infer_start"] - rec["t_chunk_ready"]
rec["infer_ms"] = rec["t_infer_end"] - rec["t_infer_start"]
rec["lag_ms"] = rec["t_first_token"] - rec["t_chunk_ready"]
logger.info(" ".join(f"{k}={v}" for k, v in rec.items()))
日志格式示例如下,数值仅表示字段长什么样,不代表任何实测结果。看日志时重点比 wait_ms 和 infer_ms 谁占大头:wait_ms 大说明音频攒块或排队在拖,infer_ms 大说明推理本身慢。
chunk_id=3 chunk_bytes=10240 t_chunk_ready=1240 t_infer_start=1252 t_infer_end=1680 t_first_token=1695 t_emit_end=1740 wait_ms=12 infer_ms=428 lag_ms=455
调整音频分块大小并观察首字延迟变化
分块参数通常在启动配置或推理脚本的入参里,名字多为 chunk_size_ms、audio_chunk_ms、`--chunk-ms` 一类,需要在你自己的配置文件里确认实际名称。改之前先把采样率固定,块字节数按 ms × 采样率 × 2(16bit 单声道)换算,避免改了毫秒数却和实际送入数据对不上。
对照取值建议从 160ms、320ms、480ms、640ms 各跑一遍,每次只改分块、其余参数保持不变。分块越小首字通常越早触发,但碎句和重复译文的概率会上升;分块越大上下文更完整,首字延迟也跟着涨。用上一节日志里的 lag_ms 和输出的断句质量一起判断,不要只看延迟数字。
| 分块大小 | 首字延迟 lag_ms | 单块推理 infer_ms | 断句/碎句情况 | 是否保留 |
|---|---|---|---|---|
| 160ms | 待填 | 待填 | 待观察 | 待定 |
| 320ms | 待填 | 待填 | 待观察 | 待定 |
| 480ms | 待填 | 待填 | 待观察 | 待定 |
| 640ms | 待填 | 待填 | 待观察 | 待定 |
修改批处理设置并记录显存与单路延迟
批处理相关参数常见的有 batch_size、max_batch_size,以及等待凑批的 batch_timeout_ms。如果服务端为了吞吐去凑批,单路请求就要在队列里等别的请求,同传场景下这类等待会直接体现为延迟。测试时让 batch_timeout_ms 先设为 0(不等待凑批),再逐步提高 batch_size,观察单路 lag_ms 是否随批大小上升。
显存监控用 nvidia-smi 就够,开一个终端持续采样:
nvidia-smi -l 1
# 或只看已用显存
watch -n 1 nvidia-smi `--query-gpu`=memory.used,memory.total `--format`=csv
| batch_size | batch_timeout_ms | 显存已用 | 单路 lag_ms | 是否出现排队 |
|---|---|---|---|---|
| 1 | 0 | 待填 | 待填 | 待观察 |
| 2 | 0 | 待填 | 待填 | 待观察 |
| 4 | 20 | 待填 | 待填 | 待观察 |
判断标准很直接:如果提高批大小后单路 lag_ms 明显变大而显存还有余量,说明批处理在拖慢单路同传,同传链路应优先降批或关闭凑批等待;如果显存已经接近上限,先降批再谈延迟。
用双人对话音频跑完整同传链路并标记延迟
参数调完要在真实节奏下验证。测试音频建议自己准备:16kHz、单声道 wav,两到三人轮流发言,每人每次 3 到 8 秒,中间保留自然停顿,并故意插入一两次抢话或长句,总长两到三分钟。不要用版权不明的素材,也不要用朗读式匀速音频,匀速音频会掩盖真实对话里的停顿和重叠。
人工标记规则建议先定死再听:以原文一句话说完的时刻为锚点,看译文首字出现落在哪个区间,分三档记录——正常(译文在说话人下一句开始前出现)、滞后一句(译文落后一个完整话轮)、滞后两句以上(已经开始堆积)。每段话轮记一行,最后统计三档各占多少,比只看单块延迟更接近实际体验。
把时间戳画成时间线并标出主要延迟段
把日志按 chunk_id 排开画成时间线,瓶颈在哪一段一眼就能看出来。下面是一个纯文本模板,横轴单位毫秒,直接按日志字段填即可:
时间轴(ms) 0 500 1000 1500 2000 2500
音频块 [c1 ][c2 ][c3 ][c4 ][c5 ]
推理 [i1] [i2] [i3] [i4]
译文 [t1] [t2] [t3]
等待段 |w1| |w2|
字段对应关系:每段音频块的起点是 t_chunk_ready,宽度等于分块大小;推理条起点是 t_infer_start,宽度等于 infer_ms;译文条起点是 t_first_token,宽度是 t_emit_end - t_first_token;w 段表示 wait_ms,即块就绪到推理开始之间的调度等待。画出来以后,如果 w 段连成片,先查队列和凑批;如果推理条明显变长,先查模型和批大小;如果译文条起点远落后于推理结束点,说明瓶颈在输出拼接而不是模型。