VAE/TAE 解码段:分片等价与精确切分
判断“能不能切、切哪个轴、怎么切才逐位精确、切了值不值”。核心纪律只有两条:
- 先证独立性,再谈切分——切法由“网络里有哪些跨轴耦合算子”唯一决定,不由“我们想切哪根轴”决定;
- 精确性不是口号,是四道闸的实测结论——CPU 真值对拍 → 端到端产物 md5 → 帧级健康指标 → 交换预算核算。
0. 范围与边界
范围(本技能拥有)
- VAE / TAE 解码(含任意 decoder 形态):能否沿某个轴切到多张卡、切哪个轴、怎么切才逐位精确、切了值不值;
- VAE encode:范围已含,当前无内容(待补)——编码段有任务时,按本技能同一套判据(独立性审计 → 选轴 → 状态 / halo → 四道闸)补进本技能,不要新开技能、也不要在别处复制这套判据;
- 计算与通信同技能:解码段的计算侧动作(减工作量 / 换实现 / 降精度)与其跨卡搬运(halo 交换、状态交换、all-gather)在本技能内闭环,不再拆成两个技能。
本技能拥有的 VAE 特有判据
潜帧边界切分、状态携带(state = 记忆块输入处末帧激活)、halo 宽度与 MINLEN 这类条件等价、交换预算核算、以及本对象的设备缺陷史。这些判据在别处没有,也不应复制到别处。
与既有技能的分工(只引用,不复制)
| 维度 | 归谁 | 本技能怎么做 |
|---|
| 计算动作的手段(pattern/compile 融合、Copy 消减、算子本体开发与调优) | pattern-dev / operator-dev(能力供给) | 只引用,不复制写法;本技能只说“这段计算还能不能少、能不能换” |
| 选档(开不开某特性、开哪一档) | DiT 侧参照 dit-perf-opt;特性真源 docs/zh/features/* | 只给 VAE 侧特有约束(潜帧边界 / 条件等价 / 交换预算),不复制选档表 |
| 通信方法论(并行形态选择、通信掩盖、通信账、差异归因) | dit-parallel-opt | 只引用;本技能只声明 VAE 侧特有判据(见上)。两者常同时出现在一个任务里,但判据完全不同:前者看吞吐/带宽,本技能看逐位相等 |
| 一致性判据(逐位 / 数值门 / 质量门三级) | accuracy-gate | 本技能产出“切分是否等价”的证据(闸①~④),判据口径引用它,不在此重定义等价分层 |
| 数字能否入库(同窗 A/B、窗口口径、噪声地板) | perf-gate | 收益宣称必须按它的口径出;本技能不定义测量纪律 |
| 交付搬运 / 装载预热(mp4 编码、worker→API 通路、落盘、权重加载、编译预热、镜像预热) | host-opt | 解码段之外的 host 固定开销不属本技能;本技能只把“解码段占比”作为是否值得切的输入 |
| 通用排障流程(CPU 真值 → 指标量化 → 逐层打点找首个分叉 → 算子隔离 → 阈值扫描 → 陈旧内存判定) | accuracy-gate/references/silent-failure-localization.md | 只留本对象的判定与规避(references/troubleshooting-cann-upsample.md),通用流程单向引用 |
第 0 步:先算账,值不值得切
切分只省“解码那段”,不会让 DiT 变快。先量清楚解码在 e2e 里的占比,再决定投入:
同环境实测(15 s / 768P / 4 步;绝对值按数字纪律出库,见 {run_results_dir}/archive/) | 量级 | 占 e2e |
|---|
| 视频 TAE 解码(8 卡各自完整解一遍) | 亚秒级 | 个位数百分比 |
| 音频 VAE 解码(与视频并行) | 亚秒级,明显小于视频解码 | 远小于视频解码(占比需现场量) |
| 交付开销(mp4 编码 + worker→API 通路) | 秒级 | 个位数百分比,明显高于视频解码(数倍) |
| DiT 去噪 4 步 | 十秒级 | 绝大部分(近九成) |
⇒ 解码占比 <5 % 时(占比本身要现场量,别沿用本文的数字),优先去啃 DiT 与 host 固定开销(后者见 host-opt);只有当解码占比高(大分辨率 / 长视频 / 解码器本身重)时,切分才划算。同时记住:解码若已到屋顶线(同环境视频 TAE 的 cube_utilization 已到接近饱和的量级、aicore_time == Task Duration),kernel 级没有余量,只能靠“减少工作量”——切分(摊到多卡)与降精度是仅有的两条路。
第 1 步:证明独立性(不可跳过,也别信直觉)
把网络里所有可能跨轴耦合的算子列全,逐个判定“它是否把不同切片绑在一起”。判据只有一条:输出元素是否依赖切片边界外的输入。
for op in network.ops: # 逐算子
if op couples(axis): -> 该轴不可直接切(或需要 halo / 需要状态携带)
else: -> 该轴可自由切
两个真实对照(同一份 latent、同一台机器实测):
| 网络 | 跨轴耦合算子 | 帧轴切分结论 |
|---|
逐帧 2-D 预览网(Kijai tae-2d,2.45 M 参数) | 无:Clamp、3×3 conv、Upsample、逐帧 quantile 归一化全部逐帧独立 | 精确、无需 halo;实测 12/12 请求产物 md5 与基线一致,video_tae 耗时降到约四分之一(读数见归档 {run_results_dir}/archive/) |
时间维 taehv(官方 taehv-temporal,9.87 M 参数) | MemBlock(past):past 是上一帧在该块输入处的激活,逐块递推 | 帧轴切分不可能精确(第 t 帧依赖整段前缀),除非每个 rank 重放前缀 |
否证要点(最容易走错的一步):stock 实现里的 chunk_latents=5 / trim=3 / 末尾丢 12 是“边界伪影裁剪”,不是独立性证据。看到 chunk 化就以为“天然可切”是错的——必须回到“输出是否依赖切片外输入”这一条判据。同理,“8 个 rank 解出来的结果逐位相同(指纹一致)”只说明当前是冗余计算,不说明可切。
细节与判定表见 references/independence-proof.md(含如何把审计写成可复核的 proof 记录)。
第 2 步:按独立性选轴
所有跨轴算子都不跨 T 轴 -> 帧轴(或 batch 轴)等分;无需 halo
存在 1 帧深度的因果递推 -> 帧轴不可分;改用「状态携带切分」(见第 3 步)
跨轴算子只跨 H/W(空间) -> 空间分块 + halo(halo ≥ 该层感受野),精确
同时跨 T 与 H/W -> 只能做 (T, H, W) 对齐的块状划分,或退回单卡
- 空间分块:逐帧卷积网(含 3×3 conv 与最近邻上采样)沿 H/W 切,halo 取该点感受野即可精确。注意交换代价(见第 5 步):按输出张量形状手算字节数(T×H×W×C×dtype),视频输出单卡 fp32 就达数 GB 量级(形状与读数见归档
{run_results_dir}/archive/),直接 all-gather 会把省下的时间全吃掉。
- 帧轴:优先选它——通信量小、halo 无需计算、实现最简单;前提是第 1 步证明通过。
第 3 步:递归网络的正确切法 = 潜帧边界切分 + 携带每块末帧状态
这是实测中最有复用价值的一条。若网络里有“上一帧反馈”的记忆块(MemBlock / past / state 之类):
- 非递归层逐帧独立:conv / upsample / 时间上采样(
TGrow)都不跨帧 ⇒ 只有记忆块需要状态;
- 状态定义:
state[i] = 上一段末帧在 MemBlock i 输入处的激活(不是输出!)。首段用 zeros pad 复现整段语义(等价于 F.pad(view, ..., 1, 0)[:, :tt]);
- 切分点必须落在潜帧边界:本案例
time_upscale=4,潜帧 j ↔ 输出帧 4j,因此每个潜帧边界在所有层都是帧边界(每级时间上采样都是整数倍)。这条不成立时,必须按“最粗那一级”的边界对齐;
- 段的顺序执行是必须的(第 k 段依赖第 k−1 段末帧状态)——所以它省的是显存与设备缺陷,不是并行度;要并行就得放弃精确性。
可直接抄的骨架:scripts/sliced_state_decode.py(apply_sliced(model, x, patch_size, slices, state=None))。同环境实测(同一 latent、同一权重):
| 环境 | 整段 | 2 段 | 4 段 |
|---|
| CPU(等价性真值) | 基准 | bitwise 等价,max|d| = 0 | bitwise 等价,max|d| = 0 |
| NPU 时间/峰值显存 | 基准 | 耗时降到约四成、峰值显存约减半 | 耗时与 2 段基本持平、显存再降一档 |
| NPU 尾段正确性 | 自某个绝对帧号起塌陷(首坏帧号属会话读数,见归档) | 塌陷消失(与 CPU 逐桶一致) | 同样干净 |
语义与推导细节见 references/sliced-state-decode.md。
第 4 步:判定必须与 rank 无关(否则死锁)
进入“要通信”的路径(状态交换、all-gather)的条件只能由 env + shape 推导,绝不能依赖 rank:
# 对:所有 rank 得到同一结论,要么全进集体通信,要么全不进
pieces = shard_pieces_from_env(env) # 全 rank 一致
ok = pieces >= 2 and world >= 2 and shape_is_divisible(shape)
if ok: ...collective...
反例(实测中踩过):rank < pieces 的选举式参与,只在“输出 shape 可由算数独立推出”时安全;一旦接收方需要知道别的 rank 的输出形状来分配缓冲,就必须全员参与。
第 5 步:交换预算必须先算(先算再实现)
交换量 = 输出张量字节数 × (pieces-1)/pieces × (1 - 可压缩比)
可接受 ⇔ 交换耗时 < 切分省下的计算时间
同环境判据:按输出张量形状与 dtype 手算(T×H×W×C×4),fp32 视频输出单卡就是数 GB 量级(形状与读数见归档),all-gather 不可接受;改 uint8(解码输出已 clamp 到 [0,1])降到其约四分之一,或不交换——按 tile 直接写进编码器/落盘。先算这笔账,再决定轴;否则“切完更快”会在通信处翻车。算式细则与逐方案对照见 references/verification-and-budget.md §1。
第 6 步:切分同时是“规避设备缺陷”的手段
切分不只提供并行度与显存收益,也是绕过设备算子缺陷的开关:本对象遇到的是“最近邻上采样在单次调用 batch 过大时少写输出尾部”(读到陈旧显存 ⇒ 成品从某个绝对帧号起塌陷,全程无报错、形状与帧数都正确)。
- 缺陷要自己判定,不能由“切完就好了”反推——否则无法区分“切分治愈了缺陷”与“缺陷本来就不出现”。本对象的判定实验(算子隔离 → 等价写法交叉验 → 规模阈值扫描 → 陈旧内存判定)与判读规则见
references/troubleshooting-cann-upsample.md §2,此处不重复;
- 规避手段:把整段递推切成多段,让该算子单次调用 batch 落到干净区(即第 3 步的状态携带切分)。不要用
repeat_interleave 复制替代空间上采样——数值等价,但额外分配整张输出张量(见下方陷阱清单);
- 复核与回退:换 CANN / 驱动 / 形状后必须重做判定实验;确认缺陷已修时,“分段”只剩省显存一个理由,需按回退条件重新评估是否保留(
references/troubleshooting-cann-upsample.md §4)。
已知陷阱清单(都是静默失败,必须逐条自查)
| 陷阱 | 现象 | 正确做法 |
|---|
all_gather_into_tensor 传 torch.stack(list) | 集合通信写到栈副本上,原列表一片未初始化 ⇒ 全错但无报错 | 先 torch.empty((world, *shape)) 真张量,再 gather,再切片拼接 |
repeat_interleave 逐轴复制当 upsample 替换 | 数值等价但多分配整张输出,显存紧张时 OOM | 省显存选“切片”,不选“复制” |
rank < pieces 选举式参与 | 部分 rank 不进集体通信 ⇒ 死锁或半张量 | 见第 4 步:判定与 rank 无关 |
| 按“chunk 化”推断可切 | 相关性/纹理由此而错,静默 | 回到“输出是否依赖切片外输入” |
| 只看产物 md5 就宣布无损 | md5 只证明“优化没改变本链产物”,不证明解码器忠实(基线自己也可能用了劣质解码器) | 另做与真值解码器的逐帧对照(实测:官方 TAE 对 stock VAE 的逐帧误差为个位数 /255 量级、相关性接近 1;2-D 预览网误差大一个数量级以上、相关性几乎为 0,且含数十段冻结帧;绝对值见归档) |
有损/条件性分片:必须带“精确性前置条件”
分片在某些长度下才逐字节精确时,代码里必须同时给出前置条件、自动修复与回退:
- 同环境音频 VAE 分片:逐字节精确只在片长满足某个下限时成立——该下限由
halo 宽度 / 记忆块几何决定,
须按现场片长重扫(重算与判定方式见 references/verification-and-budget.md §3;具体常数不得沿用历史值);
短片实测绝大多数样本不逐字节相等(相对偏差极小,读数见归档)。修复:按扫描出的下限设 MINLEN,
短片补零到该帧数后 ndiff = 0;
- 扩到 8 片反而不逐字节精确(同一实现、仅片数变化)⇒ 片数不是越多越好,片数必须由等价性判据决定,不能由“卡多”决定。
验证流程(四道闸,缺一不可)
| 闸 | 做法 | 合格判据 |
|---|
| ① CPU 等价性 | 整段 vs 2/4 段在 CPU 上逐元素对拍 | max|d| = 0(bitwise);不为 0 先怀疑状态搬运写错 |
| ② 端到端产物 | 同窗口 4 请求(1 冷 + 3 热),比 md5 | 4/4 md5 一致;片数/开关的开与关产物一致 |
| ③ 帧级健康 | 桶化高频能量、帧间差、近重复帧段 | 无“后半段塌陷/平坦段”:同环境坏片后半段各桶 hf 明显低于健康档(修复后回到健康档量级),坏帧 PNG 体积只有健康帧的几十分之一(≈全平) |
| ④ 预算与代价 | 交换量、耗时、峰值显存 | 交换耗时 < 省下的计算时间;显存不升(同环境实测反而明显下降,约减半) |
工具:scripts/shard_equivalence_check.py(①,CPU 对拍)、scripts/frame_health.py(③,张量或 mp4 的桶化健康指标)。
判据细节与阈值见 references/verification-and-budget.md;三级等价口径与质量门属 accuracy-gate。
分片“到底有没有生效”:先证明切片发生,再谈等价与收益
“看起来生效了但收益测不出 / 结果不对”时,先用状态量(pieces 的实读值、片内 shape、state 是否为 None)证明分片确实发生,再做闸① 逐位对拍。顺序反过来会把静默回退(门控没生效 ⇒ 仍是整段冗余解码)误判成“无损”。探针写法、两侧对照表与逐层收窄的判别量见 references/parallel-scope-effectiveness.md(本节不重复)。
脚本调用时机
| 脚本 | 何时调用 |
|---|
scripts/sliced_state_decode.py | 网络里有记忆块、要按潜帧边界切分并携带状态时:抄它的 apply_sliced,替换两个 ADAPTER 钩子 |
scripts/shard_equivalence_check.py | 实现完切分后第一件事:CPU 上整段 vs N 段 bitwise 对拍(不通过就不要上卡) |
scripts/frame_health.py | ② 通过后做 ③:对张量或 mp4 算桶化 hf / 帧间差 / 近重复帧段,定位“从第几帧开始坏” |
维护与更新
- 触发:解码器结构 / 权重形态变化、CANN 或 torch_npu 版本升级、交换拓扑变化、本对象出现新的失败形态时,按 dev-workflow 的复盘流程刷新本技能(判据进正文,实测数字按数字纪律出库或带作用域)。
- 新增内容的归属:VAE encode 段一旦有经验,按 §0 的范围用同一套判据补进本技能正文与
references/;计算手段 / 通信方法论 / 等价判据 / 数字口径仍只引用(pattern-dev / operator-dev / dit-perf-opt / dit-parallel-opt / accuracy-gate / perf-gate),不在本技能复制。
- 如何判定记录仍存在(先复核、再套用):
- 设备缺陷类:按
references/troubleshooting-cann-upsample.md §2 的三条判定实验(算子隔离 / 等价写法交叉验 / 规模阈值扫描)跑一遍;阈值与首坏索引不再复现 ⇒ 该条已失效,删除或改写为“已被修复”,并按该文件 §4 决定分段是否保留;
- 可切性 / 独立性类:拿目标网络的算子表重跑
scripts/shard_equivalence_check.py 的逐位对拍;不再为 max|d| = 0 ⇒ 结构或版本已变,重做独立性审计(references/independence-proof.md §1 表);
- 条件等价类(
MINLEN 一类):用当前长度 / 形状重算前置条件并重跑闸①;条件不再成立 ⇒ 前置条件与自动修复一并重写,不得沿用旧常数;
- 占比 / 收益类:按
perf-gate 同窗重测,并写明步数档 + 分辨率 + 卡数口径(同一模型不同步数档的固定开销占比差异极大)。
- 失效信号(出现任一条即视为对应记录过期):换版本后首坏索引 / 阈值区间变了;同一实现仅改段数就不再逐位相等;同窗对照差异落进噪声地板;解码段占比不再领先(结论退回“先去啃 DiT 与 host 段”)。
- 删除纪律:确认已修复 / 不再复现的条目直接删除,不要留“以防万一”;绕行一律走可一键关闭、可整体删除的开关,不长期分裂代码路径。
结论是动态的:记判据,不记死结论
本技能里出现的每个数字都是某次观测,写进来是为了让你知道“该期待什么量级、该怎么判”,不是让你
照抄。判断随下面这些轴变化——任何一条变了,旧结论就不再成立,必须重测(这也是为什么本技能把
流程与判据放在正文、把数值放在“实例”里):
| 会变的轴 | 为什么会推翻旧结论 | 重测方式 |
|---|
| 硬件 / 驱动 / CANN 或框架版本 / kernel 实现 | 阈值、首坏索引、数值路径都可能随实现改变(同一个算子换版本可能就好了,也可能新坏) | 重跑阈值扫描 + 逐层打点 |
| 模型 / 形状 / 序列长度 / 分块参数 | 阈值与首坏索引通常是形状的函数,不能按比例外推 | 用真实形状重扫 |
| 布局 / dtype / 自动混合精度是否真的生效 | “autocast 没下转”这类观察是版本相关的 | 开关对照一次(逐位比对)即可判定 |
| 并行拓扑与卡数 | 参与方式、交换量、状态边界都会变 | 重做独立性审计与预算核算 |
| 窗口 / 邻居负载 / 共享机器 | 绝对值漂移可能大于收益本身 | 同窗 A/B(必要时 A/B/A 校正) |
| 权重 / 解码器 / 预处理约定 | 同一道门禁下语义可能完全不同 | 与独立真值做忠实度对照 |
| 用户裁决(某参数冻结、某档不采用) | 这是决策,不是技术结论 | 记录裁决与其出处,不要写成“更快/更好” |
写结论时请带上证据卡三件套,缺一件别人就无法复核、也无法判断它何时过期:
- 口径:哪台机器、什么形状/步数、哪个时间窗口、哪份权重与解码器;
- 判据:多大差异算成立、噪声地板是多少、阈值只二分到哪个区间(未定值就写“区间”);
- 失效信号:出现什么现象就说明这条结论不再适用(例如“换版本后首坏索引变了”“同窗对照差异接近地板”)。
案例索引(同环境口径下的观测,用于对照形态;数值本身不是普适结论)
每条都绑定“当时的口径与条件”,条件一变就重测(见上「结论是动态的」)。
- 帧轴切分精确:逐帧 2-D 预览网(
_decode_2d,按 chunk_latents 对齐切,无 halo)→ 12/12 md5 一致、video_tae 耗时降到约四分之一;代价为数 GB 量级的额外 HBM/卡。
- 帧轴切分否证:官方时间维 taehv 的
MemBlock 前缀依赖 → 改走状态携带切分(否证是结构性的,与版本无关;但换成别的递归网络要重新审计依赖范围)。
- 状态携带切分:CPU bitwise 等价;设备上实测 2 段即消除尾段塌陷(段数以现场阈值扫描为准,别照搬 2 这个数),且明显更快更省(耗时降到约四成、峰值显存约减半)。
- 切分绕过设备缺陷:CANN
nn.Upsample 大 batch 少写尾部(缺陷阈值与首坏索引随版本/形状变化,必须现场重扫;判定实验见 references/troubleshooting-cann-upsample.md)。
- 条件性分片:音频 VAE 2 片(按现场重扫出的下限设
MINLEN 修复短片)、8 片不精确的否证(精确性前置条件要按现场长度条件重算)。
- 交换量的口径是:按输出张量形状(T×H×W×C)× dtype 字节数手算,fp32 视频输出单卡达数 GB 量级 ⇒ 改 uint8 可降到约四分之一,或按 tile 落盘(尚未实现,作为待办;换分辨率/时长要按实际张量重算)。