显存够画面还是不连贯——Matrix-Game3.5 的流式输出该从哪查?

文章导读
显存够、进程不报错,画面却一顿一顿,通常不是算力不足,而是“生成—缓冲—消费”这条链路的节奏没对上:模型侧生成间隔大于显示刷新间隔,或者中间队列满了在丢弃,或者显示端在等前面。排查顺序建议固定:先在输出端打时间戳,把问题归到模型侧还是消费侧;再查缓冲队列有没有丢帧;然后用固定采样步长复跑看首帧;最后才动预处理与后处理。这条顺序的价值在于,每一步都能用日志或页面行为验证,不需要靠观感猜。需要说明的边
📋 目录
  1. A 在输出端打时间戳,分清生成慢还是显示慢
  2. B 检查缓冲队列是否在丢帧
  3. C 固定采样步长复跑,比较每次的首帧时间
  4. D 把预处理与后处理挪出关键路径再测
  5. E 用同一段输入做前后对照确认改动生效
A A

显存够、进程不报错,画面却一顿一顿,通常不是算力不足,而是“生成—缓冲—消费”这条链路的节奏没对上:模型侧生成间隔大于显示刷新间隔,或者中间队列满了在丢弃,或者显示端在等前面。排查顺序建议固定:先在输出端打时间戳,把问题归到模型侧还是消费侧;再查缓冲队列有没有丢帧;然后用固定采样步长复跑看首帧;最后才动预处理与后处理。这条顺序的价值在于,每一步都能用日志或页面行为验证,不需要靠观感猜。需要说明的边界是:如果现有日志里根本没有逐帧时间戳,先补打点再谈优化,否则后面所有对比都不可信。

显存够但画面不连贯,多数情况不是显存或算力不足,而是生成节拍与消费节拍不一致。建议先区分“生成慢”和“等待慢”:在生成完成与送显两处打时间戳看差值,再查队列水位与丢帧计数,然后用固定参数多轮复跑比较首帧与稳态耗时,最后才调整预处理与后处理的执行位置。所有判断需在同一段输入、固定参数、多轮对照下成立,单次观感或单次最快值不足以证明改动生效。

在输出端打时间戳,分清生成慢还是显示慢

这一段的目标只有一个:把“一顿一顿”归到模型侧还是消费侧。做法是在两处打点,一是模型侧一帧真正算完的时刻,二是消费侧真正提交显示或推流的时刻。两个时间点之间的差值表示这一帧在中间被压了多久,相邻两帧送显时刻的间隔才是人眼看到的节拍。

import time

# 位置①:模型侧一帧生成完成(后处理之前记一次,之后可再记一次)
t_gen_start = time.perf_counter()
frame = runner.step(state)
t_gen_done = time.perf_counter()

# 位置②:消费侧真正提交显示/推流
t_present = time.perf_counter()
sink.push(frame, ts=t_present)

log.info('id=%d gen_ms=%.1f delta_ms=%.1f',
         fid,
         (t_gen_done - t_gen_start) * 1000,
         (t_present - t_gen_done) * 1000)

三个量要分开看:gen_ms 是本帧生成耗时;delta_ms 是生成完成到送显之间的等待;相邻两帧 present 的间隔是实际节拍。判断方式可以先按这三种情况对号入座:gen_ms 大且比较稳定,说明模型侧本身慢,画面表现为均匀地慢,仍然连贯;gen_ms 正常但 delta_ms 持续变大,说明消费侧或传输侧在阻塞,帧在中间堆积;gen_ms 和 delta_ms 都不大、present 间隔却在抖,更可能是显示端、合成或刷新同步的问题,需要结合运行环境确认。把这段打点放在主循环里,跑同一段输入几十帧即可,不需要额外压测。

检查缓冲队列是否在丢帧

确认不连贯是否由队列满后主动丢弃造成。常见的实现是固定长度的环形缓冲,生产端快于消费端时队列迟早贴到上限,此时要么阻塞生产,要么丢弃旧帧,后者会让画面看起来“跳过一段”。所以队列必须记录三个字段:当前长度、历史最大水位、累计丢弃数。

class FrameQueue:
    def __init__(self, cap):
        self.cap = cap
        self.buf = []
        self.enqueued = 0
        self.dropped = 0
        self.max_watermark = 0

    def push(self, frame, ts):
        self.enqueued += 1
        if len(self.buf) >= self.cap:
            self.dropped += 1                      # 队满,主动丢弃
            log.warning('drop qlen=%d cap=%d dropped_total=%d',
                        len(self.buf), self.cap, self.dropped)
            return False
        self.buf.append((ts, frame))
        self.max_watermark = max(self.max_watermark, len(self.buf))
        log.info('push qlen=%d watermark=%d', len(self.buf), self.max_watermark)
        return True

