Download code/gc25d/model.py from teawhite/maliang: direct link, hf CLI and curl.
- Browser
- Download file 41.5 kB
-
https://huggingface.co/teawhite/maliang/resolve/main/code/gc25d/model.py
- Command line
-
hf download hf://teawhite/maliang/code/gc25d/model.py
-
curl -L -o model.py https://huggingface.co/teawhite/maliang/resolve/main/code/gc25d/model.py
41.5 kB
| """gc25d 模型:Wan2.2-TI2V-5B DiT + 逐像素数字语义图条件(SPEC §6)。 | |
| x = dit.patch_embedding(latents) + dit.gc_sem_cond(sem_ids) # [b, 3072, 21, H/32, W/32] | |
| 形状对分辨率通用(全部从输入张量取);下面的数是 832×480(geometry 默认 GC_RES)的例子,1280×704 时 | |
| H/4×W/4 = 176×320、latent 44×80、token 22×40。 | |
| `gc_sem_cond`(SemCondEncoder)把 81×480×832 的 uint8 语义 id 视频压到 latent 网格: | |
| 1. LUT:raw 0..6 → 0..6,10 → 7,11 → 8,其余(含 UNKNOWN=255)→ 9(K=10); | |
| 2. 逐类 4×4 平均池化 one-hot → [b, K, 81, 120, 208](一类一类算,不物化全分辨率 one-hot); | |
| 3. 时间分组同 VAE:帧 0 重复 4 次 + 帧 1..80 → [b, 4K, 21, 120, 208]; | |
| 4. pixel-unshuffle ×4 → [b, 64K=640, 21, 30, 52]; | |
| 5. Conv3d(640, 512, 1) → SiLU → Conv3d(512, 512, (1,3,3), pad (0,1,1)) → SiLU → | |
| 零初始化 Conv3d(512, dim, (1,2,2), stride (1,2,2))。 | |
| 刚装上时最后一层权重与偏置全 0 ⇒ 输出恒为 0 ⇒ 前向与原模型逐位相同(tests/test_model.py::test_a)。 | |
| `gc_forward` 是 `actionrope.arope.arope_forward` 普通路径(offset_px=None, mask_input=None, arm=None)的照抄, | |
| 只在 patch embedding 后多加一项 sem 条件;block / RoPE 查表 / 逐 cell 文本 / 逐 token timestep 全部直接 | |
| import `actionrope.arope` 的函数,不复制。 | |
| 实现上的取舍(SPEC 没写死、这里定下来的约定,infer 侧必须一致 —— 它直接 import 本文件即可) | |
| ------------------------------------------------------------------------------------------ | |
| * 通道顺序:时间分组后通道 = k·4 + τ(k 类别,τ 组内帧 0..3),unshuffle 后通道 = (k·4 + τ)·16 + p·4 + q | |
| (p 行内偏移、q 列内偏移),即 einops "b k (f t) (h p) (w q) -> b (k t p q) f h w"。 | |
| * 第 9 类("其它 / UNKNOWN")的池化值 = 1 − Σ_{k<9} 池化值。池化值都是 1/16 的整数倍,fp32 下精确, | |
| 所以与逐像素判断"不属于前 9 类"再池化逐位相同,而且省一次全分辨率比较。 | |
| * one-hot 池化不依赖参数,放在 no_grad 里算([b,10,81,120,208] bf16 ≈ 40 MB/样本,1280×704 ≈ 2.26×),再 cast 到模块 dtype。 | |
| * 前两层 conv 用 PyTorch 默认初始化,但在 fork_rng + 固定种子下建:各 rank、各次运行初始化相同, | |
| 不吃全局 RNG(DeepSpeed 也会从 rank 0 广播,这里是双保险,且单卡 / 多卡结果可比)。 | |
| 白模条件(cond_type="whitebox",2026-10-12,GameCrafter-Parkour;见文件中部"白模条件"一节) | |
| -------------------------------------------------------------------------------------- | |
| 另一种控制视频:pk 渲染的白模(灰度,像素对齐 RGB,255 留作 UNKNOWN)。模块挂在**另一个名字** `dit.gc_wb_cond` | |
| (WhiteboxCondEncoder),结构与 SemCondEncoder 同构(逐帧 4×4 块特征 → 同一套 4 帧分组 + unshuffle → 1×1 → SiLU → | |
| 1×3×3 → SiLU → 零初始化 patch-stride conv),只有输入特征不同(5 通道:RGB 块均值、亮度块内标准差、已知像素比例)。 | |
| 默认一律是 "ids":不传 cond_type、dit 上没有 gc_wb_cond 时,所有函数的行为与加白模之前逐位相同。 | |
| """ | |
| from __future__ import annotations | |
| import functools | |
| import math | |
| import os | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from einops import rearrange | |
| from safetensors import safe_open | |
| from safetensors.torch import load_file | |
| from actionrope.arope import ( | |
| _block_forward, | |
| _embed_context, | |
| _per_token_timestep, | |
| build_world_freqs, | |
| ) | |
| from diffsynth.core.gradient import gradient_checkpoint_forward | |
| from diffsynth.models.wan_video_dit import WanModel, modulate, rope_apply, sinusoidal_embedding_1d | |
| UNKNOWN_ID = 255 | |
| SEM_COND_NAME = "gc_sem_cond" | |
| # raw id → 类别下标(K=10)。0..6 原样,10 玩家 → 7,11 boss → 8,其余(含 255)→ 9 | |
| RAW_TO_INDEX = {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 10: 7, 11: 8} | |
| NUM_CLASSES = 10 | |
| OTHER_INDEX = 9 | |
| # ---------------------------------------------------------------------------- | |
| # 词表(maliang / v10,2026-10-22) | |
| # ---------------------------------------------------------------------------- | |
| # "ids" = 上面的 K=10(v3/v4/v8 的 gc25d,逐位不变)。 | |
| # "parts" = v10 胶囊白模角色按部位拆开(数值代理,不过 VAE):玩家 / Boss 各 6 个部位 raw id,K=20。 | |
| # raw 10 / 11 = 玩家 / Boss 躯干(pelvis_spine, spine_neck, head),保持与 "ids" 相同的值; | |
| # raw 12..16 = 玩家 左臂 / 右臂 / 左腿 / 右腿 / 武器,raw 17..21 = Boss 同序;左右按骨骼名(l_* / r_*),不按屏幕。 | |
| # 部位由代码对 json['v9']['characters'] 的胶囊逐像素射线求交得到(capsules.py),推理时代码世界用同一函数画。 | |
| # collapse_parts():12..16 → 10,17..21 → 11,即退回 "ids" 词表。 | |
| PART_NAMES = ("core", "l_arm", "r_arm", "l_leg", "r_leg", "weapon") | |
| PLAYER_PART_IDS = (10, 12, 13, 14, 15, 16) | |
| BOSS_PART_IDS = (11, 17, 18, 19, 20, 21) | |
| RAW_TO_INDEX_PARTS = {**{i: i for i in range(7)}, | |
| **{raw: 7 + j for j, raw in enumerate(PLAYER_PART_IDS)}, | |
| **{raw: 13 + j for j, raw in enumerate(BOSS_PART_IDS)}} | |
| VOCABS = {"ids": (RAW_TO_INDEX, NUM_CLASSES), "parts": (RAW_TO_INDEX_PARTS, 20)} | |
| DEFAULT_VOCAB = "ids" | |
| assert all(len(m) == k - 1 and sorted(m.values()) == list(range(k - 1)) for m, k in VOCABS.values()) | |
| def vocab_spec(vocab: str) -> tuple[dict, int]: | |
| if vocab not in VOCABS: | |
| raise ValueError(f"vocab 只能是 {tuple(VOCABS)},得到 {vocab!r}") | |
| return VOCABS[vocab] | |
| def valid_raw_ids(vocab: str) -> frozenset: | |
| """条件图里允许出现的 raw 值(含 UNKNOWN)。其它值模型会静默当成 "other",prep / 代码世界应断言。""" | |
| return frozenset(vocab_spec(vocab)[0]) | {UNKNOWN_ID} | |
| def collapse_parts(ids): | |
| """parts 词表的 raw id → ids 词表(12..16 → 10,17..21 → 11);numpy / torch 都行,返回同类型新数组。""" | |
| lut = np.arange(256, dtype=np.uint8) | |
| lut[list(PLAYER_PART_IDS)] = 10 | |
| lut[list(BOSS_PART_IDS)] = 11 | |
| if isinstance(ids, torch.Tensor): | |
| return torch.from_numpy(lut).to(ids.device)[ids.long()] | |
| return lut[ids] | |
| def vocab_from_in_channels(in_channels: int) -> str: | |
| for name, (_, k) in VOCABS.items(): | |
| if k * T_GROUP * UNSHUFFLE * UNSHUFFLE == in_channels: | |
| return name | |
| raise ValueError(f"gc_sem_cond.proj_in 输入通道 {in_channels} 不对应任何词表 {tuple(VOCABS)}") | |
| # 空间:池化 4× + unshuffle 4× = 16× = VAE 空间压缩;时间:VAE 4×(帧 0 单独成组,这里重复 4 次补齐) | |
| POOL = 4 | |
| UNSHUFFLE = 4 | |
| T_GROUP = 4 | |
| HIDDEN = 512 | |
| SEM_COND_INIT_SEED = 20261010 | |
| def _lut(vocab: str = DEFAULT_VOCAB) -> torch.Tensor: | |
| m, k = vocab_spec(vocab) | |
| lut = torch.full((256,), k - 1, dtype=torch.uint8) | |
| for raw, i in m.items(): | |
| lut[raw] = i | |
| return lut | |
| def sem_ids_to_index(sem_ids: torch.Tensor, vocab: str = DEFAULT_VOCAB) -> torch.Tensor: | |
| """raw id → 类别下标(uint8,同形状)。只给测试 / 可视化用;前向不走这条(见 pooled_onehot)。""" | |
| ids = sem_ids.long() | |
| if ids.min() < 0 or ids.max() > 255: | |
| raise ValueError(f"sem_ids 超出 uint8 范围:[{int(ids.min())}, {int(ids.max())}]") | |
| return _lut(vocab).to(sem_ids.device)[ids] | |
| def pooled_onehot(sem_ids: torch.Tensor, dtype=torch.float32, vocab: str = DEFAULT_VOCAB) -> torch.Tensor: | |
| """sem_ids [b, T, H, W](uint8 或整数)→ 逐类 4×4 平均池化 one-hot [b, K, T, H/4, W/4](dtype)。 | |
| 一类一类算:每次只有一张 [b·T, 1, H, W] 的 0/1 图(fp32 ≈ 128 MB/样本,用完即弃), | |
| 从不物化 [b, K, T, H, W] 的全分辨率 one-hot。""" | |
| assert sem_ids.dim() == 4, f"sem_ids 应为 [b, T, H, W],得到 {tuple(sem_ids.shape)}" | |
| if sem_ids.dtype.is_floating_point or sem_ids.dtype == torch.bool: | |
| raise TypeError(f"sem_ids 必须是整数 id(uint8 / long),得到 {sem_ids.dtype}") | |
| b, T, H, W = sem_ids.shape | |
| assert H % POOL == 0 and W % POOL == 0, (H, W) | |
| flat = sem_ids.reshape(b * T, 1, H, W) | |
| raw_to_index, num_classes = vocab_spec(vocab) | |
| raw_of = {k: raw for raw, k in raw_to_index.items()} | |
| planes = [] | |
| for k in range(num_classes - 1): | |
| m = (flat == raw_of[k]).to(torch.float32) | |
| planes.append(F.avg_pool2d(m, POOL).view(b, T, H // POOL, W // POOL)) | |
| del m | |
| # 最后一类("other",ids 词表是第 9 类)= 1 − 其余之和(池化值是 1/16 的整数倍,fp32 精确,见文件头) | |
| other = 1.0 - torch.stack(planes, 0).sum(0) | |
| planes.append(other) | |
| out = torch.stack(planes, 1) # [b, K, T, H/4, W/4] | |
| return out.to(dtype) | |
| def group_and_unshuffle(pooled: torch.Tensor) -> torch.Tensor: | |
| """[b, K, T=4f−3, h4, w4] → 帧 0 重复 4 次后按 4 帧分组 → pixel-unshuffle ×4 → [b, K·4·16, f, h4/4, w4/4]。""" | |
| b, K, T, h4, w4 = pooled.shape | |
| assert (T - 1) % T_GROUP == 0, f"帧数应为 4k+1,得到 {T}" | |
| x = torch.cat([pooled[:, :, :1].expand(b, K, T_GROUP - 1, h4, w4), pooled], dim=2) # [b, K, 4f, h4, w4] | |
| return rearrange(x, "b k (f t) (h p) (w q) -> b (k t p q) f h w", t=T_GROUP, p=UNSHUFFLE, q=UNSHUFFLE) | |
| class SemCondEncoder(nn.Module): | |
| """语义 id 视频 → patch-embedding 空间的加性条件。见文件头。""" | |
| def __init__(self, dim: int, patch_size=(1, 2, 2), hidden: int = HIDDEN, vocab: str = DEFAULT_VOCAB): | |
| super().__init__() | |
| _, num_classes = vocab_spec(vocab) | |
| self.vocab = vocab | |
| self.num_classes = num_classes | |
| self.in_channels = num_classes * T_GROUP * UNSHUFFLE * UNSHUFFLE # ids 640 / parts 1280 | |
| self.dim = dim | |
| self.proj_in = nn.Conv3d(self.in_channels, hidden, kernel_size=1) | |
| self.conv_mid = nn.Conv3d(hidden, hidden, kernel_size=(1, 3, 3), padding=(0, 1, 1)) | |
| self.proj_out = nn.Conv3d(hidden, dim, kernel_size=tuple(patch_size), stride=tuple(patch_size)) | |
| nn.init.zeros_(self.proj_out.weight) | |
| nn.init.zeros_(self.proj_out.bias) | |
| def features(self, sem_ids: torch.Tensor) -> torch.Tensor: | |
| """sem_ids [b, 81, H, W] → 卷积前的输入 [b, 64K, 21, H/16, W/16](模块 dtype;832×480:[b, 640, 21, 30, 52])。""" | |
| dtype = self.proj_in.weight.dtype | |
| x = group_and_unshuffle(pooled_onehot(sem_ids.to(self.proj_in.weight.device), torch.float32, self.vocab)) | |
| return x.to(dtype) | |
| def forward(self, sem_ids: torch.Tensor) -> torch.Tensor: | |
| x = self.features(sem_ids) | |
| assert x.shape[1] == self.in_channels, (x.shape, self.in_channels) | |
| x = F.silu(self.proj_in(x)) | |
| x = F.silu(self.conv_mid(x)) | |
| return self.proj_out(x) | |
| def forward_pooled(self, pooled: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: | |
| """pooled one-hot [b, K, T, H/4, W/4] (fp32) → (patch-embedding term [b, dim, f, h, w], trunk features | |
| [b, 512, f, 2h, 2w]). Same numbers as forward(); the trunk features feed the v2 deep adapter.""" | |
| dtype = self.proj_in.weight.dtype | |
| x = group_and_unshuffle(pooled).to(dtype) | |
| x = F.silu(self.proj_in(x)) | |
| h = F.silu(self.conv_mid(x)) | |
| return self.proj_out(h), h | |
| def is_zero_output(self) -> bool: | |
| """最后一层权重与偏置全 0(刚装上 / 从无此模块的 ckpt 热启动)⇒ 输出恒为 0。""" | |
| return bool((self.proj_out.weight == 0).all() and (self.proj_out.bias == 0).all()) | |
| # ---------------------------------------------------------------------------- | |
| # 白模条件(cond_type="whitebox",2026-10-12) | |
| # ---------------------------------------------------------------------------- | |
| # GameCrafter-Parkour 的控制视频是 pk 渲染的白模:只有方块 / 圆柱 / 胶囊、固定光照(亮度 ∝ 面朝向)、黑天空、1–3 px 的 | |
| # 暗轮廓线(深度 / 法线突变处;窗框、窗台、障碍物顶面与屋顶的分界只靠它)。数据里是灰度(R=G=B),prep 存 uint8 | |
| # [81,480,832] 并把亮度截到 ≤254,255 留作 UNKNOWN(整段条件 dropout / CFG 负支 = 全 255,与语义 id 同一约定)。 | |
| # | |
| # 编码器与 SemCondEncoder 同构,只换"逐帧 4×4 块特征"这一步(K=10 的池化 one-hot → WB_FEATURES=5 个通道): | |
| # 0..2 R, G, B 块均值(x/127.5 − 1);3 亮度块内标准差(轮廓线在 4×4 均值里几乎消失,在标准差里是强信号); | |
| # 4 已知像素比例(全 255 ⇒ 0;没有它,"未知"会与均值 ≈ 0 的中灰立面混在一起)。 | |
| # 均值 / 标准差只在已知像素上统计。之后帧 0 重复 4 次 + 4 帧分组 + unshuffle ×4(group_and_unshuffle,对通道数通用) | |
| # → [b, 5·4·16 = 320, 21, 30, 52] → 1×1 → SiLU → 1×3×3 → SiLU → 零初始化 Conv3d(512, dim, (1,2,2), stride (1,2,2))。 | |
| # 时间方向的核全是 1 ⇒ cell j 的条件只依赖 cell j 自己的帧(逐 latent cell 因果,可蒸馏成因果学生, | |
| # tests/test_wb_cond.py 验证)。输入也接受 RGB uint8 [b,T,H,W,3](三通道都是 255 才算 UNKNOWN),给以后的彩色控制视频用。 | |
| WB_COND_NAME = "gc_wb_cond" | |
| WB_COND_INIT_SEED = 20261012 | |
| WB_FEATURES = 5 | |
| WB_FEATURE_NAMES = ("mean_r", "mean_g", "mean_b", "luma_std", "known_frac") | |
| LUMA_WEIGHTS = (0.299, 0.587, 0.114) | |
| COND_TYPES = ("ids", "whitebox") | |
| COND_MODULE = {"ids": SEM_COND_NAME, "whitebox": WB_COND_NAME} | |
| def wb_pooled_features(wb: torch.Tensor, dtype=torch.float32) -> torch.Tensor: | |
| """白模视频 uint8 [b, T, H, W](灰度)或 [b, T, H, W, 3](RGB)→ 逐帧 4×4 块特征 [b, 5, T, H/4, W/4](dtype)。 | |
| 通道见上(WB_FEATURE_NAMES)。255 = UNKNOWN:灰度看该像素,RGB 看三通道是否都是 255。块内一个已知像素都没有时 | |
| 前 4 个通道为 0、第 5 个为 0。逐帧独立计算(不跨帧),fp32。""" | |
| if wb.dtype != torch.uint8: | |
| raise TypeError(f"白模必须是 uint8 像素值(255 = UNKNOWN),得到 {wb.dtype}") | |
| if wb.dim() == 4: | |
| rgb = False | |
| elif wb.dim() == 5 and wb.shape[-1] == 3: | |
| rgb = True | |
| else: | |
| raise AssertionError(f"白模应为 [b, T, H, W] 或 [b, T, H, W, 3],得到 {tuple(wb.shape)}") | |
| b, T, H, W = wb.shape[:4] | |
| assert H % POOL == 0 and W % POOL == 0, (H, W) | |
| h, w = H // POOL, W // POOL | |
| if rgb: | |
| x = wb.reshape(b * T, H, W, 3).permute(0, 3, 1, 2) # [bT, 3, H, W] | |
| known = (x != UNKNOWN_ID).all(1, keepdim=True) | |
| v = x.float().div_(127.5).sub_(1.0).mul_(known) | |
| luma = sum(c * v[:, i:i + 1] for i, c in enumerate(LUMA_WEIGHTS)) | |
| else: | |
| x = wb.reshape(b * T, 1, H, W) | |
| known = x != UNKNOWN_ID | |
| v = x.float().div_(127.5).sub_(1.0).mul_(known) | |
| luma = v | |
| kf = F.avg_pool2d(known.float(), POOL) # [bT, 1, h, w],1/16 的整数倍 | |
| denom = kf.clamp_min(1.0 / (POOL * POOL)) | |
| mean = F.avg_pool2d(v, POOL) / denom # [bT, 1 或 3, h, w] | |
| lmean = mean if not rgb else F.avg_pool2d(luma, POOL) / denom | |
| dev = (luma - lmean.repeat_interleave(POOL, 2).repeat_interleave(POOL, 3)).mul_(known) | |
| std = (F.avg_pool2d(dev.square_(), POOL) / denom).sqrt_() # 两遍法:常数块的 std 恰为 0 | |
| if not rgb: | |
| mean = mean.expand(-1, 3, -1, -1) | |
| out = torch.cat([mean, std, kf], 1) # [bT, 5, h, w] | |
| return out.view(b, T, WB_FEATURES, h, w).transpose(1, 2).to(dtype) | |
| class WhiteboxCondEncoder(nn.Module): | |
| """白模视频 → patch-embedding 空间的加性条件(与 SemCondEncoder 同构、同层名,只换输入特征,见上)。""" | |
| def __init__(self, dim: int, patch_size=(1, 2, 2), hidden: int = HIDDEN): | |
| super().__init__() | |
| self.n_features = WB_FEATURES | |
| self.in_channels = WB_FEATURES * T_GROUP * UNSHUFFLE * UNSHUFFLE # 320 | |
| self.dim = dim | |
| self.proj_in = nn.Conv3d(self.in_channels, hidden, kernel_size=1) | |
| self.conv_mid = nn.Conv3d(hidden, hidden, kernel_size=(1, 3, 3), padding=(0, 1, 1)) | |
| self.proj_out = nn.Conv3d(hidden, dim, kernel_size=tuple(patch_size), stride=tuple(patch_size)) | |
| nn.init.zeros_(self.proj_out.weight) | |
| nn.init.zeros_(self.proj_out.bias) | |
| def features(self, wb: torch.Tensor) -> torch.Tensor: | |
| """白模 [b, 81, H, W(, 3)] uint8 → 卷积前的输入 [b, 320, 21, H/16, W/16](模块 dtype)。""" | |
| x = group_and_unshuffle(wb_pooled_features(wb.to(self.proj_in.weight.device), torch.float32)) | |
| return x.to(self.proj_in.weight.dtype) | |
| def forward(self, wb: torch.Tensor) -> torch.Tensor: | |
| x = self.features(wb) | |
| assert x.shape[1] == self.in_channels, (x.shape, self.in_channels) | |
| x = F.silu(self.proj_in(x)) | |
| x = F.silu(self.conv_mid(x)) | |
| return self.proj_out(x) | |
| def forward_pooled(self, pooled: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]: | |
| """wb_pooled_features 的输出 [b, 5, T, H/4, W/4] → (patch-embedding 项, trunk)。与 forward() 同一组数。""" | |
| x = group_and_unshuffle(pooled).to(self.proj_in.weight.dtype) | |
| x = F.silu(self.proj_in(x)) | |
| h = F.silu(self.conv_mid(x)) | |
| return self.proj_out(h), h | |
| def is_zero_output(self) -> bool: | |
| return bool((self.proj_out.weight == 0).all() and (self.proj_out.bias == 0).all()) | |
| # ---------------------------------------------------------------------------- | |
| # v2 architecture (2026-10-10): deep semantic injection + semantic attention bias. Both zero-initialised ⇒ a model with | |
| # them freshly installed computes exactly what v1 computes (warm start from any v1 export). | |
| # ---------------------------------------------------------------------------- | |
| ADAPTER_NAME = "adapter" # dit.gc_sem_cond.adapter | |
| ATTN_GAIN_NAME = "attn_qk" # dit.gc_sem_cond.attn_qk [n_blocks, 2, K, dim] | |
| ADAPTER_HIDDEN = 1024 | |
| class SemAdapter(nn.Module): | |
| """Deep injection (VACE / ControlNet-style, but light): the encoder's trunk features are patchified to the token grid, | |
| mixed by a per-cell 3×3 conv, and added to the residual stream before each block in `layers` through a per-layer | |
| zero-init Linear. In v1 the ids enter only once at the patch embedding (sem term ≈ 12 % of its RMS) and the | |
| cond / no-cond val gap stays tiny, i.e. the network barely relies on the map deeper in.""" | |
| def __init__(self, dim: int, layers, hidden_in: int = HIDDEN, hidden: int = ADAPTER_HIDDEN): | |
| super().__init__() | |
| self.patch = nn.Conv3d(hidden_in, hidden, kernel_size=(1, 2, 2), stride=(1, 2, 2)) | |
| # spatial only: every latent cell sees only its own ids ⇒ the condition stays causal / chunk-local, so the model | |
| # can be distilled to an autoregressive (chunked, KV-cached) student where future ids are not known yet | |
| self.mix = nn.Conv3d(hidden, hidden, kernel_size=(1, 3, 3), padding=(0, 1, 1)) | |
| self.inj = nn.ModuleDict({str(int(i)): nn.Linear(hidden, dim) for i in layers}) | |
| for lin in self.inj.values(): | |
| nn.init.zeros_(lin.weight) | |
| nn.init.zeros_(lin.bias) | |
| def layers(self) -> list[int]: | |
| return sorted(int(k) for k in self.inj.keys()) | |
| def forward(self, h: torch.Tensor) -> torch.Tensor: | |
| """trunk [b, 512, f, H/16, W/16] → token features [b, f·(H/32)·(W/32), hidden](832×480:f·15·26).""" | |
| t = F.silu(self.patch(h)) | |
| t = F.silu(self.mix(t)) | |
| t = rearrange(t, "b c f h w -> b (f h w) c") | |
| return F.layer_norm(t, (t.shape[-1],)) # unit-RMS tokens: the zero-init injections become effective quickly | |
| def is_zero_output(self) -> bool: | |
| return all(bool((l.weight == 0).all() and (l.bias == 0).all()) for l in self.inj.values()) | |
| def token_class_dist(pooled: torch.Tensor) -> torch.Tensor: | |
| """pooled one-hot [b, K, 4f−3, H/4, W/4] → per-token class fractions [b, f·(H/32)·(W/32), K] (fp32), the | |
| 'other / UNKNOWN' class zeroed (unknown tokens neither give nor get the same-class bonus).""" | |
| b, K, T, h4, w4 = pooled.shape | |
| x = torch.cat([pooled[:, :, :1].expand(b, K, T_GROUP - 1, h4, w4), pooled], dim=2) # [b, K, 4f, h4, w4] | |
| f = x.shape[2] // T_GROUP | |
| x = x.view(b, K, f, T_GROUP, h4, w4).mean(3) # [b, K, f, h4, w4] | |
| tok = UNSHUFFLE * 2 # a DiT token = 2×2 latent px = 8×8 pooled px = 32×32 image px | |
| x = F.avg_pool2d(x.reshape(b, K * f, h4, w4), tok) | |
| x = x.view(b, K, f, h4 // tok, w4 // tok).clone() | |
| x[:, K - 1] = 0 | |
| return rearrange(x, "b k f h w -> b (f h w) k") | |
| def _self_attn(sa, x, freqs, p_tok=None, e_qk=None): | |
| """WanSelfAttention.forward; with (p_tok [b, L, K], e_qk [2, K, dim]) learned class embeddings are added to q and k | |
| after RoPE (q += p·E_q, k += p·E_k). Δq·Δk is a learnable class-pair attention bias (per head a rank-≤128 K×K | |
| matrix), q·Δk lets content look for a class. Head dim stays 128 ⇒ same SDPA kernel as v1; E = 0 ⇒ bit-identical.""" | |
| if e_qk is None: | |
| return sa(x, freqs) | |
| n = sa.num_heads | |
| q = rope_apply(sa.norm_q(sa.q(x)), freqs, n) | |
| k = rope_apply(sa.norm_k(sa.k(x)), freqs, n) | |
| p = p_tok.to(q.dtype) | |
| q = q + p @ e_qk[0].to(q.dtype) | |
| k = k + p @ e_qk[1].to(k.dtype) | |
| return sa.o(sa.attn(q, k, sa.v(x))) | |
| def gc_block_forward(block, x, context, t_mod, freqs, p_tok=None, e_qk=None, *, f: int): | |
| """actionrope.arope._block_forward with arm=None (DiTBlock.forward + per-cell text cross-attn), self-attention | |
| through _self_attn. e_qk=None ⇒ identical to _block_forward.""" | |
| has_seq = len(t_mod.shape) == 4 | |
| chunk_dim = 2 if has_seq else 1 | |
| shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = ( | |
| block.modulation.to(dtype=t_mod.dtype, device=t_mod.device) + t_mod).chunk(6, dim=chunk_dim) | |
| if has_seq: | |
| shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = ( | |
| shift_msa.squeeze(2), scale_msa.squeeze(2), gate_msa.squeeze(2), | |
| shift_mlp.squeeze(2), scale_mlp.squeeze(2), gate_mlp.squeeze(2), | |
| ) | |
| input_x = modulate(block.norm1(x), shift_msa, scale_msa) | |
| x = block.gate(x, gate_msa, _self_attn(block.self_attn, input_x, freqs, p_tok, e_qk)) | |
| q_in = block.norm3(x) | |
| if context.shape[0] == x.shape[0]: | |
| x = x + block.cross_attn(q_in, context) | |
| else: | |
| b = x.shape[0] | |
| assert context.shape[0] == b * f, f"逐 cell context 应有 b*f={b * f} 行,得到 {context.shape[0]}" | |
| q_in = rearrange(q_in, "b (f n) c -> (b f) n c", f=f) | |
| y = block.cross_attn(q_in, context) | |
| x = x + rearrange(y, "(b f) n c -> b (f n) c", f=f) | |
| input_x = modulate(block.norm2(x), shift_mlp, scale_mlp) | |
| x = block.gate(x, gate_mlp, block.ffn(input_x)) | |
| return x | |
| def install_v2(dit: WanModel, adapter_every: int = 0, sem_attn: bool = False) -> WanModel: | |
| """Add the zero-init v2 modules under dit.gc_sem_cond (idempotent). adapter_every=k ⇒ inject before blocks 0, k, 2k…""" | |
| if not hasattr(dit, SEM_COND_NAME): | |
| install_sem_cond(dit) | |
| enc = getattr(dit, SEM_COND_NAME) | |
| ref = dit.patch_embedding.weight | |
| with torch.random.fork_rng(devices=[]): | |
| torch.manual_seed(SEM_COND_INIT_SEED + 1) | |
| if adapter_every and not hasattr(enc, ADAPTER_NAME): | |
| ad = SemAdapter(dit.dim, range(0, len(dit.blocks), int(adapter_every))) | |
| enc.add_module(ADAPTER_NAME, ad.to(device=ref.device, dtype=ref.dtype)) | |
| if sem_attn and not hasattr(enc, ATTN_GAIN_NAME): | |
| enc.register_parameter(ATTN_GAIN_NAME, nn.Parameter( | |
| torch.zeros(len(dit.blocks), 2, enc.num_classes, dit.dim, device=ref.device, dtype=ref.dtype))) | |
| return dit | |
| def v2_config_from_keys(keys) -> dict: | |
| """Which v2 modules a checkpoint carries (from its key names).""" | |
| pre = SEM_COND_NAME + "." | |
| inj = sorted({int(k.split(".")[3]) for k in keys if k.startswith(f"{pre}{ADAPTER_NAME}.inj.")}) | |
| every = (inj[1] - inj[0]) if len(inj) > 1 else (1 if inj else 0) | |
| return {"adapter_every": every, "sem_attn": f"{pre}{ATTN_GAIN_NAME}" in set(keys)} | |
| # ---------------------------------------------------------------------------- | |
| # 加载 / 安装 | |
| # ---------------------------------------------------------------------------- | |
| def load_dit(model_dir: str, device: str = "cpu") -> WanModel: | |
| """只加载 DiT(bf16),与 actionrope/train.py::load_dit 同一条路径(逐行照抄,免得 import actionrope.train 拖进 | |
| baseline / dataset;maliang 不带 actionrope/train.py)。""" | |
| from diffsynth.pipelines.wan_video import ModelConfig, WanVideoPipeline | |
| files = [os.path.join(model_dir, f"diffusion_pytorch_model-0000{i}-of-00003.safetensors") for i in (1, 2, 3)] | |
| pipe = WanVideoPipeline.from_pretrained( | |
| torch_dtype=torch.bfloat16, device=device, model_configs=[ModelConfig(path=files)], | |
| tokenizer_config=None, redirect_common_files=False, | |
| ) | |
| return pipe.dit | |
| def install_sem_cond(dit: WanModel, vocab: str = DEFAULT_VOCAB) -> WanModel: | |
| """给 DiT 装零初始化的 `dit.gc_sem_cond`(幂等:已装则原样返回,不重置权重;已装的词表与 vocab 不同则报错)。""" | |
| if hasattr(dit, SEM_COND_NAME): | |
| enc = getattr(dit, SEM_COND_NAME) | |
| assert isinstance(enc, SemCondEncoder), f"dit.{SEM_COND_NAME} 不是 SemCondEncoder:{type(enc)}" | |
| if enc.vocab != vocab: | |
| raise ValueError(f"dit.{SEM_COND_NAME} 已按词表 {enc.vocab!r} 安装,要求的是 {vocab!r}") | |
| return dit | |
| ref = dit.patch_embedding.weight | |
| # 固定种子建模块:不消耗、也不依赖全局 RNG(各 rank 一致,见文件头) | |
| with torch.random.fork_rng(devices=[]): | |
| torch.manual_seed(SEM_COND_INIT_SEED) | |
| enc = SemCondEncoder(dit.dim, patch_size=tuple(dit.patch_size), vocab=vocab) | |
| enc.to(device=ref.device, dtype=ref.dtype) | |
| dit.add_module(SEM_COND_NAME, enc) | |
| return dit | |
| def install_wb_cond(dit: WanModel) -> WanModel: | |
| """给 DiT 装零初始化的 `dit.gc_wb_cond`(白模条件;幂等)。与 gc_sem_cond 互斥(双条件以后再说)。""" | |
| if hasattr(dit, SEM_COND_NAME): | |
| raise ValueError(f"dit 上已有 {SEM_COND_NAME}(语义 id 条件);白模模型只装 {WB_COND_NAME}") | |
| if hasattr(dit, WB_COND_NAME): | |
| enc = getattr(dit, WB_COND_NAME) | |
| assert isinstance(enc, WhiteboxCondEncoder), f"dit.{WB_COND_NAME} 不是 WhiteboxCondEncoder:{type(enc)}" | |
| return dit | |
| ref = dit.patch_embedding.weight | |
| with torch.random.fork_rng(devices=[]): | |
| torch.manual_seed(WB_COND_INIT_SEED) | |
| enc = WhiteboxCondEncoder(dit.dim, patch_size=tuple(dit.patch_size)) | |
| enc.to(device=ref.device, dtype=ref.dtype) | |
| dit.add_module(WB_COND_NAME, enc) | |
| return dit | |
| def install_cond(dit: WanModel, cond_type: str = "ids") -> WanModel: | |
| """按 cond_type 装条件模块:ids ⇒ install_sem_cond,whitebox ⇒ install_wb_cond。""" | |
| if cond_type not in COND_TYPES: | |
| raise ValueError(f"cond_type 只能是 {COND_TYPES},得到 {cond_type!r}") | |
| return install_wb_cond(dit) if cond_type == "whitebox" else install_sem_cond(dit) | |
| def resolve_cond_type(dit, cond_type: str | None = None) -> str: | |
| """显式给了就用(并核对合法);None ⇒ 看 dit 上装了哪个:只有 gc_wb_cond ⇒ "whitebox",否则 "ids"(旧行为)。""" | |
| if cond_type is not None: | |
| if cond_type not in COND_TYPES: | |
| raise ValueError(f"cond_type 只能是 {COND_TYPES},得到 {cond_type!r}") | |
| return cond_type | |
| has_sem, has_wb = hasattr(dit, SEM_COND_NAME), hasattr(dit, WB_COND_NAME) | |
| if has_sem and has_wb: | |
| raise ValueError(f"dit 上同时有 {SEM_COND_NAME} 和 {WB_COND_NAME},请显式传 cond_type") | |
| return "whitebox" if has_wb else "ids" | |
| def sem_vocab(dit) -> str: | |
| """dit 上已装的 gc_sem_cond 的词表。""" | |
| return getattr(dit, SEM_COND_NAME).vocab | |
| def vocab_from_state(sd_or_header) -> str | None: | |
| """state dict / safetensors 头(键 → 张量或 {'shape': ...})→ gc_sem_cond 的词表;没有 gc_sem_cond 键 ⇒ None。""" | |
| for k in (f"{SEM_COND_NAME}.proj_in.weight", f"dit.{SEM_COND_NAME}.proj_in.weight"): | |
| if k in sd_or_header: | |
| v = sd_or_header[k] | |
| shape = v["shape"] if isinstance(v, dict) else tuple(v.shape) | |
| return vocab_from_in_channels(int(shape[1])) | |
| return None | |
| def cond_type_from_keys(keys) -> str: | |
| """ckpt 的键名 → 它是哪种条件的导出(有 gc_wb_cond.* / dit.gc_wb_cond.* ⇒ whitebox,否则 ids)。""" | |
| pre = (WB_COND_NAME + ".", "dit." + WB_COND_NAME + ".") | |
| return "whitebox" if any(k.startswith(pre) for k in keys) else "ids" | |
| def _read_safetensors(path) -> tuple[dict, dict]: | |
| paths = [path] if isinstance(path, (str, os.PathLike)) else list(path) | |
| sd, meta = {}, {} | |
| for p in paths: | |
| part = load_file(str(p)) | |
| dup = set(part) & set(sd) | |
| if dup: | |
| raise RuntimeError(f"{p} 与前面的分片有重复键:{sorted(dup)[:5]}") | |
| sd.update(part) | |
| with safe_open(str(p), framework="pt") as fh: | |
| meta.update(fh.metadata() or {}) | |
| return sd, meta | |
| def load_gc_weights(dit: WanModel, path, cond_type: str = "ids") -> dict: | |
| """从导出的 safetensors(或分片列表)加载 DiT 权重。先 install_sem_cond(幂等),再按 ckpt 里出现的键装上 v2 模块 | |
| (gc_sem_cond.adapter.* / gc_sem_cond.attn_gain)。 | |
| 严格匹配 dit.state_dict() 的键集;放行的缺失只有两类,且对应模块必须仍是零输出(刚装上): | |
| * 全部 `gc_sem_cond.*`(从普通 Wan / ActionRoPE plain 导出热启动); | |
| * v2 模块(调用方先 install_v2 再从 v1 导出热启动)。 | |
| 返回 info:missing / n_tensors / has_sem_cond / v2 / metadata。 | |
| cond_type="whitebox" ⇒ 走 _load_wb_weights(gc_wb_cond,见那里);默认 "ids" 的行为与原来完全相同。""" | |
| if cond_type != "ids": | |
| if cond_type != "whitebox": | |
| raise ValueError(f"cond_type 只能是 {COND_TYPES},得到 {cond_type!r}") | |
| return _load_wb_weights(dit, path) | |
| sd, meta = _read_safetensors(path) | |
| # 词表:ckpt 里有 gc_sem_cond ⇒ 按它的输入通道定;没有(Wan / plain 热启动)⇒ 用 dit 上已装的,未装则默认 ids | |
| ck_vocab = vocab_from_state(sd) | |
| have = sem_vocab(dit) if hasattr(dit, SEM_COND_NAME) else None | |
| if ck_vocab and have and ck_vocab != have: | |
| raise RuntimeError(f"{path} 的 gc_sem_cond 是 {ck_vocab!r} 词表,dit 上装的是 {have!r}") | |
| install_sem_cond(dit, ck_vocab or have or DEFAULT_VOCAB) | |
| v2 = v2_config_from_keys(sd.keys()) | |
| install_v2(dit, **v2) | |
| missing, unexpected = dit.load_state_dict(sd, strict=False) | |
| prefix = SEM_COND_NAME + "." | |
| v2_prefixes = (f"{prefix}{ADAPTER_NAME}.", f"{prefix}{ATTN_GAIN_NAME}") | |
| bad_missing = [k for k in missing if not k.startswith(prefix)] | |
| if bad_missing or unexpected: | |
| raise RuntimeError(f"{path} 与模型键集不符:缺 {bad_missing[:5]}…({len(bad_missing)})," | |
| f"多 {list(unexpected)[:5]}…({len(unexpected)})") | |
| enc = getattr(dit, SEM_COND_NAME) | |
| sem_missing = [k for k in missing if k.startswith(prefix) and not k.startswith(v2_prefixes)] | |
| v2_missing = [k for k in missing if k.startswith(v2_prefixes)] | |
| n_sem_total = sum(1 for k in dit.state_dict() if k.startswith(prefix) and not k.startswith(v2_prefixes)) | |
| if sem_missing and len(sem_missing) != n_sem_total: | |
| raise RuntimeError(f"{path} 只含部分 {prefix}* 键(缺 {sem_missing}),ckpt 不完整") | |
| if sem_missing and not enc.is_zero_output(): | |
| raise RuntimeError(f"{path} 没有 {prefix}* 键,但 dit 上的 {SEM_COND_NAME} 已不是零初始化状态;" | |
| f"请在全新 install 的模型上热启动") | |
| if v2_missing: | |
| ad, g = getattr(enc, ADAPTER_NAME, None), getattr(enc, ATTN_GAIN_NAME, None) | |
| if (ad is not None and not ad.is_zero_output()) or (g is not None and bool((g != 0).any())): | |
| raise RuntimeError(f"{path} 缺 v2 键 {v2_missing[:3]}…,但 v2 模块已不是零初始化状态") | |
| return {"missing": list(missing), "n_tensors": len(sd), "has_sem_cond": not sem_missing, "v2": v2, "vocab": enc.vocab, | |
| "v2_missing": len(v2_missing), "metadata": meta, "path": str(path)} | |
| def _load_wb_weights(dit: WanModel, path) -> dict: | |
| """白模模型(dit.gc_wb_cond)的加载:install_wb_cond(幂等)后严格匹配键集。 | |
| * ckpt 里的 `gc_sem_cond.*`(含 v2 子模块)一律丢弃并记进 info["dropped_sem_keys"] —— 从语义 id 模型 | |
| (如 gc25d_v3v4/step-6000,825 个 Wan 键 + 6 个 gc_sem_cond 键)热启动时只取 825 个 DiT 键; | |
| * 唯一放行的缺失:6 个 `gc_wb_cond.*` 全缺(热启动),且模块仍是零输出(刚装上);只缺一部分 ⇒ 报错; | |
| * 缺任何 DiT 键 / 有其它多余键 ⇒ 报错;同时带 gc_sem_cond.* 与 gc_wb_cond.* 的 ckpt ⇒ 报错。""" | |
| install_wb_cond(dit) | |
| sd, meta = _read_safetensors(path) | |
| n_file = len(sd) | |
| sem_prefix, prefix = SEM_COND_NAME + ".", WB_COND_NAME + "." | |
| dropped = sorted(k for k in sd if k.startswith(sem_prefix)) | |
| has_wb = any(k.startswith(prefix) for k in sd) | |
| if dropped and has_wb: | |
| raise RuntimeError(f"{path} 同时有 {sem_prefix}* 和 {prefix}* 键,不知道该用哪个") | |
| for k in dropped: | |
| del sd[k] | |
| missing, unexpected = dit.load_state_dict(sd, strict=False) | |
| bad_missing = [k for k in missing if not k.startswith(prefix)] | |
| if bad_missing or unexpected: | |
| raise RuntimeError(f"{path} 与白模模型键集不符:缺 {bad_missing[:5]}…({len(bad_missing)})," | |
| f"多 {list(unexpected)[:5]}…({len(unexpected)})") | |
| enc = getattr(dit, WB_COND_NAME) | |
| wb_missing = [k for k in missing if k.startswith(prefix)] | |
| n_wb_total = sum(1 for k in dit.state_dict() if k.startswith(prefix)) | |
| if wb_missing and len(wb_missing) != n_wb_total: | |
| raise RuntimeError(f"{path} 只含部分 {prefix}* 键(缺 {wb_missing}),ckpt 不完整") | |
| if wb_missing and not enc.is_zero_output(): | |
| raise RuntimeError(f"{path} 没有 {prefix}* 键,但 dit 上的 {WB_COND_NAME} 已不是零初始化状态;" | |
| f"请在全新 install 的模型上热启动") | |
| return {"missing": list(missing), "n_tensors": n_file, "n_loaded": len(sd), "cond_type": "whitebox", | |
| "has_wb_cond": not wb_missing, "has_sem_cond": False, "dropped_sem_keys": dropped, | |
| "v2": {"adapter_every": 0, "sem_attn": False}, "v2_missing": 0, "metadata": meta, "path": str(path)} | |
| # ---------------------------------------------------------------------------- | |
| # 前向 | |
| # ---------------------------------------------------------------------------- | |
| def gc_forward( | |
| dit: WanModel, | |
| latents: torch.Tensor, | |
| timestep: torch.Tensor, | |
| context: torch.Tensor, | |
| sem_ids, | |
| *, | |
| first_frame_cond: bool = True, | |
| context_ids=None, | |
| use_gradient_checkpointing: bool = False, | |
| use_gradient_checkpointing_offload: bool = False, | |
| aux: dict | None = None, | |
| cond_type: str | None = None, | |
| ) -> torch.Tensor: | |
| """gc25d 的 DiT 前向。latents [b, 48, f, H/16, W/16] → v_pred 同形(832×480:[b, 48, f, 30, 52])。 | |
| cond_type:"ids"(gc_sem_cond,语义 id)/ "whitebox"(gc_wb_cond,sem_ids 参数里放白模 uint8 [b, 4f−3, H, W(, 3)]); | |
| None ⇒ resolve_cond_type(只装了 gc_wb_cond ⇒ whitebox,否则 ids —— 不传时与原来逐位相同)。 | |
| = arope_forward(offset_px=None, mask_input=None, arm=None) 的逐行照抄 + patch embedding 后的 sem 条件项。 | |
| sem_ids: [b, 4f−3, 16·lat_h, 16·lat_w] uint8 / 整数 raw id(255 = UNKNOWN);None ⇒ 不加 sem 项(≡ 原模型)。 | |
| 给了但 dit 没装 gc_sem_cond ⇒ 报错(避免"以为用了条件其实没用")。 | |
| context: [b, 512, 4096](整条)或 [b, f, 512, 4096](逐 cell);context_ids [b, f] 用于逐 cell 去重。 | |
| timestep: 标量 / [1] / [b](1000 制);first_frame_cond ⇒ 帧 0 的 t=0。 | |
| aux: 给一个 dict ⇒ 填入诊断量(sem 项与 patch embedding 的 RMS),不影响计算。 | |
| """ | |
| b, _, f, lat_h, lat_w = latents.shape | |
| p_f, p_h, p_w = dit.patch_size | |
| n_tok_per_frame = (lat_h // p_h) * (lat_w // p_w) | |
| # 时间步:逐 token,帧 0 为 0(照抄 arope_forward / model_fn_wan_video) | |
| t_tok = _per_token_timestep(latents, timestep, n_tok_per_frame, first_frame_cond) | |
| t = dit.time_embedding(sinusoidal_embedding_1d(dit.freq_dim, t_tok.reshape(-1)).view(b, t_tok.shape[1], -1)) | |
| t_mod = dit.time_projection(t).unflatten(2, (6, dit.dim)) | |
| # 文本(逐 cell 时只对唯一串算一次 text_embedding) | |
| context = _embed_context(dit, context, context_ids) | |
| # patch embedding + 语义条件 | |
| x = dit.patch_embedding(latents) | |
| adapter = attn_gain = tok = p_tok = None | |
| if sem_ids is not None and resolve_cond_type(dit, cond_type) == "whitebox": | |
| enc = getattr(dit, WB_COND_NAME, None) | |
| if enc is None: | |
| raise ValueError(f"cond_type=whitebox 但 dit 没有 {WB_COND_NAME},请先 install_wb_cond(dit) / " | |
| f"load_gc_weights(..., cond_type='whitebox')") | |
| exp = (b, T_GROUP * (f - 1) + 1, lat_h * POOL * UNSHUFFLE, lat_w * POOL * UNSHUFFLE) | |
| assert tuple(sem_ids.shape[:4]) == exp and sem_ids.dim() in (4, 5), \ | |
| f"白模形状应为 {exp}(灰度)或 {exp + (3,)}(RGB),得到 {tuple(sem_ids.shape)}" | |
| s, _ = enc.forward_pooled(wb_pooled_features(sem_ids.to(enc.proj_in.weight.device), torch.float32)) | |
| assert s.shape == x.shape, f"白模项 {tuple(s.shape)} ≠ patch embedding {tuple(x.shape)}" | |
| if aux is not None: | |
| aux["sem_rms"] = s.detach().float().pow(2).mean().sqrt() | |
| aux["x_rms"] = x.detach().float().pow(2).mean().sqrt() | |
| x = x + s.to(x.dtype) | |
| elif sem_ids is not None: | |
| enc = getattr(dit, SEM_COND_NAME, None) | |
| if enc is None: | |
| raise ValueError(f"给了 sem_ids 但 dit 没有 {SEM_COND_NAME},请先 install_sem_cond(dit) / load_gc_weights") | |
| exp = (b, T_GROUP * (f - 1) + 1, lat_h * POOL * UNSHUFFLE, lat_w * POOL * UNSHUFFLE) | |
| assert tuple(sem_ids.shape) == exp, f"sem_ids 形状应为 {exp},得到 {tuple(sem_ids.shape)}" | |
| adapter = getattr(enc, ADAPTER_NAME, None) | |
| attn_gain = getattr(enc, ATTN_GAIN_NAME, None) | |
| pooled = pooled_onehot(sem_ids.to(enc.proj_in.weight.device), torch.float32, enc.vocab) | |
| s, trunk = enc.forward_pooled(pooled) | |
| assert s.shape == x.shape, f"sem 项 {tuple(s.shape)} ≠ patch embedding {tuple(x.shape)}" | |
| if aux is not None: | |
| aux["sem_rms"] = s.detach().float().pow(2).mean().sqrt() | |
| aux["x_rms"] = x.detach().float().pow(2).mean().sqrt() | |
| x = x + s.to(x.dtype) | |
| if adapter is not None: | |
| tok = adapter(trunk) # [b, L, 1024] | |
| if attn_gain is not None: | |
| p_tok = token_class_dist(pooled) # [b, L, K] fp32 | |
| del pooled | |
| f, h, w = x.shape[2:] | |
| assert f * h * w == t_tok.shape[1] | |
| x = rearrange(x, "b c f h w -> b (f h w) c").contiguous() | |
| # 普通 RoPE 查表(与 model_fn_wan_video 逐位相同) | |
| freqs = build_world_freqs(dit, f, h, w, None, device=x.device) | |
| # blocks:v1 modules only ⇒ actionrope._block_forward (bit-identical to v1); v2 ⇒ gc_block_forward + injections | |
| inj_rms = [] | |
| for i, block in enumerate(dit.blocks): | |
| if adapter is not None and str(i) in adapter.inj: | |
| r = adapter.inj[str(i)](tok) | |
| if aux is not None: | |
| inj_rms.append(r.detach().float().pow(2).mean()) | |
| x = x + r.to(x.dtype) | |
| if attn_gain is not None: | |
| x = gradient_checkpoint_forward( | |
| functools.partial(gc_block_forward, block, f=f), | |
| use_gradient_checkpointing, use_gradient_checkpointing_offload, | |
| x, context, t_mod, freqs, p_tok, attn_gain[i], | |
| ) | |
| else: | |
| x = gradient_checkpoint_forward( | |
| functools.partial(_block_forward, block, f=f, arm=None, idx=i, grid=None), | |
| use_gradient_checkpointing, | |
| use_gradient_checkpointing_offload, | |
| x, context, t_mod, freqs, None, | |
| ) | |
| if aux is not None and inj_rms: | |
| aux["inj_rms"] = torch.stack(inj_rms).mean().sqrt() | |
| if aux is not None and attn_gain is not None: | |
| aux["attn_bias_absmax"] = attn_gain.detach().float().pow(2).mean().sqrt() # RMS of the class embeddings | |
| x = dit.head(x, t) | |
| return dit.unpatchify(x, (f, h, w)) | |