- fastvideo-kernel/wan22_vae_decode: the Wan2.2 (TI2V-5B) VAE decoder on sm_100a / sm_103a at 704x1248, built on first use against fastvideo-kernel's CUTLASS submodule. It frees intermediates early and carries 4 latent frames per pass: a 5 s video peaks at 20.3 GiB, down from 94.6 GiB in the kernel's first version, with the same output. - WanVAEConfig.decode_kernel routes AutoencoderKLWan.decode through it where it applies (bf16 with fp32 accumulation, about 7x faster than the fp32 decode on a B300, PSNR 61-63 dB, SSIM 0.998 against it); decode_kernel_parallel splits the decode over the sequence-parallel group (2, 4 or 8 GPUs) and broadcasts the video, with the ranks agreeing on whether every one of them could build the kernel. - PAN2's DiT runs fused Triton kernels for Q/K RMSNorm + RoPE and LayerNorm + modulation (pan2_fusions, set_fused_kernels). - PAN2PipelineConfig turns all three on (vae_decode_kernel, vae_decode_kernel_parallel, dit_fused_kernels). - VideoGenerator copies a decoded RGB video's frames to the host once instead of once per frame. - The PAN2 presets sample at guidance 2.5, the reference launcher's default since 2026-10-23.
429 lines
26 KiB
Python
429 lines
26 KiB
Python
"""The task: decoding Wan2.2 VAE latents into video on one, two or four GB200 GPUs of one node, as bench.py, ci.py and
|
|
bounds.py read it. It defines the clips and workloads, the weights' order, the data, the error allowance and the work
|
|
of one decode.
|
|
|
|
What is computed
|
|
----------------
|
|
reference.py (in the kernel repo) defines the result: diffusers' AutoencoderKLWan._decode of the Wan2.2-TI2V-5B VAE,
|
|
latents z [1, 48, T, 44, 78] to a video [1, 3, 4T-3, 704, 1248] in [-1, 1]: post_quant_conv, then the decoder one
|
|
latent frame at a time with its causal caches (the first latent frame yields one frame, every later one four),
|
|
unpatchify 2 x 2, clamp. The judge compares a solution with the fp32 decode of the same latents, cached as videos in
|
|
$DATA (see Correctness).
|
|
|
|
The interface (as loader.py builds it and bench.py calls it, positional, destination passing)
|
|
-------------
|
|
run(z, weights, rank, world, pg, out)
|
|
prepare(weights, rank, world, pg) -> list[Tensor] optional
|
|
|
|
- z: [1, 48, T, 44, 78] bf16, contiguous, the full latents on every rank: what a pipeline passes to
|
|
vae.decode(latents), rounded to bf16.
|
|
- weights: the 110 tensors of WEIGHTS below, bf16, on this rank's GPU; or what prepare returned.
|
|
- rank, world: this process's rank and the number of ranks (1, 2 or 4), rank r on GPU r.
|
|
- pg: torch.distributed's NCCL ProcessGroup over the ranks; a C++ entry takes it as
|
|
`const c10::intrusive_ptr<c10d::ProcessGroup>&` (from <torch/csrc/distributed/c10d/ProcessGroup.hpp>).
|
|
- out: on rank 0, the video [1, 3, 4T-3, 704, 1248] bf16, contiguous, from torch.empty and not zeroed (every element
|
|
must be written); on the other ranks an empty tensor. When run has returned on every rank, rank 0's out holds the
|
|
video.
|
|
- prepare, when the module exports it, is called once per process before any timed call, and is not timed. It sees
|
|
the weights, not the latents; its result replaces `weights` in every run.
|
|
|
|
Numerics: bf16 weights and activations with fp32 accumulation, and fp32 statistics in the RMS norms; nothing narrower
|
|
(no FP8, FP6 or FP4). This is a rule for the reviewers: no check reads it from the code.
|
|
|
|
Workloads
|
|
---------
|
|
A workload is a clip decoded at a world size, named <clip>x<world>. A clip is the first T latent frames of one of
|
|
three real 20 s clips encoded by the VAE (latents_<source>.pt in $DATA):
|
|
|
|
clip source latent frames T video frames 4T-3 worlds timed
|
|
f1 row_000168 1 1 1, 4 no
|
|
f5 row_000168 2 5 1, 4 no
|
|
f17 row_000168 5 17 1, 4 no
|
|
f121 row_000168 31 121 1, 2, 4 yes
|
|
f241 row_000129 61 241 1, 2, 4 yes
|
|
f481 row_000090 121 481 1, 2, 4 yes
|
|
|
|
The nine timed workloads (f121x1 ... f481x4) make the objective, their geomean; the six short ones (f1x1 ... f17x4)
|
|
are checked for correctness and never timed: a decoder sees every clip length, and the first latent frame is a
|
|
special case.
|
|
|
|
Correctness
|
|
-----------
|
|
On rank 0's out, against ref_<clip>.pt, the fp32 decode of the clip's latents (fp32 latents, TF32 off), stored as fp16:
|
|
every element finite and within [-1, 1]; and with d = out - ref in fp32,
|
|
|
|
max |d| <= min(MAX_ABS_CAP, ERROR_FACTOR x bf16_max_abs)
|
|
rms(d) <= ERROR_FACTOR x bf16_rms
|
|
|
|
bf16_max_abs and bf16_rms being the error of the release's bf16 decode (bf16 weights under bf16 autocast, the
|
|
latents rounded to bf16) against that fp32 decode on the same clip, from calibration.json. PSNR, 10 log10(4 / mse), is
|
|
shown beside them.
|
|
|
|
The weights
|
|
-----------
|
|
run() gets the decoder's parameters and post_quant_conv's, 110 tensors, as a list in diffusers' registration order
|
|
(that of AutoencoderKLWan.named_parameters(), post_quant_conv first, then the decoder from conv_in to conv_out;
|
|
$DATA/weight_names.json lists the same), bf16, in PyTorch's layouts: a conv3d's weight is [out, in, kt, kh, kw], a
|
|
conv2d's [out, in, kh, kw], a bias [out], an RMS norm's gain [C, 1, 1, 1] (the attention's [C, 1, 1]).
|
|
$DATA/decoder_weights_bf16.pt is that list: the release's fp32 weights rounded to bf16 to nearest even, as diffusers'
|
|
from_pretrained(torch_dtype=torch.bfloat16) casts them. The order is not the order of execution: the mid block
|
|
registers its attention before its two residual blocks, though it runs resnets.0, the attention, then resnets.1; an
|
|
upsampler registers its 3x3 conv (resample.1) before its time conv, though the time conv runs first. The attention's
|
|
to_qkv gives q, k and v as its output channels 0-1023, 1024-2047 and 2048-3071. A time conv C -> 2C gives frame 2t of
|
|
its output as channels 0..C-1 and frame 2t+1 as channels C..2C-1. conv_out's 12 channels are (colour k, column offset
|
|
j, row offset i) of 2 x 2 pixel patches: video[k, f, 2y + i, 2x + j] = channel 4k + 2j + i at (f, y, x).
|
|
|
|
# name shape role
|
|
0 post_quant_conv.weight [48, 48, 1, 1, 1] 1x1x1 conv 48 -> 48, on the latents
|
|
1 post_quant_conv.bias [48]
|
|
2 decoder.conv_in.weight [1024, 48, 3, 3, 3] causal 3x3x3 conv 48 -> 1024
|
|
3 decoder.conv_in.bias [1024]
|
|
4 decoder.mid_block.attentions.0.norm.gamma [1024, 1, 1] RMS norm gain, before to_qkv
|
|
5 decoder.mid_block.attentions.0.to_qkv.weight [3072, 1024, 1, 1] 1x1 conv2d 1024 -> 3072 (q, k, v)
|
|
6 decoder.mid_block.attentions.0.to_qkv.bias [3072]
|
|
7 decoder.mid_block.attentions.0.proj.weight [1024, 1024, 1, 1] 1x1 conv2d 1024 -> 1024
|
|
8 decoder.mid_block.attentions.0.proj.bias [1024]
|
|
9 decoder.mid_block.resnets.0.norm1.gamma [1024, 1, 1, 1] RMS norm gain, before conv1
|
|
10 decoder.mid_block.resnets.0.conv1.weight [1024, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 1024
|
|
11 decoder.mid_block.resnets.0.conv1.bias [1024]
|
|
12 decoder.mid_block.resnets.0.norm2.gamma [1024, 1, 1, 1] RMS norm gain, before conv2
|
|
13 decoder.mid_block.resnets.0.conv2.weight [1024, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 1024
|
|
14 decoder.mid_block.resnets.0.conv2.bias [1024]
|
|
15 decoder.mid_block.resnets.1.norm1.gamma [1024, 1, 1, 1] RMS norm gain, before conv1
|
|
16 decoder.mid_block.resnets.1.conv1.weight [1024, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 1024
|
|
17 decoder.mid_block.resnets.1.conv1.bias [1024]
|
|
18 decoder.mid_block.resnets.1.norm2.gamma [1024, 1, 1, 1] RMS norm gain, before conv2
|
|
19 decoder.mid_block.resnets.1.conv2.weight [1024, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 1024
|
|
20 decoder.mid_block.resnets.1.conv2.bias [1024]
|
|
21 decoder.up_blocks.0.resnets.0.norm1.gamma [1024, 1, 1, 1] RMS norm gain, before conv1
|
|
22 decoder.up_blocks.0.resnets.0.conv1.weight [1024, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 1024
|
|
23 decoder.up_blocks.0.resnets.0.conv1.bias [1024]
|
|
24 decoder.up_blocks.0.resnets.0.norm2.gamma [1024, 1, 1, 1] RMS norm gain, before conv2
|
|
25 decoder.up_blocks.0.resnets.0.conv2.weight [1024, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 1024
|
|
26 decoder.up_blocks.0.resnets.0.conv2.bias [1024]
|
|
27 decoder.up_blocks.0.resnets.1.norm1.gamma [1024, 1, 1, 1] RMS norm gain, before conv1
|
|
28 decoder.up_blocks.0.resnets.1.conv1.weight [1024, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 1024
|
|
29 decoder.up_blocks.0.resnets.1.conv1.bias [1024]
|
|
30 decoder.up_blocks.0.resnets.1.norm2.gamma [1024, 1, 1, 1] RMS norm gain, before conv2
|
|
31 decoder.up_blocks.0.resnets.1.conv2.weight [1024, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 1024
|
|
32 decoder.up_blocks.0.resnets.1.conv2.bias [1024]
|
|
33 decoder.up_blocks.0.resnets.2.norm1.gamma [1024, 1, 1, 1] RMS norm gain, before conv1
|
|
34 decoder.up_blocks.0.resnets.2.conv1.weight [1024, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 1024
|
|
35 decoder.up_blocks.0.resnets.2.conv1.bias [1024]
|
|
36 decoder.up_blocks.0.resnets.2.norm2.gamma [1024, 1, 1, 1] RMS norm gain, before conv2
|
|
37 decoder.up_blocks.0.resnets.2.conv2.weight [1024, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 1024
|
|
38 decoder.up_blocks.0.resnets.2.conv2.bias [1024]
|
|
39 decoder.up_blocks.0.upsampler.resample.1.weight [1024, 1024, 3, 3] 3x3 conv2d 1024 -> 1024, after x2
|
|
40 decoder.up_blocks.0.upsampler.resample.1.bias [1024]
|
|
41 decoder.up_blocks.0.upsampler.time_conv.weight [2048, 1024, 3, 1, 1] causal 3x1x1 conv 1024 -> 2048
|
|
42 decoder.up_blocks.0.upsampler.time_conv.bias [2048]
|
|
43 decoder.up_blocks.1.resnets.0.norm1.gamma [1024, 1, 1, 1] RMS norm gain, before conv1
|
|
44 decoder.up_blocks.1.resnets.0.conv1.weight [1024, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 1024
|
|
45 decoder.up_blocks.1.resnets.0.conv1.bias [1024]
|
|
46 decoder.up_blocks.1.resnets.0.norm2.gamma [1024, 1, 1, 1] RMS norm gain, before conv2
|
|
47 decoder.up_blocks.1.resnets.0.conv2.weight [1024, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 1024
|
|
48 decoder.up_blocks.1.resnets.0.conv2.bias [1024]
|
|
49 decoder.up_blocks.1.resnets.1.norm1.gamma [1024, 1, 1, 1] RMS norm gain, before conv1
|
|
50 decoder.up_blocks.1.resnets.1.conv1.weight [1024, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 1024
|
|
51 decoder.up_blocks.1.resnets.1.conv1.bias [1024]
|
|
52 decoder.up_blocks.1.resnets.1.norm2.gamma [1024, 1, 1, 1] RMS norm gain, before conv2
|
|
53 decoder.up_blocks.1.resnets.1.conv2.weight [1024, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 1024
|
|
54 decoder.up_blocks.1.resnets.1.conv2.bias [1024]
|
|
55 decoder.up_blocks.1.resnets.2.norm1.gamma [1024, 1, 1, 1] RMS norm gain, before conv1
|
|
56 decoder.up_blocks.1.resnets.2.conv1.weight [1024, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 1024
|
|
57 decoder.up_blocks.1.resnets.2.conv1.bias [1024]
|
|
58 decoder.up_blocks.1.resnets.2.norm2.gamma [1024, 1, 1, 1] RMS norm gain, before conv2
|
|
59 decoder.up_blocks.1.resnets.2.conv2.weight [1024, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 1024
|
|
60 decoder.up_blocks.1.resnets.2.conv2.bias [1024]
|
|
61 decoder.up_blocks.1.upsampler.resample.1.weight [1024, 1024, 3, 3] 3x3 conv2d 1024 -> 1024, after x2
|
|
62 decoder.up_blocks.1.upsampler.resample.1.bias [1024]
|
|
63 decoder.up_blocks.1.upsampler.time_conv.weight [2048, 1024, 3, 1, 1] causal 3x1x1 conv 1024 -> 2048
|
|
64 decoder.up_blocks.1.upsampler.time_conv.bias [2048]
|
|
65 decoder.up_blocks.2.resnets.0.norm1.gamma [1024, 1, 1, 1] RMS norm gain, before conv1
|
|
66 decoder.up_blocks.2.resnets.0.conv1.weight [512, 1024, 3, 3, 3] causal 3x3x3 conv 1024 -> 512
|
|
67 decoder.up_blocks.2.resnets.0.conv1.bias [512]
|
|
68 decoder.up_blocks.2.resnets.0.norm2.gamma [512, 1, 1, 1] RMS norm gain, before conv2
|
|
69 decoder.up_blocks.2.resnets.0.conv2.weight [512, 512, 3, 3, 3] causal 3x3x3 conv 512 -> 512
|
|
70 decoder.up_blocks.2.resnets.0.conv2.bias [512]
|
|
71 decoder.up_blocks.2.resnets.0.conv_shortcut.weight [512, 1024, 1, 1, 1] 1x1x1 conv 1024 -> 512, shortcut
|
|
72 decoder.up_blocks.2.resnets.0.conv_shortcut.bias [512]
|
|
73 decoder.up_blocks.2.resnets.1.norm1.gamma [512, 1, 1, 1] RMS norm gain, before conv1
|
|
74 decoder.up_blocks.2.resnets.1.conv1.weight [512, 512, 3, 3, 3] causal 3x3x3 conv 512 -> 512
|
|
75 decoder.up_blocks.2.resnets.1.conv1.bias [512]
|
|
76 decoder.up_blocks.2.resnets.1.norm2.gamma [512, 1, 1, 1] RMS norm gain, before conv2
|
|
77 decoder.up_blocks.2.resnets.1.conv2.weight [512, 512, 3, 3, 3] causal 3x3x3 conv 512 -> 512
|
|
78 decoder.up_blocks.2.resnets.1.conv2.bias [512]
|
|
79 decoder.up_blocks.2.resnets.2.norm1.gamma [512, 1, 1, 1] RMS norm gain, before conv1
|
|
80 decoder.up_blocks.2.resnets.2.conv1.weight [512, 512, 3, 3, 3] causal 3x3x3 conv 512 -> 512
|
|
81 decoder.up_blocks.2.resnets.2.conv1.bias [512]
|
|
82 decoder.up_blocks.2.resnets.2.norm2.gamma [512, 1, 1, 1] RMS norm gain, before conv2
|
|
83 decoder.up_blocks.2.resnets.2.conv2.weight [512, 512, 3, 3, 3] causal 3x3x3 conv 512 -> 512
|
|
84 decoder.up_blocks.2.resnets.2.conv2.bias [512]
|
|
85 decoder.up_blocks.2.upsampler.resample.1.weight [512, 512, 3, 3] 3x3 conv2d 512 -> 512, after x2
|
|
86 decoder.up_blocks.2.upsampler.resample.1.bias [512]
|
|
87 decoder.up_blocks.3.resnets.0.norm1.gamma [512, 1, 1, 1] RMS norm gain, before conv1
|
|
88 decoder.up_blocks.3.resnets.0.conv1.weight [256, 512, 3, 3, 3] causal 3x3x3 conv 512 -> 256
|
|
89 decoder.up_blocks.3.resnets.0.conv1.bias [256]
|
|
90 decoder.up_blocks.3.resnets.0.norm2.gamma [256, 1, 1, 1] RMS norm gain, before conv2
|
|
91 decoder.up_blocks.3.resnets.0.conv2.weight [256, 256, 3, 3, 3] causal 3x3x3 conv 256 -> 256
|
|
92 decoder.up_blocks.3.resnets.0.conv2.bias [256]
|
|
93 decoder.up_blocks.3.resnets.0.conv_shortcut.weight [256, 512, 1, 1, 1] 1x1x1 conv 512 -> 256, shortcut
|
|
94 decoder.up_blocks.3.resnets.0.conv_shortcut.bias [256]
|
|
95 decoder.up_blocks.3.resnets.1.norm1.gamma [256, 1, 1, 1] RMS norm gain, before conv1
|
|
96 decoder.up_blocks.3.resnets.1.conv1.weight [256, 256, 3, 3, 3] causal 3x3x3 conv 256 -> 256
|
|
97 decoder.up_blocks.3.resnets.1.conv1.bias [256]
|
|
98 decoder.up_blocks.3.resnets.1.norm2.gamma [256, 1, 1, 1] RMS norm gain, before conv2
|
|
99 decoder.up_blocks.3.resnets.1.conv2.weight [256, 256, 3, 3, 3] causal 3x3x3 conv 256 -> 256
|
|
100 decoder.up_blocks.3.resnets.1.conv2.bias [256]
|
|
101 decoder.up_blocks.3.resnets.2.norm1.gamma [256, 1, 1, 1] RMS norm gain, before conv1
|
|
102 decoder.up_blocks.3.resnets.2.conv1.weight [256, 256, 3, 3, 3] causal 3x3x3 conv 256 -> 256
|
|
103 decoder.up_blocks.3.resnets.2.conv1.bias [256]
|
|
104 decoder.up_blocks.3.resnets.2.norm2.gamma [256, 1, 1, 1] RMS norm gain, before conv2
|
|
105 decoder.up_blocks.3.resnets.2.conv2.weight [256, 256, 3, 3, 3] causal 3x3x3 conv 256 -> 256
|
|
106 decoder.up_blocks.3.resnets.2.conv2.bias [256]
|
|
107 decoder.norm_out.gamma [256, 1, 1, 1] RMS norm gain, before conv_out
|
|
108 decoder.conv_out.weight [12, 256, 3, 3, 3] causal 3x3x3 conv 256 -> 12
|
|
109 decoder.conv_out.bias [12]
|
|
|
|
Up blocks 0 and 1 run at 1024 channels, up block 2 from 1024 to 512, up block 3 from 512 to 256; a residual block's
|
|
first conv changes the channel count, and its 1x1x1 conv_shortcut does so on the shortcut.
|
|
|
|
Data
|
|
----
|
|
$DATA (default /data), read-only: latents_<source>.pt ({"latents": fp32 [1, 48, 121, 44, 78]}), ref_<clip>.pt
|
|
({"video": fp16 [1, 3, 4T-3, 704, 1248], "source", "latent_frames"}), calibration.json (per clip: bf16_max_abs,
|
|
bf16_mean_abs, bf16_rms, bf16_psnr_db, fp32_decode_s, bf16_decode_s), weight_names.json and decoder_weights_bf16.pt.
|
|
|
|
The work of one decode (bounds)
|
|
-------------------------------
|
|
FLOPs, a multiply-add counting 2: every convolution at its full kernel over its output positions (2 x the weight's
|
|
elements x frames x rows x columns, zero padding included, which adds a few percent at 44 x 78 and less than one
|
|
beyond), plus the attention's two products per latent frame (2 x 2 x 3432^2 x 1024). The frames at each level, for T
|
|
latent frames: T at 44 x 78 (post_quant_conv, conv_in, the mid block, up block 0's residual blocks); T - 1 for up
|
|
block 0's time conv, which skips the first latent frame; 2T - 1 at 88 x 156 (up block 0's 3x3 conv, up block 1's
|
|
residual blocks); 2T - 2 for up block 1's time conv; 4T - 3 at 176 x 312 (up block 1's 3x3 conv, up block 2's
|
|
residual blocks) and at 352 x 624 (up block 2's 3x3 conv, up block 3, conv_out). The FLOPs are divided over the
|
|
workload's GPUs. Bytes: the video written (on rank 0) and the weights read once.
|
|
"""
|
|
|
|
import json
|
|
import math
|
|
import os
|
|
|
|
LATENT_CHANNELS = 48
|
|
LATENT_HW = (44, 78)
|
|
VIDEO_HW = (704, 1248)
|
|
# How far a solution may err, against what the release's bf16 decode errs by on the same clip (see Correctness).
|
|
ERROR_FACTOR = 3.0
|
|
MAX_ABS_CAP = 2.0
|
|
WEIGHTS_FILE = "decoder_weights_bf16.pt"
|
|
NAMES_FILE = "weight_names.json"
|
|
|
|
CLIPS = {
|
|
"f1": {"source": "row_000168", "latent_frames": 1},
|
|
"f5": {"source": "row_000168", "latent_frames": 2},
|
|
"f17": {"source": "row_000168", "latent_frames": 5},
|
|
"f121": {"source": "row_000168", "latent_frames": 31},
|
|
"f241": {"source": "row_000129", "latent_frames": 61},
|
|
"f481": {"source": "row_000090", "latent_frames": 121},
|
|
}
|
|
# The short clips are checked at one GPU and at four, never timed; the long ones are checked and timed at 1, 2 and 4.
|
|
SHORT_CLIPS, LONG_CLIPS = ("f1", "f5", "f17"), ("f121", "f241", "f481")
|
|
WORKLOADS = {
|
|
**{f"{c}x{n}": {"clip": c, "world": n, "timed": False} for c in SHORT_CLIPS for n in (1, 4)},
|
|
**{f"{c}x{n}": {"clip": c, "world": n, "timed": True} for c in LONG_CLIPS for n in (1, 2, 4)},
|
|
}
|
|
|
|
# decoder.up_blocks.{i}: (in channels, out channels, temporal upsampling, spatial upsampling)
|
|
UP_BLOCKS = ((1024, 1024, True, True), (1024, 1024, True, True), (1024, 512, False, True), (512, 256, False, False))
|
|
RESNETS_PER_UP_BLOCK = 3
|
|
|
|
|
|
def _weights():
|
|
"""(name, shape) of run()'s weights in diffusers' registration order (see The weights)."""
|
|
out = []
|
|
|
|
def conv(name, shape):
|
|
out.extend([(f"{name}.weight", tuple(shape)), (f"{name}.bias", (shape[0],))])
|
|
|
|
def resnet(name, cin, cout):
|
|
out.append((f"{name}.norm1.gamma", (cin, 1, 1, 1)))
|
|
conv(f"{name}.conv1", (cout, cin, 3, 3, 3))
|
|
out.append((f"{name}.norm2.gamma", (cout, 1, 1, 1)))
|
|
conv(f"{name}.conv2", (cout, cout, 3, 3, 3))
|
|
if cin != cout:
|
|
conv(f"{name}.conv_shortcut", (cout, cin, 1, 1, 1))
|
|
|
|
conv("post_quant_conv", (48, 48, 1, 1, 1))
|
|
conv("decoder.conv_in", (1024, 48, 3, 3, 3))
|
|
out.append(("decoder.mid_block.attentions.0.norm.gamma", (1024, 1, 1)))
|
|
conv("decoder.mid_block.attentions.0.to_qkv", (3072, 1024, 1, 1))
|
|
conv("decoder.mid_block.attentions.0.proj", (1024, 1024, 1, 1))
|
|
for j in range(2):
|
|
resnet(f"decoder.mid_block.resnets.{j}", 1024, 1024)
|
|
for i, (cin, cout, temporal, spatial) in enumerate(UP_BLOCKS):
|
|
for j in range(RESNETS_PER_UP_BLOCK):
|
|
resnet(f"decoder.up_blocks.{i}.resnets.{j}", cin if j == 0 else cout, cout)
|
|
if spatial:
|
|
conv(f"decoder.up_blocks.{i}.upsampler.resample.1", (cout, cout, 3, 3))
|
|
if temporal:
|
|
conv(f"decoder.up_blocks.{i}.upsampler.time_conv", (2 * cout, cout, 3, 1, 1))
|
|
out.append(("decoder.norm_out.gamma", (256, 1, 1, 1)))
|
|
conv("decoder.conv_out", (12, 256, 3, 3, 3))
|
|
return tuple(out)
|
|
|
|
|
|
WEIGHTS = _weights()
|
|
WEIGHT_NAMES = [name for name, _ in WEIGHTS]
|
|
|
|
|
|
def data_dir():
|
|
"""The task's data: $DATA, else /data, where the jobs mount it."""
|
|
return os.environ.get("DATA") or "/data"
|
|
|
|
|
|
def frames(latent_frames):
|
|
"""The video frames of T latent frames: one for the first, four for every later one."""
|
|
return 4 * latent_frames - 3
|
|
|
|
|
|
def output_shape(workload):
|
|
"""The shape of rank 0's out at a workload."""
|
|
return (1, 3, frames(CLIPS[WORKLOADS[workload]["clip"]]["latent_frames"]), *VIDEO_HW)
|
|
|
|
|
|
def _load(path):
|
|
import torch
|
|
|
|
try:
|
|
return torch.load(path, map_location="cpu", weights_only=True, mmap=True)
|
|
except (OSError, RuntimeError) as e:
|
|
raise ValueError(f"cannot read {path}: {e}") from e
|
|
|
|
|
|
def make_inputs(workload, device):
|
|
"""[z]: the clip's latents, the first T latent frames of its source, rounded to bf16, contiguous on `device`."""
|
|
import torch
|
|
|
|
clip = CLIPS[WORKLOADS[workload]["clip"]]
|
|
path = os.path.join(data_dir(), f"latents_{clip['source']}.pt")
|
|
latents = _load(path)["latents"]
|
|
t = clip["latent_frames"]
|
|
if latents.dim() != 5 or tuple(latents.shape[:2]) != (1, LATENT_CHANNELS) or tuple(latents.shape[3:]) != LATENT_HW \
|
|
or latents.shape[2] < t:
|
|
raise ValueError(f"{path}: latents {tuple(latents.shape)}, expected [1, 48, >= {t}, 44, 78]")
|
|
return [latents[:, :, :t].to(device=device, dtype=torch.bfloat16).contiguous()]
|
|
|
|
|
|
def load_weights(device):
|
|
"""run()'s weights on `device`: decoder_weights_bf16.pt, checked against WEIGHTS (and weight_names.json where it
|
|
is), so that a solution indexes what this file documents."""
|
|
import torch
|
|
|
|
listed = os.path.join(data_dir(), NAMES_FILE)
|
|
if os.path.exists(listed):
|
|
with open(listed) as f:
|
|
names = [(e["name"], tuple(e["shape"])) for e in json.load(f)]
|
|
if names != list(WEIGHTS):
|
|
raise ValueError(f"{listed} does not list spec.WEIGHTS: the weights' order or shapes changed")
|
|
path = os.path.join(data_dir(), WEIGHTS_FILE)
|
|
weights = _load(path)
|
|
if not isinstance(weights, list) or len(weights) != len(WEIGHTS):
|
|
raise ValueError(f"{path}: expected a list of {len(WEIGHTS)} tensors")
|
|
for i, (t, (name, shape)) in enumerate(zip(weights, WEIGHTS)):
|
|
if not isinstance(t, torch.Tensor) or t.dtype != torch.bfloat16 or tuple(t.shape) != shape:
|
|
got = f"{t.dtype} {tuple(t.shape)}" if isinstance(t, torch.Tensor) else type(t).__name__
|
|
raise ValueError(f"{path}: tensor {i} is {got}, expected {name}, bf16 {shape}")
|
|
return [t.to(device).contiguous() for t in weights]
|
|
|
|
|
|
def load_reference(clip):
|
|
"""The clip's fp32 decode, as stored: a CPU fp16 tensor [1, 3, 4T-3, 704, 1248] (memory-mapped)."""
|
|
import torch
|
|
|
|
path = os.path.join(data_dir(), f"ref_{clip}.pt")
|
|
ref = _load(path)
|
|
t, source = CLIPS[clip]["latent_frames"], CLIPS[clip]["source"]
|
|
video = ref.get("video") if isinstance(ref, dict) else None
|
|
shape = (1, 3, frames(t), *VIDEO_HW)
|
|
if not isinstance(video, torch.Tensor) or tuple(video.shape) != shape or video.dtype != torch.float16:
|
|
raise ValueError(f"{path}: expected video, fp16 {list(shape)}")
|
|
if ref.get("source") != source or ref.get("latent_frames") != t:
|
|
raise ValueError(f"{path} decodes {ref.get('source')}'s first {ref.get('latent_frames')} latent frames, not "
|
|
f"{source}'s first {t}")
|
|
return video
|
|
|
|
|
|
def calibration(clip):
|
|
"""The release's bf16 decode of the clip against its fp32 decode (calibration.json's entry)."""
|
|
path = os.path.join(data_dir(), "calibration.json")
|
|
try:
|
|
with open(path) as f:
|
|
entry = json.load(f)[clip]
|
|
except (OSError, ValueError, KeyError) as e:
|
|
raise ValueError(f"{path} has no entry for {clip}: {e}") from e
|
|
if entry.get("source") != CLIPS[clip]["source"] or entry.get("latent_frames") != CLIPS[clip]["latent_frames"]:
|
|
raise ValueError(f"{path}: {clip} is calibrated on another clip ({entry.get('source')}, "
|
|
f"{entry.get('latent_frames')} latent frames)")
|
|
return entry
|
|
|
|
|
|
def allowance(clip):
|
|
"""{"max_abs", "rms"}: how far a solution's video may be from the fp32 decode of the clip (see Correctness)."""
|
|
entry = calibration(clip)
|
|
return {"max_abs": min(MAX_ABS_CAP, ERROR_FACTOR * entry["bf16_max_abs"]), "rms": ERROR_FACTOR * entry["bf16_rms"]}
|
|
|
|
|
|
def _conv_level(name, t):
|
|
"""(output frames, upsampling factor in space) of the conv whose weight is `name`, decoding t latent frames."""
|
|
if name.startswith(("post_quant_conv.", "decoder.conv_in.", "decoder.mid_block.", "decoder.up_blocks.0.resnets.")):
|
|
return t, 1
|
|
if name.startswith("decoder.up_blocks.0.upsampler.time_conv."):
|
|
return t - 1, 1
|
|
if name.startswith(("decoder.up_blocks.0.upsampler.resample.", "decoder.up_blocks.1.resnets.")):
|
|
return 2 * t - 1, 2
|
|
if name.startswith("decoder.up_blocks.1.upsampler.time_conv."):
|
|
return 2 * t - 2, 2
|
|
if name.startswith(("decoder.up_blocks.1.upsampler.resample.", "decoder.up_blocks.2.resnets.")):
|
|
return 4 * t - 3, 4
|
|
return 4 * t - 3, 8
|
|
|
|
|
|
def decode_flops(t):
|
|
"""FLOPs of one decode of t latent frames (see The work of one decode)."""
|
|
h, w = LATENT_HW
|
|
total = 0
|
|
for name, shape in WEIGHTS:
|
|
if name.endswith(".weight") and len(shape) >= 4:
|
|
count, scale = _conv_level(name, t)
|
|
total += 2 * math.prod(shape) * count * (h * scale) * (w * scale)
|
|
return total + 2 * 2 * (h * w) ** 2 * 1024 * t
|
|
|
|
|
|
def bounds(workload):
|
|
"""{"flops", "bytes", "gpus"} of one call at the workload, for bounds.py: the decode's FLOPs (spread over the
|
|
world's GPUs), and the bytes that must move: the video written and the weights read."""
|
|
entry = WORKLOADS[workload]
|
|
t = CLIPS[entry["clip"]]["latent_frames"]
|
|
video = math.prod(output_shape(workload)) * 2
|
|
weights = sum(math.prod(shape) for _, shape in WEIGHTS) * 2
|
|
return {"flops": decode_flops(t), "bytes": video + weights, "gpus": entry["world"]}
|
|
|
|
|
|
# The stack rules beside audit.py's: run() reads nothing but its arguments.
|
|
RULES = {
|
|
"file-io": {
|
|
"include": r"(fstream|fcntl\.h|sys/mman\.h|filesystem|experimental/filesystem)",
|
|
"text": r"\b(fopen|fopen64|freopen|fdopen|fread|fwrite|ifstream|ofstream|fstream|filebuf|mmap|mmap64|shm_open|"
|
|
r"torch::load|torch::save|torch::pickle_load|torch::pickle_save|torch::jit::load)\b|"
|
|
r"::\s*(open|openat|read|pread|write|pwrite)\s*\(",
|
|
"why": "run() and prepare() read nothing but their arguments and write nothing but out: no files (the "
|
|
"reference videos lie on the job's disk) and nothing that outlives a call"},
|
|
}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
for w in WORKLOADS:
|
|
b = bounds(w)
|
|
print(f"{w:<8} {b['flops'] / 1e15:8.4f} PFLOP over {b['gpus']} GPU(s), {b['bytes'] / 1e9:6.2f} GB")
|