丢帧计数也可以不依赖显式丢弃逻辑,用 enqueued 减去已消费数再减去在处理中的帧来推算,两种方式取一种即可,关键是能逐帧对账。判读上有一个很有用的区分:丢弃数持续上升、水位长期贴着上限,画面表现为动作或视角直接跳变,中间没有慢放的过渡;水位长期贴着 0、丢弃数为 0 但生成间隔大,画面表现为慢但连续。前者要处理队列消费能力,后者回到第一段看 gen_ms。日志建议按固定间隔汇总一行,别看单条,看趋势。

显存够画面还是不连贯——Matrix-Game3.5 的流式输出该从哪查?

固定采样步长复跑,比较每次的首帧时间

这一步判断不连贯是否集中出现在起始阶段。首帧通常包含图编译、显存池首次分配、权重加载或缓存预热,明显慢于稳态;如果卡顿只在开头几帧出现,后面的优化方向与全程卡顿完全不同。复跑前先把参数固定死,避免每次条件漂移:

  • 分辨率与帧数(或时长)固定
  • 采样步长 steps、随机种子 seed 固定
  • 引导强度、负向条件等采样参数固定
  • 输入 prompt 或动作序列固定为同一段
  • 精度(fp16 / bf16)与是否开启图编译保持一致
  • 预热轮次固定,冷启动与热启动分开记录
t0 = time.perf_counter()
first = runner.step(state0)
t_first = time.perf_counter() - t0
log.info('first_frame_ms=%.1f', t_first * 1000)
轮次首帧耗时(ms)第2帧起平均(ms)稳态最大(ms)备注
1冷启动
2热启动
3热启动
4与第1轮对比看差异是否只来自冷启动

建议热启动状态至少跑三轮,把每轮的首帧与稳态分开看。首帧明显慢、稳态正常,卡顿集中在开头,属于启动阶段问题,可以接受或单独预热一次;首帧正常、稳态慢或抖动,属于持续输出问题,回到第一、二段查生成耗时与队列水位。两种情形的处理动作不同,混在一起调容易白忙。

把预处理与后处理挪出关键路径再测

这一节验证是否是非核心步骤拖慢了每帧节奏。流式输出里,每帧除了模型推理,还夹着输入解码与归一化、latent 解码、上采样、格式转换与编码,这些串行执行时会直接占掉帧间隔。做法是保留同一套生成参数,只改流程编排,做前后两版对比。

显存够画面还是不连贯——Matrix-Game3.5 的流式输出该从哪查?
  • 版本 A(原流程):读输入 → 解码与归一化 → 生成 → latent 解码与上采样 → 编码送显,全部串在主循环里。
  • 版本 B(调整后):输入预处理提前批量做完并缓存;latent 解码与上采样放到独立线程或延后一帧输出;主循环只保留生成与送队列两段。

两版都按同一位置打点:预处理起止、生成起止、后处理起止、送显时刻。对比方式不是看总时长一个数,而是看三件事:相邻送显间隔是否变稳、gen_ms 是否被挤占、delta_ms 是否变大。需要提醒的边界是,把预处理后处理挪出关键路径只是减少每帧节拍里的占用,通常会让端到端延迟增加约一帧,也不会让单帧生成变快;如果 gen_ms 本身占大头,这一步收益有限,不要把它当成提速手段写进预期。

用同一段输入做前后对照确认改动生效

避免把偶然波动当成优化结果。对照组设置上,固定同一段输入、同一组采样参数、同一分辨率与帧数,A 组为改动前配置,B 组为改动后配置,建议交替跑(A B A B)而不是先跑完 A 再跑 B,以减少机器状态、缓存与后台负载带来的漂移。每组至少三轮,取分布而不是取最好的一次。

每轮需要记录的字段包括:每帧耗时(至少保留中位数与最大值)、首帧耗时、丢帧数 dropped_total、队列水位均值与最大值、以及生成完成到送显的 delta_ms。

组别/轮次首帧(ms)每帧耗时中位数(ms)每帧耗时最大(ms)丢帧数水位最大
A-1
B-1
A-2
B-2

判定改动有效可以先按这几条:在固定时长内丢帧数不再增长或增长明显放缓,队列水位不再长期贴上限;每帧耗时分布收窄,最大值收敛且中位数不劣化;首帧耗时没有因为改动明显变大,若变大需确认这部分成本是否可以接受。反过来,如果只有某一轮的最小值变好,中位数与最大值没有变化,一般按波动处理,不算生效。所有结论都要结合运行环境确认,换机器或换负载后建议再跑一次同样的对照表。