会不会卡,取决于延迟堆在哪一段,而不是模型本身。把一帧从读取、送进推理、拿到结果并渲染的三个时间点都打出来,再看队列积压是否持续上涨,通常就能分清是抽帧与消费侧堵住,还是推理慢。两类问题的处理动作不同:前者优先降抽帧频率或加快消费,后者优先降输入尺寸、减少单次送帧数。
先取分段时间,再调参数。若从读取到进入推理的等待随运行时间变长、队列长度长期不回落,多半是抽帧或消费跟不上,降抽帧间隔最直接;若推理段稳定占大头并随画面内容(文字密集、运动剧烈)明显波动,则是推理瓶颈,应优先降输入分辨率或每轮送帧数。判断以时间戳日志和队列采样为依据,具体阈值需结合自己的机器和业务能接受的延迟确认。
在每帧上加时间戳,把端到端延迟拆成读取、推理、后处理三段
目标是拿到分段时间,而不是一个笼统的总延迟。在读取侧给帧对象挂上 t_read,在送入推理前后各打一点,在后处理与渲染结束时再打一点。下面的骨架是通用写法,model.infer 与 render 换成实际调用即可。
import time, json
def now_ms():
return int(time.time() * 1000)
def worker(frame_q, drop_stat):
while True:
frame = frame_q.get()
t_deq = now_ms() # 出队时刻,用于算排队
t_in = now_ms() # 送进推理前
result = model.infer(frame.image) # 替换为实际推理调用
t_out = now_ms()
text = render(result) # 后处理与渲染
t_post = now_ms()
print(json.dumps({
'seq': frame.seq,
'qlen_at_deq': frame_q.qsize(),
't_deq': t_deq,
'wait_ms': t_in - frame.t_read, # 读取到进推理,含排队
'infer_ms': t_out - t_in,
'post_ms': t_post - t_out,
'e2e_ms': t_post - frame.t_read,
}), flush=True)
单帧端到端 = t_post − t_read;拆开就是 wait_ms + infer_ms + post_ms。如果三项之和明显小于 e2e_ms,说明中间还有没打点的环节,例如解码缓冲或传输缓冲,需要补点后再判断。日志样例的格式如下,字段名可自定,但每行要能被解析:
{'seq': 10231, 'qlen_at_deq': 3, 'wait_ms': 0, 'infer_ms': 0, 'post_ms': 0, 'e2e_ms': 0}
先连续看一两分钟,比较 wait_ms 与 infer_ms 随时间的变化趋势,别只看第一条。趋势比单条数值有用。
统计队列积压长度与丢帧计数
队列长度用独立的采样线程记录,不要在推理循环里频繁打印,否则日志本身会拖慢消费。丢帧计数建议埋两处:入队时队列已满而丢弃(drop_on_enqueue),以及出队时发现帧的 t_read 距今超过你设定的最大等待而主动丢弃(drop_on_lag)。两处分开计数,才能分清是生产太快还是消费太慢。
def sampler(frame_q, drop_stat, stop):
while not stop.is_set():
print(json.dumps({'t': now_ms(),
'qlen': frame_q.qsize(),
'drop_enq': drop_stat['enq'],
'drop_lag': drop_stat['lag']}), flush=True)
time.sleep(1)
| 时间 | 队列长度 | 丢帧累计 | wait_ms 均值 | infer_ms 均值 |
|---|---|---|---|---|
| 每 1 秒一行 | 填写 | 填写 | 填写 | 填写 |
| 观察是否单调上升 | 是否回落 | 增长快慢 | 是否同步变大 | 是否稳定 |
判断上,队列长度在 0 附近波动、wait_ms 很小,说明消费跟得上;队列长期非零且呈锯齿不回落、丢帧计数持续上涨,说明消费慢于生产,单帧延迟里的大头其实是排队。
固定抽帧间隔做一次对照运行
抽帧在读取侧做,按帧时间戳跳帧,比 sleep 更稳定。两组间隔只改抽帧,其余参数(输入尺寸、并发数、每轮送帧数)保持一致,各跑一段相近的时长。
last_sent = 0
INTERVAL_MS = 200 # 对照组改成 500,其余不动
if frame.pts_ms - last_sent < INTERVAL_MS:
continue
last_sent = frame.pts_ms
| 配置 | wait_ms | infer_ms | post_ms | e2e_ms | 丢帧计数 |
|---|---|---|---|---|---|
| 间隔 A(较密) | 填写 | 填写 | 填写 | 填写 | 填写 |
| 间隔 B(较疏) | 填写 | 填写 | 填写 | 填写 | 填写 |
判读看两点:若 e2e_ms 随抽帧间隔变疏而明显下降,且下降主要来自 wait_ms,说明堵在管线,降抽帧有效;若 e2e_ms 基本不变、infer_ms 仍占大头,说明是推理瓶颈,降抽帧只是降低刷新频率,画面问答的延迟感不会因此变好。注意抽帧间隔变疏后丢帧计数也可能下降,别把它单独当成改善证据。
把推理耗时与画面复杂度对应起来看
推理变慢有时不是管线问题,而是某些画面本身更费时。在拿到 t_out 后记录几个元信息:resize 后的输入宽高、帧差大小(粗略判断运动剧烈程度)、亮度均值,以及是否属于文字或表格类画面(可用人工标注小样本,或用边缘密度做粗筛)。按这些分组统计 infer_ms 的中位数与最大值。
| 画面类型 | 帧数 | infer_ms 中位数 | infer_ms 最大值 | 备注 |
|---|---|---|---|---|
| 静态画面 | 填写 | 填写 | 填写 | 填写 |
| 文字或表格 | 填写 | 填写 | 填写 | 填写 |
| 快速运动 | 填写 | 填写 | 填写 | 填写 |
样本选择上每组取一段连续时间窗内的全部帧,别只挑最慢的几帧,否则结论会失真。异常帧的排查入口是:按 infer_ms 排序取最慢的一批,记录 seq、原始帧与 resize 后输入,离线单帧重跑一次。能复现说明是画面内容或输入尺寸导致;不能复现则更可能是并发争抢、显存波动或同一进程里的其他负载,需要回头看资源占用。
按结论排一个降级顺序并复测
调整顺序建议从代价最小的动作开始。第一步降抽帧间隔,损失的是时间分辨率,对回答「刚刚发生了什么」有影响,但不改模型输入形态,改完最容易验证。第二步降输入尺寸,对推理耗时影响直接,风险是文字类画面里的小字可能识别变差,必须回放样本看答案质量。第三步才是并发数:提高并发通常不会降低单帧延迟,反而拉长排队,只有在设备明显没跑满且队列为空时才值得试;反过来,适当降低并发有时能让单帧更快完成。把每一步单独改、单独复测,别一次改两项。
| 改动 | e2e_ms | wait_ms | infer_ms | 队列趋势 | 答案抽查 |
|---|---|---|---|---|---|
| 基线(不改) | 填写 | 填写 | 填写 | 填写 | 填写 |
| 降抽帧 | 填写 | 填写 | 填写 | 填写 | 填写 |
| 再降输入尺寸 | 填写 | 填写 | 填写 | 填写 | 填写 |
验收判据可以定三条:端到端延迟稳定落在你设定的可接受上限以内;队列长度不随时间单调增长、丢帧计数不再持续上涨;抽样问答的答案没有明显退化。三条同时成立再定稿,只满足前两条而答案质量掉了,说明降尺寸这一步走过了,退回上一档重新找一个折中点。