File size: 16,511 Bytes
742c169
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
from dataclasses import dataclass
from typing import Optional, Unpack

import torch
import torch.nn as nn
from transformers import DINOv3ViTModel, DINOv3ViTConfig
from transformers.utils import TransformersKwargs
from transformers.modeling_outputs import ModelOutput
from transformers.models.dinov3_vit.modeling_dinov3_vit import DINOv3ViTEmbeddings
from .mup import mup_init, mup_init_output

from .layers import AttentivePooling, RMSNorm
from .patch import TemporalPatch
from .tt import TemporalTransfer
from .tt3d import TemporalTransfer3D


@dataclass
class VideoModelOutputWithMotionTokens(ModelOutput):
    last_hidden_state: Optional[torch.FloatTensor] = None
    frames_output: Optional[torch.FloatTensor] = None
    pooler_output: Optional[torch.FloatTensor] = None
    motion_output: Optional[torch.FloatTensor] = None
    cls_output: Optional[torch.FloatTensor] = None
    hidden_states: Optional[tuple[torch.FloatTensor, ...]] = None
    attentions: Optional[tuple[torch.FloatTensor, ...]] = None


class DINOv3VidTConfig(DINOv3ViTConfig):
    model_type = "dinov3_vit"

    def __init__(
        self,
        patch_size: int = 16,
        temporal_patch_size: int = 1,
        temporal_patch_overlap: int = 0,
        hidden_size: int = 384,
        intermediate_size: int = 1536,
        num_hidden_layers: int = 12,
        motion_layers_period: int = 0,
        num_attention_heads: int = 6,
        hidden_act: str = "gelu",
        attention_dropout: float = 0.0,
        initializer_range: float = 0.02,
        layer_norm_eps: float = 1e-5,
        rope_theta: float = 100.0,
        image_size: int = 224,
        num_channels: int = 3,
        query_bias: bool = True,
        key_bias: bool = False,
        value_bias: bool = True,
        proj_bias: bool = True,
        mlp_bias: bool = True,
        layerscale_value: float = 1.0,
        drop_path_rate: float = 0.0,
        use_gated_mlp: bool = False,
        num_register_tokens: int = 0,
        num_motion_tokens: int = 0,
        motion_ffn_type: str = "swiglu",  # "swiglu" or "gelu" for TemporalTransfer
        tt_mode: str = "1d",  # "1d" (motion-only) or "3d" (motion + downsampled spatial)
        tt_downsample: int = 4,  # spatial downsample factor for tt_mode="3d"
        # train augs
        pos_embed_shift: Optional[float] = None,
        pos_embed_jitter: Optional[float] = None,
        pos_embed_rescale: Optional[float] = 2.0,
        **kwargs,
    ):
        super().__init__(**kwargs)

        self.image_size = image_size
        self.patch_size = patch_size
        self.temporal_patch_size = temporal_patch_size
        self.temporal_patch_overlap = temporal_patch_overlap
        self.num_channels = num_channels
        self.hidden_size = hidden_size
        self.intermediate_size = intermediate_size
        self.num_hidden_layers = num_hidden_layers
        self.motion_layers_period = motion_layers_period
        self.num_attention_heads = num_attention_heads
        self.hidden_act = hidden_act
        self.attention_dropout = attention_dropout
        self.initializer_range = initializer_range
        self.layer_norm_eps = layer_norm_eps
        self.layerscale_value = layerscale_value
        self.drop_path_rate = drop_path_rate
        self.use_gated_mlp = use_gated_mlp
        self.rope_theta = rope_theta
        self.query_bias = query_bias
        self.key_bias = key_bias
        self.value_bias = value_bias
        self.proj_bias = proj_bias
        self.mlp_bias = mlp_bias
        self.num_register_tokens = num_register_tokens
        self.num_motion_tokens = num_motion_tokens
        self.motion_ffn_type = motion_ffn_type
        self.tt_mode = tt_mode
        self.tt_downsample = tt_downsample

        # train augs
        self.pos_embed_shift = pos_embed_shift
        self.pos_embed_jitter = pos_embed_jitter
        self.pos_embed_rescale = pos_embed_rescale


