File size: 13,933 Bytes
775b8fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""General (non-causal) fast H3 VAE decoder: the released H3 VAE ViT decoder pruned to 24 layers, decoding whole
native 5-latent blocks with window attention and no tiling / temporal-overlap recomputation.

* time: native 5-latent H3 blocks (17 video frames). Attention is bidirectional inside a temporal window of 5
  latent frames. Odd layers shift the temporal windows by 2 latent frames (Video-Swin style), so neighbouring
  blocks exchange information inside the network; nothing is recomputed and nothing is cross-faded.
* space: 16x16-token windows (256 px, the released tile size); odd layers shift them by half a window.
* global tokens: every native 5-latent block owns 5 global tokens (4 register + 1 class, from the released decoder).
  They attend to every token of their block; every window attends to the global tokens of the blocks it overlaps.
* rotary positions: spatial ids are window-local (as in the released tile decode); time ids are window-local too,
  so nothing depends on the absolute position in the video and any length decodes the same way.
* output: per token a 3 x 4 x 16 x 16 pixel patch; the first latent of every block keeps 1 of its 4 frames,
  exactly as the released decoder, so 5 latent frames give 17 video frames.

Input z: normalized latents (B, T, 24, H, W), t = 0 is the start of a native block. Output: decode_base-space
pixels (B, 3, F, 16H, 16W); apply vae.processor.revert_tensor for RGB.
"""
from __future__ import annotations

import copy

import torch
from torch import nn
from torch.nn import functional as F

from spark_compat import _apply_qk_norm, _apply_rotary_pos_emb_impl

PATCH, PATCH_T, OUT_CH = 16, 4, 3
KEEP_24 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 20, 21, 31, 32, 35]
# greedy single-layer removal on the released 36-layer decoder (vae_prune/layer_scan.py)
KEEP_16 = [0, 1, 2, 3, 5, 6, 8, 9, 10, 11, 14, 15, 20, 31, 32, 35]
KEEP_12 = [0, 1, 2, 3, 9, 10, 14, 15, 20, 31, 32, 35]
KEEP_8 = [0, 1, 2, 15, 20, 31, 32, 35]
KEEP_6 = [0, 1, 2, 15, 20, 35]          # KEEP_8 minus the two layers with the smallest single-removal PSNR drop
KEEP = {24: KEEP_24, 16: KEEP_16, 12: KEEP_12, 8: KEEP_8, 6: KEEP_6}


def shrink_ffn(ff, idx):
    """Keep the SwiGLU hidden channels idx: gate rows idx, up rows I+idx of w1 and columns idx of w2."""
    I = ff.w2.weight.shape[1]
    rows = torch.cat([idx, idx + I])
    w1 = nn.Linear(ff.w1.in_features, 2 * len(idx), bias=ff.w1.bias is not None).to(ff.w1.weight)
    w2 = nn.Linear(len(idx), ff.w2.out_features, bias=ff.w2.bias is not None).to(ff.w2.weight)
    with torch.no_grad():
        w1.weight.copy_(ff.w1.weight[rows]); w2.weight.copy_(ff.w2.weight[:, idx])
        if ff.w1.bias is not None: w1.bias.copy_(ff.w1.bias[rows])
        if ff.w2.bias is not None: w2.bias.copy_(ff.w2.bias)
    ff.w1, ff.w2 = w1, w2


class NonCausalViTDecoder(nn.Module):
    def __init__(self, vae, keep_layers=KEEP_24, *, window: int = 16, block: int = 5, t_shift: int = 2,
                 frames_to_trim: int = 3, time_norm: float | None = None, ffn_keep: int | None = None):
        super().__init__()
        dec = vae.decoder
        self.keep_layers = list(keep_layers)
        self.window, self.block, self.t_shift, self.frames_to_trim = window, block, t_shift, frames_to_trim
        # time ids: (window-local index + 0.5) / time_norm * 2 - 1. The released decoder decodes 7-latent chunks
        # (5 + 2 overlap) with length-normalized ids, i.e. time_norm = 7; default = block keeps the v1 behaviour.
        self.time_norm = float(time_norm if time_norm is not None else block)
        self.post_quant_conv = copy.deepcopy(vae.post_quant_conv)
        self.x_embedder = copy.deepcopy(dec.x_embedder)
        self.register_tokens = nn.Parameter(dec.register_tokens.detach().clone())
        self.blocks = nn.ModuleList([copy.deepcopy(dec.transformer_blocks[i]) for i in self.keep_layers])
        self.norm_out = copy.deepcopy(dec.norm_out)
        self.proj_out = copy.deepcopy(dec.proj_out)
        self.pos_embed = copy.deepcopy(dec.pos_embed)
        self.heads = self.blocks[0].attn.heads
        self.dh = self.blocks[0].attn.dim_head
        self.register_buffer("latents_mean", vae.latents_mean.detach().clone().float(), persistent=False)
        self.register_buffer("latents_std", vae.latents_std.detach().clone().float(), persistent=False)
        for p in self.pos_embed.parameters():
            p.requires_grad_(False)
        self.grad_checkpointing = False      # per-layer activation checkpointing (training only)
        if ffn_keep is not None:             # placeholder channels; prune_ffn picks the real ones before training
            for blk in self.blocks:
                shrink_ffn(blk.ff, torch.arange(ffn_keep, device=blk.ff.w2.weight.device))

    def prune_ffn(self, vae, scores, keep: int, src_model=None):
        """Rebuild every FFN with the `keep` highest-scoring hidden channels of the source FFN.
        Source: the full-width layers of src_model (e.g. a trained decoder), else the released VAE layers.
        scores[li]: (I,) importance of the source layer's hidden channels."""
        for li, blk in enumerate(self.blocks):
            ff = src_model.blocks[li].ff if src_model is not None else vae.decoder.transformer_blocks[self.keep_layers[li]].ff
            src = copy.deepcopy(ff).to(blk.ff.w2.weight.device)
            idx = torch.topk(scores[li].to(src.w2.weight.device), keep).indices.sort().values
            shrink_ffn(src, idx)
            blk.ff = src

    # ------------------------------------------------------------------ geometry
    def _shifts(self, li):
        odd = li % 2 == 1
        return (self.window // 2 if odd else 0), (self.t_shift if odd else 0)

    def _rope(self, T, H, W, s, st, device):
        """cos/sin for tokens in (T, H, W) order with window-local time and space ids for shifts (st, s)."""
        lt = ((torch.arange(T, device=device) + st) % self.block).float()
        ly = ((torch.arange(H, device=device) + s) % self.window).float()
        lx = ((torch.arange(W, device=device) + s) % self.window).float()
        t = (lt + 0.5) / self.time_norm * 2 - 1
        hy = (ly + 0.5) / self.window * 2 - 1
        wx = (lx + 0.5) / self.window * 2 - 1
        grid = torch.stack(torch.meshgrid(t, hy, wx, indexing="ij"), -1).reshape(1, -1, 3)
        return self.pos_embed(grid.float())

    def _partition(self, x, s, st):
        """x: (B, T, H, W, ...) -> windows (B*nW, Tw*ws*ws, ...), valid (nW, Tw*ws*ws), info.
        Window order is (time window, row, col)."""
        B, T, H, W = x.shape[:4]
        ws, tb = self.window, self.block
        rest = x.shape[4:]
        Tp = ((T + st + tb - 1) // tb) * tb
        Hp = ((H + s + ws - 1) // ws) * ws
        Wp = ((W + s + ws - 1) // ws) * ws
        xp = x.new_zeros(B, Tp, Hp, Wp, *rest)
        xp[:, st:st + T, s:s + H, s:s + W] = x
        valid = torch.zeros(Tp, Hp, Wp, dtype=torch.bool, device=x.device)
        valid[st:st + T, s:s + H, s:s + W] = True
        nt, nh, nw = Tp // tb, Hp // ws, Wp // ws
        r = len(rest)
        xw = xp.view(B, nt, tb, nh, ws, nw, ws, *rest).permute(0, 1, 3, 5, 2, 4, 6, *range(7, 7 + r))
        xw = xw.reshape(B * nt * nh * nw, tb * ws * ws, *rest)
        vw = valid.view(nt, tb, nh, ws, nw, ws).permute(0, 2, 4, 1, 3, 5).reshape(nt * nh * nw, tb * ws * ws)
        return xw, vw, (B, T, H, W, Tp, Hp, Wp, nt, nh, nw)

    def _unpartition(self, xw, info, s, st):
        B, T, H, W, Tp, Hp, Wp, nt, nh, nw = info
        ws, tb = self.window, self.block
        rest = xw.shape[2:]
        r = len(rest)
        x = xw.view(B, nt, nh, nw, tb, ws, ws, *rest).permute(0, 1, 4, 2, 5, 3, 6, *range(7, 7 + r))
        x = x.reshape(B, Tp, Hp, Wp, *rest)
        return x[:, st:st + T, s:s + H, s:s + W]

    def _layer(self, li, blk, x, g):
        """One transformer layer on the tokens x (B,T,H,W,D) and the per-block global tokens g (B,nb,S,D)."""
        B, T, H, W, nb, S = self._geom
        dev = x.device
        tb = self.block
        s, st = self._shifts(li)
        h = blk.norm1(x.float()).to(x.dtype)
        hg = blk.norm1(g.float()).to(g.dtype)
        q, k, v = blk.attn.to_qkv(h).view(B, T, H, W, self.heads, 3 * self.dh).chunk(3, -1)
        qg, kg, vg = blk.attn.to_qkv(hg).view(B, nb, S, self.heads, 3 * self.dh).chunk(3, -1)
        if blk.attn.norm_q is not None:
            q, qg = _apply_qk_norm(blk.attn.norm_q, q), _apply_qk_norm(blk.attn.norm_q, qg)
            k, kg = _apply_qk_norm(blk.attn.norm_k, k), _apply_qk_norm(blk.attn.norm_k, kg)
        cos, sin = self._rope(T, H, W, s, st, dev)
        q = _apply_rotary_pos_emb_impl(q.reshape(B, T * H * W, self.heads, self.dh), (cos, sin)).view(B, T, H, W, self.heads, self.dh)
        k = _apply_rotary_pos_emb_impl(k.reshape(B, T * H * W, self.heads, self.dh), (cos, sin)).view(B, T, H, W, self.heads, self.dh)
        # ---- window attention: tokens of a window + global tokens of the native blocks the window overlaps
        qw, vmask, info = self._partition(q, s, st)
        kw, _, _ = self._partition(k, s, st)
        vw, _, _ = self._partition(v, s, st)
        nt, nh, nw = info[7:]
        nW = nt * nh * nw
        # native blocks overlapped by temporal window j: latent range [j*tb - st, (j+1)*tb - st)
        tw_lo = (torch.arange(nt, device=dev) * tb - st).clamp(min=0)
        tw_hi = ((torch.arange(nt, device=dev) + 1) * tb - st - 1).clamp(max=T - 1)
        b0, b1 = tw_lo // tb, tw_hi // tb                         # at most 2 blocks per window
        gk_idx = torch.stack([b0, b1], 1)                          # nt, 2
        gk_valid = torch.stack([torch.ones_like(b0, dtype=torch.bool), b1 != b0], 1)   # nt, 2
        kgw = kg[:, gk_idx].reshape(B, nt, 2 * S, self.heads, self.dh)
        vgw = vg[:, gk_idx].reshape(B, nt, 2 * S, self.heads, self.dh)
        kgw = kgw[:, :, None, None].expand(B, nt, nh, nw, 2 * S, self.heads, self.dh).reshape(B * nW, 2 * S, self.heads, self.dh)
        vgw = vgw[:, :, None, None].expand(B, nt, nh, nw, 2 * S, self.heads, self.dh).reshape(B * nW, 2 * S, self.heads, self.dh)
        gmask = gk_valid[:, :, None].expand(nt, 2, S).reshape(nt, 2 * S)
        gmask = gmask[:, None, None].expand(nt, nh, nw, 2 * S).reshape(nW, 2 * S)
        Kc = torch.cat([kw, kgw], 1).transpose(1, 2)
        Vc = torch.cat([vw, vgw], 1).transpose(1, 2)
        kmask = torch.cat([vmask, gmask], 1).repeat(B, 1)[:, None, None, :]
        ow = F.scaled_dot_product_attention(qw.transpose(1, 2), Kc, Vc, attn_mask=kmask).transpose(1, 2)
        o = self._unpartition(ow, info, s, st).reshape(B, T, H, W, self.heads * self.dh)
        # ---- global tokens: attend to every token of their own native block + the block's global tokens
        Tp = nb * tb
        kb = k.new_zeros(B, Tp, H, W, self.heads, self.dh); kb[:, :T] = k
        vb = v.new_zeros(B, Tp, H, W, self.heads, self.dh); vb[:, :T] = v
        kb = kb.view(B, nb, tb * H * W, self.heads, self.dh)
        vb = vb.view(B, nb, tb * H * W, self.heads, self.dh)
        tvalid = torch.arange(Tp, device=dev) < T
        bmask = tvalid.view(nb, tb, 1).expand(nb, tb, H * W).reshape(nb, tb * H * W)
        Kg = torch.cat([kb, kg], 2).reshape(B * nb, tb * H * W + S, self.heads, self.dh).transpose(1, 2)
        Vg = torch.cat([vb, vg], 2).reshape(B * nb, tb * H * W + S, self.heads, self.dh).transpose(1, 2)
        gm = torch.cat([bmask, torch.ones(nb, S, dtype=torch.bool, device=dev)], 1).repeat(B, 1)[:, None, None, :]
        og = F.scaled_dot_product_attention(qg.reshape(B * nb, S, self.heads, self.dh).transpose(1, 2), Kg, Vg,
                                            attn_mask=gm).transpose(1, 2).reshape(B, nb, S, -1)
        x = x + blk.attn.to_out(o) * blk.scale1
        g = g + blk.attn.to_out(og) * blk.scale1
        x = x + blk.ff(blk.norm2(x.float()).to(x.dtype)) * blk.scale2
        g = g + blk.ff(blk.norm2(g.float()).to(g.dtype)) * blk.scale2
        return x, g

    # ------------------------------------------------------------------ forward
    def forward(self, z):
        B, T, _, H, W = z.shape
        dev = z.device
        tb, ws = self.block, self.window
        nb = (T + tb - 1) // tb                                     # native blocks
        compute_dtype = torch.get_autocast_dtype("cuda") if torch.is_autocast_enabled("cuda") else z.dtype
        raw = z.float() * self.latents_std.view(1, 1, -1, 1, 1) + self.latents_mean.view(1, 1, -1, 1, 1)
        raw = self.post_quant_conv(raw.transpose(1, 2))
        with torch.autocast("cuda", enabled=False):
            x = self.x_embedder(raw.permute(0, 2, 3, 4, 1).float())   # B,T,H,W,D
        x = x.to(compute_dtype)
        D = x.shape[-1]
        S = self.register_tokens.shape[1] + 1
        g = torch.cat([self.register_tokens.expand(B, -1, -1), x.new_zeros(B, 1, D)], 1).to(compute_dtype)
        g = g.unsqueeze(1).expand(B, nb, S, D).contiguous()          # B, nb, S, D  (one set per native block)
        self._geom = (B, T, H, W, nb, S)
        use_ckpt = self.grad_checkpointing and torch.is_grad_enabled() and self.training
        for li, blk in enumerate(self.blocks):
            if use_ckpt:
                x, g = torch.utils.checkpoint.checkpoint(self._layer, li, blk, x, g, use_reentrant=False)
            else:
                x, g = self._layer(li, blk, x, g)
        x = self.norm_out(x)
        with torch.autocast("cuda", enabled=False):
            out = self.proj_out(x.float())                                  # B,T,H,W,3*4*16*16
        out = out.view(B, T, H, W, OUT_CH, PATCH_T, PATCH, PATCH).permute(0, 4, 1, 5, 2, 6, 3, 7)
        out = out.reshape(B, OUT_CH, T * PATCH_T, H * PATCH, W * PATCH)
        keep = torch.ones(T * PATCH_T, dtype=torch.bool, device=dev)
        for t in range(0, T, tb):
            keep[t * PATCH_T: t * PATCH_T + self.frames_to_trim] = False
        return out[:, :, keep]