File size: 10,651 Bytes
4fe31ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""MoT decoder layer with three-path routing (text / vision-input / vision-gen).

Subclasses `HunYuanVLMoTDecoderLayer` from upstream to add the third (`_g`)
path used by Flow Matching. Upstream provides `mlp / mlp_v` and matching
LayerNorms; we add `mlp_g / input_layernorm_g / post_attention_layernorm_g`.

`modality_mask` semantics:
  * 0 = text token            โ†’ mlp / input_layernorm / post_attention_layernorm
  * 1 = input vision token    โ†’ mlp_v / *_v
  * 2 = generated vision token (FM latent) โ†’ mlp_g / *_g

Attention QKVO is **not** extended to a third path โ€” generation tokens reuse
the `_v` projection (matches the existing HY-Unified design where
`q_proj_v / k_proj_v / v_proj_v / o_proj_v` cover both input and generated
vision tokens). We just need to convert the int modality mask to a bool
"vision" mask before calling `self.self_attn`.

mlp_g may optionally have a wider intermediate (Net2Wider scale-up). When
`config.mlp_g_intermediate_size` is set and larger than `intermediate_size`,
we build mlp_g with a cloned config that overrides `intermediate_size`.
"""
from __future__ import annotations

import copy
from typing import Optional

import torch

from transformers.cache_utils import Cache
from transformers.processing_utils import Unpack
from transformers.utils import TransformersKwargs
from transformers.utils.deprecation import deprecate_kwarg

from transformers.models.hunyuan_vl_mot.modeling_hunyuan_vl_mot import (
    HunYuanVLMoTDecoderLayer,
    HunYuanVLMoTMLP,
    HunYuanVLMoTRMSNorm,
)

# Side-effect import: replaces upstream's _flash_attention_forward_mot
from . import attention_mot_packed  # noqa: F401  # pylint: disable=unused-import


# ---------------------------------------------------------------------------
# Three-way mask_apply
# ---------------------------------------------------------------------------

def mask_apply_3way(
    hidden_states: torch.Tensor,
    modality_mask: Optional[torch.Tensor],
    text_funcs,
    vision_funcs,
    gen_funcs,
    out_dims=None,
    padding_mask: Optional[torch.Tensor] = None,
):
    """Routes tokens to text/vision/generation function lists by modality_mask.

    hidden_states: (B, S, D)
    modality_mask: (B, S) int tensor with values in {0, 1, 2}, or None
        0 = text, 1 = vision input, 2 = vision generation
    padding_mask: (B, S) int tensor, 1 = valid token, 0 = padding

    Returns a list of stacked (B, S, out_d) tensors โ€” one per `text_funcs[i]`.
    """
    if modality_mask is None:
        # All-text: skip routing entirely
        return [text_funcs[i](hidden_states) for i in range(len(text_funcs))]

    bsz, seq_len, hidden_dim = hidden_states.size()
    flat = hidden_states.reshape(bsz * seq_len, hidden_dim)
    mask_flat = modality_mask.reshape(bsz * seq_len)
    if padding_mask is not None:
        valid_flat = padding_mask.reshape(bsz * seq_len).bool()
    else:
        valid_flat = None

    placeholder = hidden_states[0:1, 0:1, :]  # (1, 1, D)
    zero_feature = 0

    num_outputs = len(text_funcs)
    if out_dims is None:
        out_dims_resolved = [hidden_dim] * num_outputs
    else:
        out_dims_resolved = list(out_dims)

    # Pre-allocate output buffers (empty, not zeros โ€” we overwrite all valid
    # positions and the rest are masked out by the caller / padding).
    out_flat = [
        torch.empty(bsz * seq_len, od, device=flat.device, dtype=flat.dtype)
        for od in out_dims_resolved
    ]
    # Padding positions need to be zeroed โ€” we won't touch them in the
    # gather/scatter below. Cheaper than zeroing the whole tensor: only
    # zero the rows that won't be hit by any of the three modalities.
    if valid_flat is not None:
        invalid_flat = ~valid_flat
        if invalid_flat.any():
            for buf in out_flat:
                buf[invalid_flat] = 0
    # else: all rows will be hit by exactly one of {text, vision, gen}.

    def _dispatch(idx_mask, funcs):
        """Run `funcs` on rows selected by `idx_mask`, scatter back. If no
        rows are selected, multiply through a placeholder so the params still
        receive grad (avoids "unused parameter" DDP errors)."""
        nonlocal zero_feature
        if idx_mask.any():
            hs_sel = flat[idx_mask]
            for i, fn in enumerate(funcs):
                out_flat[i][idx_mask] = fn(hs_sel)
        else:
            for fn in funcs:
                zero_feature = zero_feature + fn(placeholder).mean() * 0

    # Text: mask == 0
    text_idx = (mask_flat == 0)
    if valid_flat is not None:
        text_idx = text_idx & valid_flat
    _dispatch(text_idx, text_funcs)

    # Vision input: mask == 1
    vis_idx = (mask_flat == 1)
    if valid_flat is not None:
        vis_idx = vis_idx & valid_flat
    _dispatch(vis_idx, vision_funcs)

    # Generation: mask == 2
    gen_idx = (mask_flat == 2)
    if valid_flat is not None:
        gen_idx = gen_idx & valid_flat
    _dispatch(gen_idx, gen_funcs)

    result = [out.view(bsz, seq_len, -1) for out in out_flat]
    result[0] = result[0] + zero_feature
    return result


# ---------------------------------------------------------------------------
# Decoder layer subclass
# ---------------------------------------------------------------------------

def _make_g_config(config, mlp_g_intermediate_size: int):
    """Clone config with a wider intermediate_size for mlp_g (Net2Wider).

    MUST be `copy.deepcopy`, NOT `copy.copy`. Reason:
      HunYuanVLMoTConfig.__setattr__ is a proxy that re-routes any attribute
      writes (when the key is in text_config.__dict__) into self.text_config.
      Shallow-copying the outer config keeps `g_cfg.text_config is
      config.text_config` โ€” same object โ€” so the subsequent
      `g_cfg.intermediate_size = ...` setattr ends up mutating the SHARED
      text_config used by every other decoder layer's mlp/mlp_v construction.
      Layer 0 escapes (its mlp/mlp_v are built before _make_g_config runs),
      but layer 1+ then sees text_config.intermediate_size = 12288 and builds
      mlp/mlp_v at the wider size โ€” at that point ckpt weights (which are 6144
      for mlp/mlp_v) no longer match and `from_pretrained` fails with size
      mismatch. Deepcopy the whole config tree to break this aliasing.
    """
    g_cfg = copy.deepcopy(config)
    g_cfg.intermediate_size = int(mlp_g_intermediate_size)
    return g_cfg


class MoTDecoderLayer(HunYuanVLMoTDecoderLayer):
    """Three-path decoder layer: text / vision-input / vision-gen.

    Adds `mlp_g`, `input_layernorm_g`, `post_attention_layernorm_g` on top of
    upstream's two-path layer. `modality_mask` is interpreted as int{0,1,2}
    instead of bool.
    """

    def __init__(self, config, layer_idx: int):
        super().__init__(config, layer_idx)

        # Text-config-aware lookup (HunYuanVLMoTConfig proxies these to text_config)
        intermediate_size = getattr(config, "intermediate_size", None)
        mlp_g_inter = getattr(config, "mlp_g_intermediate_size", None)

        if mlp_g_inter is None or intermediate_size is None or mlp_g_inter == intermediate_size:
            g_cfg = config
        else:
            assert mlp_g_inter >= intermediate_size, (
                f"mlp_g_intermediate_size ({mlp_g_inter}) must be >= "
                f"intermediate_size ({intermediate_size}); shrinking is not supported."
            )
            g_cfg = _make_g_config(config, mlp_g_inter)

        self.mlp_g = HunYuanVLMoTMLP(g_cfg)
        self.input_layernorm_g = HunYuanVLMoTRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
        self.post_attention_layernorm_g = HunYuanVLMoTRMSNorm(
            config.hidden_size, eps=config.rms_norm_eps
        )

    @deprecate_kwarg("past_key_value", new_name="past_key_values", version="4.58")
    def forward(
        self,
        hidden_states: torch.Tensor,
        attention_mask: Optional[torch.Tensor] = None,
        position_ids: Optional[torch.LongTensor] = None,
        past_key_values: Optional[Cache] = None,
        use_cache: Optional[bool] = False,
        cache_position: Optional[torch.LongTensor] = None,
        position_embeddings: Optional[tuple[torch.Tensor, torch.Tensor]] = None,
        modality_mask: Optional[torch.Tensor] = None,
        **kwargs: Unpack[TransformersKwargs],
    ) -> torch.Tensor:
        # Pull padding_mask from attention_mask dict for GEMM exclusion
        padding_mask = None
        if isinstance(attention_mask, dict):
            pm = attention_mask.get("padding_mask", None)
            if pm is not None and hidden_states.shape[1] == pm.shape[1]:
                padding_mask = pm

        # Convert int modality_mask to bool for attention QKVO routing
        # (attention has only 2 paths: text vs vision; gen tokens go through _v)
        attn_modality_mask = None
        if modality_mask is not None:
            attn_modality_mask = (modality_mask > 0)

        residual = hidden_states

        # Pre-attention LayerNorm โ€” three paths
        hidden_states = mask_apply_3way(
            hidden_states, modality_mask,
            [self.input_layernorm],
            [self.input_layernorm_v],
            [self.input_layernorm_g],
            padding_mask=padding_mask,
        )[0]

        # Self-attention (upstream's two-path attention with bool mask)
        hidden_states, _ = self.self_attn(
            hidden_states=hidden_states,
            attention_mask=attention_mask,
            position_ids=position_ids,
            past_key_values=past_key_values,
            use_cache=use_cache,
            cache_position=cache_position,
            position_embeddings=position_embeddings,
            modality_mask=attn_modality_mask,
            **kwargs,
        )
        hidden_states = residual + hidden_states

        # MLP โ€” three paths (LayerNorm + MLP fused per path to match upstream layout)
        residual = hidden_states
        hidden_states = mask_apply_3way(
            hidden_states, modality_mask,
            [lambda x: self.mlp(self.post_attention_layernorm(x))],
            [lambda x: self.mlp_v(self.post_attention_layernorm_v(x))],
            [lambda x: self.mlp_g(self.post_attention_layernorm_g(x))],
            padding_mask=padding_mask,
        )[0]
        hidden_states = residual + hidden_states

        # Zero out padding positions to keep residuals clean (matches upstream behavior)
        if padding_mask is not None and hidden_states.shape[1] == padding_mask.shape[1]:
            hidden_states = hidden_states * padding_mask.unsqueeze(-1)

        return hidden_states


__all__ = ["MoTDecoderLayer", "mask_apply_3way"]