class DINOv3VidTModel(DINOv3ViTModel):
    config: DINOv3VidTConfig

    def __init__(self, config: DINOv3VidTConfig):
        super().__init__(config)
        self.num_motion_tokens = config.num_motion_tokens
        if self.num_motion_tokens > 0:
            self.motion_tokens = nn.Parameter(
                torch.randn(config.num_motion_tokens, config.hidden_size)
                / (config.hidden_size * config.num_motion_tokens) ** 0.5
            )
        else:
            self.motion_tokens = None
        if config.temporal_patch_size > 1:
            self.temporal_embeddings = TemporalPatch(
                config.hidden_size,
                config.hidden_size,
                config.temporal_patch_size,
                config.temporal_patch_overlap,
            )
        else:
            self.temporal_embeddings = nn.Identity()
        # Attentive pooling on motion tokens -> CLS-like conditioning token
        if self.num_motion_tokens > 0:
            self.motion_pooler = AttentivePooling(
                config.hidden_size, num_heads=config.num_attention_heads
            )
            # Pool all TT tokens → 1 motion token for decoder output
            self.motion_output_pooler = AttentivePooling(
                config.hidden_size, num_heads=config.num_attention_heads
            )
            # Normalize cls_output (AttentivePooling has no output norm; without this
            # cls_output can reach absmax~50 and destabilize AdaRMSNorm conditioning)
            self.cls_norm = RMSNorm(config.hidden_size)
        else:
            self.motion_pooler = None
            self.motion_output_pooler = None
            self.cls_norm = None

        self.motion_layers = nn.ModuleList([])
        self.tt_mode = config.tt_mode

        layer_added = 0
        for idx, layer in enumerate(self.model.layer):
            if (
                config.motion_layers_period > 0
                and idx % config.motion_layers_period == 0
            ):
                layer_added += 1
                if config.tt_mode == "3d":
                    self.motion_layers.append(
                        TemporalTransfer3D(
                            config.hidden_size,
                            config.intermediate_size,
                            config.num_attention_heads,
                            downsample_factor=config.tt_downsample,
                            ffn_type=config.motion_ffn_type,
                        )
                    )
                else:
                    self.motion_layers.append(
                        TemporalTransfer(
                            config.hidden_size,
                            config.intermediate_size,
                            config.num_attention_heads,
                            ffn_type=config.motion_ffn_type,
                        )
                    )
            else:
                self.motion_layers.append(nn.Identity())

        mup_init(self.motion_layers.parameters())
        # Restore special inits clobbered by mup_init
        for layer in self.motion_layers:
            if isinstance(layer, nn.Identity):
                continue
            # Residual output projections: near-zero init
            if hasattr(layer, 'attn') and hasattr(layer.attn, 'out_proj'):
                mup_init_output(layer.attn.out_proj.weight)
            if hasattr(layer, 'out_proj'):  # TT3D has out_proj on the module
                mup_init_output(layer.out_proj.weight)
            if hasattr(layer.mlp, 'fc2'):
                mup_init_output(layer.mlp.fc2.weight)
            elif hasattr(layer.mlp, 'down'):
                mup_init_output(layer.mlp.down.weight)
            # TT3D-specific: zero-init spatial_out_proj, identity resample channel mix, qk_scale
            if hasattr(layer, 'spatial_out_proj'):
                nn.init.zeros_(layer.spatial_out_proj.weight)
                layer.spatial_down.reset_parameters()
                layer.spatial_up.reset_parameters()
            for module in layer.modules():
                if hasattr(module, 'qk_scale'):
                    module.qk_scale.data.fill_(10.0)
        print(f"Motion layers added: {layer_added} / {config.num_hidden_layers} (mode={config.tt_mode})")

    @torch.no_grad()
    def _init_weights(self, module) -> None:
        super()._init_weights(module)
        # transformers' from_pretrained builds on the meta device and leaves
        # non-persistent buffers empty: recompute those of the TT modules
        if hasattr(module, "reset_buffers"):
            module.reset_buffers()

    def freeze_pretrained(self, requires_grad: bool = False):
        """Freeze or unfreeze the pretrained backbone components.

        Pretrained components: embeddings, rope_embeddings, transformer layers, norm.
        Leaves trainable-from-scratch parts (motion_tokens, motion_layers,
        temporal_embeddings) untouched.
        """
        self.embeddings.requires_grad_(requires_grad)
        self.rope_embeddings.requires_grad_(requires_grad)
        self.model.layer.requires_grad_(requires_grad)
        self.norm.requires_grad_(requires_grad)

    def setup_patch(self, num_channels, patch_size):
        self.config.num_channels = num_channels
        self.config.patch_size = patch_size
        self.embeddings = DINOv3ViTEmbeddings(self.config)
        mup_init(self.embeddings.parameters())

    @classmethod
    def from_dino_v3(
        cls,
        dino_v3: DINOv3ViTModel,
        temporal_patch_size: int = 0,
        temporal_patch_overlap: int = 0,
        motion_layers_period: int = 0,
        num_motion_tokens: int = 0,
        motion_ffn_type: str = "swiglu",
        tt_mode: str = "1d",
        tt_downsample: int = 4,
    ):
        dino_v3_config = dino_v3.config
        ttvidt_config = DINOv3VidTConfig(
            **dino_v3_config.to_dict(),
            temporal_patch_size=temporal_patch_size,
            temporal_patch_overlap=temporal_patch_overlap,
            motion_layers_period=motion_layers_period,
            num_motion_tokens=num_motion_tokens,
            motion_ffn_type=motion_ffn_type,
            tt_mode=tt_mode,
            tt_downsample=tt_downsample,
        )
        new_model = cls(ttvidt_config)
        new_model.load_state_dict(dino_v3.state_dict(), strict=False)
        return new_model

    def forward(
        self,
        pixel_values: torch.Tensor,
        bool_masked_pos: Optional[torch.Tensor] = None,
        head_mask: Optional[torch.Tensor] = None,
        spatial_mask: Optional[torch.Tensor] = None,
        **kwargs: Unpack[TransformersKwargs],
    ) -> VideoModelOutputWithMotionTokens:
        r"""
        pixel_values: [B, T, C, H, W]
        spatial_mask: optional bool tensor [L], True = visible/keep (for MAE tube masking)
        """
        b, t = pixel_values.shape[:2]

        _, _, _, H_in, W_in = pixel_values.shape
        ps = self.config.patch_size
        spatial_h = H_in // ps
        spatial_w = W_in // ps

        pixel_values = pixel_values.to(
            self.embeddings.patch_embeddings.weight.dtype
        ).flatten(0, 1)
        hidden_states = self.embeddings(pixel_values, bool_masked_pos=bool_masked_pos)
        position_embeddings = self.rope_embeddings(pixel_values)
        pixel_length = position_embeddings[0].size(0)

        hidden_states = self.temporal_embeddings(hidden_states.unflatten(0, (b, -1)))

        # MAE tube masking: keep only visible spatial tokens
        if spatial_mask is not None:
            # hidden_states may have prefix tokens (CLS + registers) before spatial tokens
            # spatial_mask: bool [L] where L = num spatial patches
            n_prefix = hidden_states.shape[2] - spatial_mask.shape[0]
            if n_prefix > 0:
                prefix = hidden_states[:, :, :n_prefix]
                spatial = hidden_states[:, :, n_prefix:]
                hidden_states = torch.cat([prefix, spatial[:, :, spatial_mask]], dim=2)
            else:
                hidden_states = hidden_states[:, :, spatial_mask]
            # Subsample RoPE position embeddings: (cos[L, head_dim], sin[L, head_dim])
            cos, sin = position_embeddings
            position_embeddings = (cos[spatial_mask], sin[spatial_mask])
            pixel_length = spatial_mask.sum().item()
        b, ot, seq_len, dim = hidden_states.shape
        if self.motion_tokens is not None:
            hidden_states = torch.concat(
                [self.motion_tokens[None, None].repeat(b, ot, 1, 1), hidden_states],
                dim=2,
            )

        hidden_states = hidden_states.flatten(0, 1)
        for i, (layer_module, motion_module) in enumerate(
            zip(self.model.layer, self.motion_layers)
        ):
            layer_head_mask = head_mask[i] if head_mask is not None else None
            hidden_states = layer_module(
                hidden_states,
                attention_mask=layer_head_mask,
                position_embeddings=position_embeddings,
            )
            if not isinstance(motion_module, nn.Identity):
                hidden_states = hidden_states.unflatten(0, (b, ot))
                motion_tokens = hidden_states[:, :, : self.num_motion_tokens]
                vit_state = hidden_states[:, :, self.num_motion_tokens :]

                if isinstance(motion_module, TemporalTransfer3D):
                    # TT3D: pass spatial tokens (last pixel_length of vit_state)
                    # vit_state layout: [CLS, REG_0..REG_3, S_0..S_L]
                    spatial_tokens = vit_state[:, :, -pixel_length:]

                    if spatial_mask is not None:
                        # MAE mode: pad visible tokens back to full spatial grid
                        # for pixel unshuffle/shuffle in TT3D, then extract visible
                        L_full = spatial_h * spatial_w
                        full_spatial = spatial_tokens.new_zeros(
                            b, ot, L_full, dim
                        )
                        full_spatial[:, :, spatial_mask] = spatial_tokens
                        motion_tokens, full_spatial_out = motion_module(
                            motion_tokens, full_spatial, spatial_h, spatial_w
                        )
                        spatial_out = full_spatial_out[:, :, spatial_mask]
                    else:
                        motion_tokens, spatial_out = motion_module(
                            motion_tokens, spatial_tokens, spatial_h, spatial_w
                        )

                    # Write spatial back (only last pixel_length of vit_state)
                    vit_state = torch.cat(
                        [vit_state[:, :, :-pixel_length], spatial_out], dim=2
                    )
                else:
                    # TT1D: motion tokens only
                    motion_tokens = motion_module(motion_tokens)

                hidden_states = torch.concat([motion_tokens, vit_state], dim=2)
                hidden_states = hidden_states.flatten(0, 1)

        sequence_output = self.norm(hidden_states).unflatten(0, (b, ot))
        frames_output = sequence_output[:, :, -pixel_length:, :]
        pooled_output = sequence_output[:, :, self.num_motion_tokens, :]
        if self.motion_tokens is None:
            motion_output = None
            cls_output = None
        else:
            all_tt_tokens = sequence_output[
                :, :, : self.num_motion_tokens, :
            ]  # [B, T, num_motion_tokens, dim]
            cls_output = self.cls_norm(self.motion_pooler(all_tt_tokens))  # [B, T, dim]
            # Pool all TT tokens → 1 motion token for decoder
            motion_output = self.motion_output_pooler(all_tt_tokens).unsqueeze(2)  # [B, T, 1, dim]

        return VideoModelOutputWithMotionTokens(
            last_hidden_state=sequence_output,
            frames_output=frames_output,
            pooler_output=pooled_output,
            motion_output=motion_output,
            cls_output=cls_output,
        )


if __name__ == "__main__":
    dino = DINOv3ViTModel.from_pretrained(
        "facebook/dinov3-vitb16-pretrain-lvd1689m"
    ).cuda()
    dino_vidt = DINOv3VidTModel.from_dino_v3(
        dino,
        temporal_patch_size=4,
        temporal_patch_overlap=1,
        motion_layers_period=1,
        num_motion_tokens=16,
    ).cuda()

    print(dino)
    print(dino_vidt)

    test_x = torch.randn(1, 61, 3, 256, 256).cuda()
    with torch.autocast("cuda"):
        result = dino_vidt(test_x)

    print(result.frames_output.shape)
    print(result.pooler_output.shape)
    print(result.motion_output.shape)