显存突然涨满,先别急着把 batch 调小。同传场景里常见的两种形态差别很大:一种是每处理完一段音频,占用回不到原来的基线,越跑越高,这更像音频缓冲或推理中间对象没释放;另一种是某段音频进来时显存瞬间跳高,处理完又落回去,这更像批处理或 chunk 设得太大带来的瞬时峰值。两者的处理方向相反,先拿曲线再动手,比直接改参数省事。
判断路径建议固定为三步:先用 nvidia-smi 按固定间隔采样,拿到整段同传的显存曲线,确认是缓慢抬升还是瞬间跳高;再在音频入队、出队和释放回调三处打印缓冲对象数量,看计数是否随轮次单调增长;最后固定同一段音频,把 batch_size 和 chunk 逐项调小做对照。曲线形态能区分泄漏与参数过大,但显存绝对值里还包含 CUDA 上下文和缓存池,不能只看峰值下结论。
用显存采样工具记录同传全程的占用曲线
目的是先拿到一条能和音频时间轴对上的曲线。不要只在出问题时抓一次快照,那样看不出形态。开一个终端常驻采样,把它放到一边,正常跑同传即可:
while true; do nvidia-smi -i 0 `--query-gpu`=timestamp,memory.used,utilization.gpu `--format`=csv,noheader,nounits >> gpu_mem.csv; sleep 1; done
如果怀疑是瞬时跳高,把 sleep 1 改成 sleep 0.2,采样窗口覆盖从开始推流到停止的完整过程。多卡或同卡有其他任务时,再补一条按进程归因,确认涨的是同传进程:
nvidia-smi `--query-compute-apps`=pid,process_name,used_memory `--format`=csv,noheader
曲线记录方式不必复杂:把推流开始、切换素材、停止的时间点用 date +%T 追加到 timeline.txt,事后把 memory.used 列和这些时间点对齐,用表格软件画一条折线就够判断。要注意的是,nvidia-smi 看到的是进程总占用,包含 CUDA 上下文和框架缓存池,所以关注点是“形态”而不是某一刻的绝对值。
在音频缓冲释放处打印缓冲对象数量
打印位置建议放三处:音频读入线程入队之后、推理消费取出之后、释放回调执行之后。三处用同一个计数函数,字段固定,便于用 grep 统计和做差。下面是一个可替换的骨架,把 audio_queue 换成项目里实际的缓冲对象即可:
import logging, time, os
log = logging.getLogger('audiobuf')
def snapshot(tag):
with buf_lock:
n_chunks = len(audio_queue)
n_samples = sum(len(c) for c in audio_queue)
log.info('ts=%s tag=%s chunks=%d samples=%d rss_mb=%d',
time.strftime('%H:%M:%S'), tag, n_chunks, n_samples,
rss_mb())
snapshot('push') # 入队后
snapshot('pop') # 推理取出后
snapshot('release') # 释放回调后
日志格式保持一行一条、字段顺序不变,例如 ts=12:01:03 tag=release chunks=0 samples=0 rss_mb=812,grep 出 chunks= 的最大值和最后一条,就能看出缓冲是否在持续累积。判断口径是:跑若干轮音频后 chunks 能回到 0 或稳定在缓冲上限以内,说明释放正常;如果 chunks 或 samples 单调增长且不回落,通常是某处仍持有引用,比如回调闭包、按 session 建的字典、或未 join 的线程。队列长度有上限并不等于没泄漏,样本数也要一起看。
临时调小批处理并观察显存峰值变化
先找到配置里与批处理和分块相关的项,典型位置如下(字段名按项目实际改):
inference:
batch_size: 4
max_batch_wait_ms: 20
audio:
chunk_ms: 1000
buffer_max_chunks: 8
对照取值可以这样设:batch_size 取 1、2 和当前值各跑一遍;如果峰值随 batch 明显下降,说明批处理是主要压力来源,再单独把 chunk_ms 按 500、1000、2000 试一轮。每次只改一个参数,用同一段音频、相同长度跑,记录峰值和结束后的回落值。若 batch_size=1 时显存仍随轮次抬升,基本可以把批处理从怀疑名单里排掉,回到缓冲释放方向继续查。
循环输入长音频观察显存是否稳定
短音频跑不出泄漏,需要把输入循环起来。用 ffmpeg 按实时速率循环读一个 16k 单声道素材,通过管道喂给程序的音频输入口:
ffmpeg -stream_loop -1 -re -i sample_16k_mono.wav -f s16le -ar 16000 -ac 1 - | your_app `--stdin-audio` -
-re 保证按真实语速推送,避免一次性灌满导致结论失真;-stream_loop -1 让素材首尾相接。程序侧也可以用一个定时器按块读 PCM 文件并循环,效果相同。测试时至少连续跑 30 分钟,每 5 分钟记录一次 memory.used 和 chunks 最大值;如果缓冲释放已经改对,显存应在某个水平附近波动而不是逐轮抬高。这一步只验证释放是否生效,不追求跑出多高负载。
按显存曲线形态判断是泄漏还是参数过大
把前面的采样、计数和对照结果填进下面这张表,基本能收敛到一种原因。表里的判断是模板口径,具体阈值需要结合自己的模型大小和显卡容量确认。
| 曲线形态 | 缓冲计数表现 | 更可能的原因 | 下一步动作 |
|---|---|---|---|
| 阶梯式抬升,处理完不回落 | chunks 或 samples 随轮次单调增长 | 缓冲或推理对象未释放 | 查释放回调、闭包引用、线程 join,配合对象统计 |
| 瞬时跳高后回到同一基线 | chunks 有上限,循环稳定 | 批处理或 chunk 过大 | 调小 batch_size 或 chunk_ms 做对照 |
| 每轮跳高且基线逐轮抬高 | chunks 缓慢增长 | 两者叠加 | 先修释放,再定 batch |
| 长时间平稳,个别素材段尖峰 | chunks 正常 | 该段音频过长、采样率或声道不符合预期 | 检查输入格式和分块长度 |
| 持续占用高但不再增长 | chunks 稳定 | 缓存池和 CUDA 上下文占用,不是泄漏 | 不必处理,关注峰值余量 |
需要注意的边界:torch.cuda.empty_cache() 这类操作只是把缓存还给驱动,不能修掉真正没释放的对象,把它当验证手段而不是修复手段;显存紧张时临时降 batch 或缩短 chunk 是止血,不改变代码里的释放问题。最终结论要同时满足“曲线形态”和“缓冲计数”两条证据,只凭一条容易误判。