1. 推理慢在哪:DiT 每个去噪步,都在重复造轮子
先用一个每天都在发生的场景切入。用 SD3.5 Medium 或者 Flux.1 Dev 这类 DiT 架构模型出图,单张 1024x1024 的图像,在消费级显卡上走完 50 步采样,耗时通常在 4 到 8 秒。如果做视频生成或者高分辨率放大,这个数字会直接成倍往上翻。大多数用户第一反应是"换更快的采样器""上 LoRA 蒸馏""开 FP8 推理",但很少有人去问一个更底层的问题:这 50 步里,模型到底有多少计算是必须重算的?
DiT(Diffusion Transformer)的推理过程,本质上是一个 T 次迭代的去噪循环。每次迭代要把当前带噪特征从第一层一路送到最后一层,中间经过十几个甚至二十几个 Transformer 块,每个块又包含一次自注意力、一次 MLP 和一次 AdaLN 调制。算一笔粗账:假设模型有 28 个块、隐藏维度 1152,在 1024x1024 图像 patch 化后序列长度大约 4096,单步 forward 的浮点运算量就已经超过 100 GFLOPS。再乘上 50 步,总计算量奔着 5 TFLOPS 以上去。这就是为什么采样步数一多,推理时间就肉眼可见地拉长。
但问题在于,这 50 步并不是彼此独立的任务。它们是在同一个生成任务里,沿着一条噪声逐渐减小的轨迹,一步步把图像从纯噪声里"挖"出来。相邻两步之间,输入只发生很小幅度的变化。尤其是使用 DDIM 这类确定性调度器时,第 t 步和第 t-1 步的输入特征空间位置非常接近,模型内部各层学到的中间表征,自然也会高度相似。你可以把整个去噪过程想象成一个人从远处走近你:你第一眼看到轮廓,第二眼看到衣服颜色,第三眼才看到五官细节。每多看一眼,新增的信息其实很少,但你每次都得从头到尾重新打量一遍。
从这个角度看,扩散模型的推理流程里存在大量"时间维度的重复计算":模型在连续好几步里,反复处理几乎相同的中间特征。缓存加速的核心思路,就是把这层冗余识别出来,用"多占一点显存"换"少算一堆 FLOPs"——既然两步之间特征变化不大,那么后一步完全可以跳过某些块的完整重算,直接复用前一步缓存下来的中间结果。TeaCache 正是踩在这条逻辑线上,把 DiT 推理加速这件事,从一个纯工程 hack 变成了一个有明确理论抓手的方法。
这个思路在 NLP 和 CV 推理里都不新鲜,但真正落地的难点在于:你凭什么判断"特征变化足够小"?用什么指标来量化?如果指标选得不准,稍微激进一点就会导致画面出现可感知的伪影,稍微保守一点又省不了多少计算。TeaCache 给出的答案,是回到扩散模型最本质的一个信号上——Timestep Embedding,也就是时间步嵌入。
2. Timestep Embedding:比特征更早暴露变化趋势的"哨兵"
在深入缓存策略之前,先把 Timestep Embedding 这个角色说透。扩散模型里,每个去噪步 t 都有一个对应的数值时间步,通常范围在 0 到 1000,对应训练时用的噪声调度区间。直接把这个裸数值喂给网络是没用的,因为单一标量携带的信息量太少,所以模型会用一组正弦位置编码把它映射成一个高维向量,再经过一个小 MLP 转成调制参数。这就是 Timestep Embedding。
这套编码方式最初来自 Transformer 的位置编码,公式上就是不同频率的 sin 和 cos 函数。某个时间步 t 的嵌入向量,实际上是它在若干个不同频率基函数上的投影。由于频率是连续的,映射出来的向量天然具备平滑性:当 t 从 40 变到 39 时,嵌入向量在特征空间里只是移动了一小段距离,而不会发生剧烈跳变。这一点是后续所有缓存判断的基石。
DiT 沿用这套设计,但做得比 U-Net 更"激进":每个 Transformer 块都接收完整的时间步嵌入,通过 AdaLN 调制,把嵌入向量映射成 scale、shift 和 gate 三个参数,作用在块内部的 LayerNorm 和 attention 输出上。换句话说,时间步信息不是只在某个全局位置起作用,而是以调制信号的形式渗透进了每一个块的每一次前向计算。你甚至可以把每个 DiT 块理解成一个"受时间步条件控制"的小函数——条件变了,函数行为就跟着变;条件几乎没变,函数行为也就几乎没变。
这带来一个非常关键的性质:相邻时间步对应的嵌入向量在向量空间里是平滑移动的。因为正弦位置编码本身是连续函数,当 t 从 40 变到 39 时,嵌入向量的欧氏距离非常小;再经过共享的 MLP 调制层后,scale、shift、gate 参数的差异同样很小。扩散模型的调度器设计也强化了这一点——DDIM 在相邻步之间的输入变化本身就是连续的,不像某些采样器那样存在大跳变。
但这个性质本身并不直接等于"可以省计算"。真正的价值在于:timestep embedding 的变化趋势,可以提前暴露中间特征的变化趋势。虽然嵌入向量和块输出之间没有简单的线性映射,但可以用一个更朴素、更可操作的距离度量,把两者的相关性捕捉出来。TeaCache 的工程实现里,最关键的一步就是:缓存某个参考特征,通常是第一个 DiT 块的输入特征,然后计算当前步这个特征与上一步缓存值之间的 L1 距离。如果距离低于阈值,就认为"当前步的整体语义环境与上一步足够接近,后续块的重复计算收益不大",于是跳过后续一整个区间块的运算,直接复用上一步缓存的结果。
为什么用 L1 距离而不是余弦相似度?实测下来,L1 距离对这种逐元素幅值变化更敏感,而且计算成本极低——每个块只需要一次向量减法再加绝对值求和,开销可以忽略不计。更重要的原因是,扩散模型中前几步的变换幅度(噪声级别快速下降)和后几步的变换幅度(细节微调)天然不在一个量级,L1 距离的数值范围能直接跟阈值比较,方便做动态缩放调整。相比之下,余弦相似度在微小变化下仍然可能保持很高的相似度,敏感度不够,容易把应该重算的步骤也误判成"足够相似"。
还有一个实操里很容易被忽略的细节:时间步嵌入在 DiT 里不是独立存在的,它和文本条件、图像条件一起,在每个块内做调制融合。所以单纯比较"时间步嵌入向量本身"的距离,不如比较"当前步输入给模型的那个参考特征"的距离来得可靠。TeaCache 的做法是把这个参考特征当成"代理信号",它同时携带了时间步信息和上一步留下的语义状态,比单独看嵌入向量更全面。这一点在后面讲工程实现的阈值标定时还会再碰到。
3. 从 Block-level 到 Step-level:缓存粒度决定了加速上限
在 TeaCache 之前,业内已经有人在 Diffusion 模型里尝试缓存加速,最具代表性的是 DeepCache 的思路。DeepCache 原本是针对 U-Net 和 DiT 的层间缓存方法:它在网络里选一些"锚点层",缓存这些层的输出,对于后续层则用缓存输出来替代完整计算。这个方法在空间维度上做文章:相邻层之间的特征冗余度高,所以可以跳过一些层。我们可以把它类比为 Block-level 缓存——缓存发生在某个 Transformer 块或连续块组这个粒度上,目标是"减少单次前向里的层计算"。
但 Block-level 缓存有一个先天局限:它没有利用扩散过程中时间维度的连续性。实际推理里,真正让人痛的不是"某一层算得太多",而是"每一层在每个时间步都被完整重算一遍"。即使你把 28 个块里的一半都跳过,仍然要跑 50 步,那剩下的 14 个块还是得算 50 次。所以,如果要追求更极致的加速,缓存判断必须上升到 Step-level——也就是"以单个去噪步为粒度,决定当前这一步里哪些块可以被安全地跳过"。这正是 TeaCache 核心机制和 DeepCache 的差异所在。
这两种粒度的差别可以这样理解:
| 对比维度 | Block-level 缓存 | Step-level 缓存(TeaCache) |
|---|---|---|
| 优化维度 | 空间冗余:单次前向内的层间重复 | 时间冗余:多次采样步之间的重复 |
| 判断信号 | 层与层之间的输出相似度 | 相邻时间步的参考特征距离 |
| 缓存单位 | 某个 Transformer 块的输出 | 某个时间步的中间特征 |
| 典型加速范围 | 1.2x - 1.5x | 1.4x - 2.0x+ |
| 与采样步数的关系 | 与步数无关,每步收益固定 | 步数越多收益越明显 |
Block-level 缓存解决的是"空间冗余",让你在单步计算时少算几层;Step-level 缓存解决的是"时间冗余",让你在 50 步里去重一部分完全重复的块前向。它们的优化维度正交,所以严格来说不冲突,后续工程上完全可以叠加使用。但如果你只能选一个,Step-level 的收益通常更大,因为扩散推理的计算量几乎是线性地分配在 T 个时间步上,而单步内的层数分布相比之下没那么大。
TeaCache 把这个 Step-level 判断落地成了一套可计算的规则。以 DiT 的完整流程为例,具体是这样运行的:当第 t 步开始前,模型取某个参考特征,通常是第一个 DiT 块的输入特征,与上一步缓存的参考特征做一个 L1 距离计算。若距离小于预设阈值 δ,则说明当前时间步相对上一步,语义状态几乎没变,于是直接让其后的所有块复用上一步的输出,把这一步的大部分计算省掉;若距离超过阈值,则正常执行完整前向,并用当前的特征更新缓存。这个过程在每个去噪步都会执行一次,所以是严格按"步"来做缓存决策。
这个设计有几个值得注意的点。第一,阈值 δ 不是全局定死的,它应该随采样步数、引导强度、模型规模做调整;比如 50 步采样和 20 步采样,步与步之间的特征差异分布完全不同,同一个阈值在两套配置下的表现可能差出一倍。第二,跳过的是一个"块区间"而不是所有块——TeaCache 的做法是保留第一个块的计算,因为它既承担代理信号提取的功能,又是后续判断的锚点。第三,一旦发生缓存跳过,后续块用的就是上一步缓存的特征,而不是当前步实时计算的结果,所以本质上是一种"特征复用"而非简单的"输出拷贝",这也是它能保持画面质量的关键。
从 Block-level 到 Step-level 的演进,本质上是从"优化单步效率"到"优化整体循环效率"的变化。当你站在整个采样循环的角度看待推理成本时,才会发现时间维度的冗余比空间维度更值得优先处理。TeaCache 选择把判断基准放在"步"上,而不是放在"层"上,恰恰是因为扩散过程的连续性和 DiT 的调制机制,让"步"成了一个天然稳定的冗余度量单位。
4. TeaCache 核心机制拆解:从代理特征到阈值判定
下面把 TeaCache 的实现机制拆开来看。这里不贴完整源码,而是用伪代码和核心逻辑把骨架描述清楚,因为不同版本的开源实现细节略有差异,但核心逻辑基本一致。先看整体流程:
# TeaCache 核心逻辑伪代码 cache_feat = None # 参考缓存 for t in timesteps: # 1. 取参考特征:通常是第一个 DiT 块的输入 curr_feat = extract_reference_feature(model, x_t, t) if cache_feat is not None: # 2. 计算当前步与上一步参考特征的 L1 距离 dist = l1_distance(curr_feat, cache_feat) # 3. 用动态缩放因子归一化距离 # 实践中常取上一步距离或历史距离的滑动平均 rel_dist = dist / scale_factor # 4. 阈值判断 if rel_dist < threshold: # 5. 缓存命中:跳过后续块,直接复用缓存特征 x_t = reuse_cached_layers(x_t, cached_features) continue # 6. 缓存未命中:正常执行完整前向 x_t, cached_features = full_forward(model, x_t, t) # 7. 更新参考缓存 cache_feat = curr_feat.detach()这里有一个细节容易踩坑:到底拿"哪个块的特征"作为参考信号?不同的开源实现有不同选择,但经验上,取模型较浅层(第一个或第二个块)的输入特征效果比较好。因为浅层特征还没有经过多层注意力聚合,与原始输入和时间步信息的耦合更直接;而深层特征经过高度非线性变换后,与时间的相关性被稀释了,距离信号会变得不稳定。你可以在自己的模型上验证这个现象:把每一层的输入特征在完整采样过程中的 L1 距离分布打出来,会看到浅层距离分布平坦、有区分度,深层距离分布则容易出现大量接近零的"塌缩"区域,后者显然不适合做阈值判断。
第二个关键点是 CFG(Classifier-Free Guidance)模式的处理。在 CFG 模式下,同一个时间步会同时计算条件分支和无条件分支,这相当于每次迭代要跑两次模型前向。缓存特征还需要考虑两个分支的相互影响。最简单的做法是:两个分支独立维护各自的缓存,各自做阈值判断。但这样做的代价是缓存占用翻倍。更省内存的做法是:以条件分支为基准做判断,无条件分支复用同一套缓存标记。显存充足的场景下,我建议用独立缓存,因为无条件和条件分支的特征轨迹并不完全一致,强行共享缓存,容易在引导强度较高时引入可感知的结构伪影。
第三个细节是缓存更新策略。大部分实现里,一旦判定"未命中",会用当前参考特征覆盖旧缓存。但如果采样过程中出现剧烈的语义变化(比如 CFG 切换、文本条件中途变化),单次覆盖可能让缓存状态跳变得过于剧烈。稳妥的做法是给缓存更新加一个平滑系数,类似 EMA 的方式:
cache_feat = alpha * new_feat + (1 - alpha) * cache_featalpha 取 0.5 到 0.9 之间,既能跟上语义变化,又不会因为单步抖动导致后续步的误判。这个技巧在视频生成模型中尤其有用,因为视频模型的时间步变化比图像模型更平缓,EMA 能让缓存判断更稳定。
还有一个容易被忽略的点:缓存特征的 detach 问题。PyTorch 里如果不小心把缓存特征和计算图连在一起,梯度会贯穿整个采样循环,不仅浪费显存,还会让行为变得不可预测。生产环境里,必须确保缓存张量是脱离计算图的,并且整个采样循环都在torch.no_grad()下运行。
5. 工程落地:阈值标定、加速收益和参数选择
如果把 TeaCache 当做一个现成模块拿来用,那么你需要关心的超参数其实就两个:参考特征的选择位置,以及阈值 δ 的大小。但这两个参数在不同模型上的最优值差异非常大,盲目套用开源仓库里的默认值,大概率达不到理想效果。
先说加速收益的典型区间。以当前主流的 DiT 模型为例(12 到 28 个 Transformer 块,50 步 DDIM 采样),一个标定良好的 TeaCache 通常能带来 1.4x 到 2.0x 的整体采样加速。如果配合 FP16 或 FP8 混合精度推理,累计加速可以进一步叠加。这个加速收益直接来源于"被跳过的块前向次数"占总块前向次数的比例。你可以通过日志统计每一轮的缓存命中率,如果命中率低于 30%,说明阈值太保守;如果高于 70%,则要注意质量风险。实测中,40% 到 60% 的命中率是一个比较健康的区间。
但如果你把 δ 调得过大,加速比可以轻松突破 2.5x,代价是图像质量明显下降。具体表现为:细节纹理变得"平"、某些区域出现重复模式、边缘锐度下降。为什么是这些伪影?因为 TeaCache 跳过了块的完整计算,复用的是上一步的中间特征,而上一步的特征中缺少当前步本应注入的高频更新信息。当被跳过的步数变多,特征滞后效应累积,高频细节就会逐渐丢失。直观类比就是:你连续看重播的同一个画面,时间越久,画面里的运动细节就越模糊,最后就剩下一个静止背景。
所以阈值标定不能只看平均距离,还要看距离分布的形状。我的建议是:先在目标模型上跑一轮 50 步完整采样,记录每一步参考特征 L1 距离的数值分布。然后根据你想要保留的质量底线,把阈值设在该分布的中位数附近偏右的位置,不要盲目往右推。中位数是一个合理的起点,因为这意味着大约一半的步会被判定为"足够相似",对应的理论加速接近 2 倍。然后在这个基础上微调:如果画面质量没问题,逐步加大阈值;如果出现伪影,往回调 20% 左右。
再补充一个工程细节:TeaCache 的核心缓存内容其实很小。每个块只需要缓存一份中间特征,量级和该块的激活层相当。在 1024x1024 分辨率、序列长度 4096 的场景下,单个块中间特征的显存占用在几十 MB 量级,28 个块全缓存也不到 1GB。相比模型权重和完整前向所需的中间激活,这点显存增量几乎可以忽略。所以不要为了省显存去压缩缓存精度,强烈建议缓存特征始终保持在和计算路径一致的精度,比如计算路径是 FP16,缓存就用 FP16。否则一次精度降级,会让后续所有复用特征都带上误差,而且这个误差会逐步累积,最终反映在成片质量上。
6. 什么时候不该用 TeaCache:边界条件与避坑清单
写了这么多 TeaCache 的好话,也该说说它不适用的情况。任何一个加速方法都有边界,TeaCache 的边界来自它"复用上一步特征"这个行为的本质。
第一条硬边界:当采样步数非常少时(比如 4 到 8 步),TeaCache 的收益会急剧下降,甚至变成负收益。原因不难理解:步数越少,每一步之间的语义跳跃越大,参考特征的 L1 距离分布整体抬高,阈值几乎找不到安全的"命中点"。如果你强行加大阈值,那被跳过的每一步损失的信息占比会非常大,最终效果直接崩盘。所以 TeaCache 本质上是一个面向"标准采样步数(20 到 50 步)"优化的方法,不适合配合那些已经蒸馏到极限的低步数采样器使用。
第二条边界:超长视频生成和高分辨率图像生成场景,需要额外的注意力机制处理。比如视频生成里的时序注意力块,在不同帧之间也引入了额外的特征变化,TeaCache 如果在时间步维度上缓存了特征,却忽略了帧间差异,就可能在运动区域出现闪烁伪影。这类场景下,你需要把阈值调得更保守,或者只在静态背景区域启用缓存。一个可行的策略是:对每一帧单独计算参考特征距离,超过一定帧间差异时强制全量重算,这样可以在运动剧烈的片段自动关闭缓存。
第三条边界:与其他加速方法的叠加。TeaCache 和量化、剪枝、蒸馏并不是互斥的,叠加使用的收益也确实能进一步放大。但要注意叠加后的误差累积。TeaCache 本身是一个有损加速方法,量化又是一个有损压缩方法,两个有损操作叠加时,损失不是加法,而是乘法的关系。实测中,FP8 量化加 TeaCache 的组合仍然可用,但 INT4 量化加大阈值 TeaCache 的组合,几乎必然出现可感知的退化。想叠加使用的话,建议先把 TeaCache 阈值调保守,再去做量化。
还有一个从实际使用中体会到的点:TeaCache 在不同模型的"体质"上表现差异很大。有的 DiT 模型训练充分、特征冗余度高,TeaCache 开大阈值都没问题;有的模型本身就欠拟合,中间特征在相邻步之间抖动剧烈,这种模型用 TeaCache 就特别容易翻车。所以接到一个新模型时,不要直接照搬别人博客或仓库里的参数,花半小时跑一轮距离分布的可视化,把分布形态摸清楚,再定阈值,省下的时间远大于额外投入。
用一句话总结这个方法的本质:TeaCache 的价值不在于"绕过了多少计算",而在于它找到了一个可靠的信号(参考特征的距离),来判断哪些计算可以被安全地跳过。而 Timestep Embedding 在其中扮演的角色,是让这个距离信号本身就嵌入到了 DiT 每一个块的调制参数里——所以你不需要额外的模型或模块来预测特征变化趋势,只需要监视模型自己给出的参考特征就够了。这也是为什么 TeaCache 能在几乎不改变模型结构的前提下,把 DiT 推理显著提速。
如果你正在做 DiT 服务的推理优化,我的建议是:先把采样器理顺、精度降到合适档位,再引入 TeaCache;上线前一定要用至少 50 张以上的实测图做质量回归,别只看一两张生成结果。缓存类方法的间歇性特征很难靠直觉判断,一轮覆盖率足够的回归测试比任何理论分析都可靠。后面还可以尝试把 Block-level 和 Step-level 两层缓存叠在一起跑,在部分模型上能做到 2.5x 以上的综合加速,那篇文章的思路和踩坑经历,等我把数据整理完再单独写。