File size: 41,522 Bytes
bcdd8fb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 | """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]
@torch.no_grad()
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}
@torch.no_grad()
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)
@property
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))
|