File size: 12,529 Bytes
5f18a20
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Explicit padding-safe native Matcha, with unchanged learned state layout.

``create(config)`` accepts the unchanged capacity-192 parent configuration.
Callers must record ARCHITECTURE_ID separately in branch/checkpoint metadata:
identical state keys do not identify the changed forward contract. In particular,
load the CONTROL129000 no-adapter parent strictly, not a rich-text adapter state.
The returned MatchaTTS retains its native ``forward`` and ``synthesise`` methods.
No trainer, frontend, optimizer, dropout, vocoder, or global forward hooks change.

Only PyTorch is required to import the numerical helpers. ``native_types`` and
``create`` additionally require the installed native Matcha dependency stack.
"""
from functools import lru_cache, partial
from pathlib import Path
import sys

import torch
from torch import nn


ARCHITECTURE_ID = "inflect-v3mini-matcha-padding-safe-v1"
ACOUSTIC_PARAMETERS = 10_780_425


def boolean_sequence_mask(mask, x, *, prefix=False):
    """Validate Bx1xT binary validity; True means an allowed attention key.

    Additive masks (including 0/-inf) are deliberately rejected, not cast to bool.
    Every example must contain at least one valid frame.
    """
    if x.ndim != 3 or x.shape[0] == 0 or x.shape[-1] == 0:
        raise ValueError("Expected a nonempty BxCxT tensor")
    if mask.shape != (x.shape[0], 1, x.shape[-1]) or mask.device != x.device:
        raise ValueError("Mask must have shape Bx1xT on the input device")
    if not torch.all((mask == 0) | (mask == 1)):
        raise ValueError("Mask must be finite binary validity, not additive attention bias")
    valid = mask.bool()
    if not valid.any(dim=-1).all():
        raise ValueError("Every example must contain a nonempty valid sequence")
    if prefix and torch.any(valid[..., 1:] & ~valid[..., :-1]):
        raise ValueError("Decoder masks must be right-padded contiguous prefixes")
    return valid


def _zero_padding(x, valid):
    # Multiplication by zero does not remove NaN/Inf padding.
    return x.masked_fill(~valid, 0)


def _group_norm(x, valid, norm):
    b, c, length = x.shape
    if c != norm.num_channels:
        raise ValueError("GroupNorm channel count differs from input")
    # Match native accumulation precision under autocast without lowering FP64.
    work = _zero_padding(x, valid)
    if work.dtype in (torch.float16, torch.bfloat16):
        work = work.float()
    grouped = work.reshape(b, norm.num_groups, c // norm.num_groups, length)
    weights = valid.unsqueeze(1)
    count = weights.sum((2, 3), keepdim=True).to(work.dtype) * (c // norm.num_groups)
    mean = grouped.sum((2, 3), keepdim=True) / count
    centered = (grouped - mean).masked_fill(~weights, 0)
    variance = centered.square().sum((2, 3), keepdim=True) / count
    output = (centered * torch.rsqrt(variance + norm.eps)).reshape(b, c, length)
    if norm.affine:
        output = output * norm.weight.to(work.dtype)[None, :, None]
        output = output + norm.bias.to(work.dtype)[None, :, None]
    return _zero_padding(output.to(x.dtype), valid)


def valid_frame_group_norm(x, mask, norm):
    """Biased GroupNorm statistics over valid time and channels within each group.

    Uses the supplied native nn.GroupNorm's eps and learned affine tensors;
    registers no parameters/buffers. Invalid outputs and input gradients are zero.
    """
    return _group_norm(x, boolean_sequence_mask(mask, x), norm)


def masked_flow_loss(prediction, target, mask):
    """CFM squared error divided by valid frames times mel channels, not padding."""
    if prediction.shape != target.shape:
        raise ValueError("Flow prediction and target shapes must match")
    valid = boolean_sequence_mask(mask, prediction)
    prediction, target = _zero_padding(prediction, valid), _zero_padding(target, valid)
    if prediction.dtype in (torch.float16, torch.bfloat16):
        prediction, target = prediction.float(), target.float()
    error = prediction - target
    return error.square().sum() / (valid.sum() * target.shape[1])


@lru_cache(maxsize=1)
def native_decoder_types():
    """Return (PaddingSafeBlock1D, PaddingSafeDecoder) lazily.

    Constructors run the vendor constructors first. Replacement initialization is
    RNG-neutral and strictly copies state, retaining registration order and names.
    No vendor classes, existing models, or attention processors are patched.
    """
    vendor = str(Path(__file__).resolve().parents[1] / "external/Matcha-TTS-primary")
    if vendor not in sys.path:
        sys.path.insert(0, vendor)
    from matcha.models.components.decoder import Block1D, Decoder

    class PaddingSafeBlock1D(Block1D):
        def forward(self, x, mask):
            return self._forward_validated(x, boolean_sequence_mask(mask, x))

        def _forward_validated(self, x, valid):
            h = self.block[0](_zero_padding(x, valid))
            return _zero_padding(self.block[2](_group_norm(h, valid, self.block[1])), valid)

    class PaddingSafeDecoder(Decoder):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            from matcha.models.components.transformer import BasicTransformerBlock

            for stage in (self.down_blocks, self.mid_blocks, self.up_blocks):
                for parts in stage:
                    if any(not isinstance(block, BasicTransformerBlock) for block in parts[1]):
                        raise ValueError("Padding-safe variant supports native transformer blocks only")
            for module in list(self.modules()):
                for name, child in list(module.named_children()):
                    if isinstance(child, Block1D):
                        conv, norm = child.block[:2]
                        with torch.random.fork_rng(devices=[]):
                            replacement = PaddingSafeBlock1D(
                                conv.in_channels, conv.out_channels, groups=norm.num_groups)
                        replacement.load_state_dict(child.state_dict(), strict=True)
                        setattr(module, name, replacement)

        @staticmethod
        def _resnet(resnet, x, valid, t):
            # Same native ResnetBlock1D operations, but the decoder has already
            # validated this mask. Avoid per-Block1D device-to-host reductions.
            x = _zero_padding(x, valid)
            h = resnet.block1._forward_validated(x, valid)
            h += resnet.mlp(t).unsqueeze(-1)
            h = resnet.block2._forward_validated(h, valid)
            return h + resnet.res_conv(x)

        @staticmethod
        def _attention(x, blocks, valid, t):
            x = _zero_padding(x, valid).transpose(1, 2)
            # Explicit singleton query axis: Diffusers expands heads and SDPA
            # broadcasts queries. A BxT mask alone can misbroadcast for B > 1.
            for block in blocks:
                x = block(hidden_states=x, attention_mask=valid, timestep=t)
            return _zero_padding(x.transpose(1, 2), valid)

        def forward(self, x, mask, mu, t, spks=None, cond=None):
            valid = boolean_sequence_mask(mask, x, prefix=True)
            if mu.shape != x.shape:
                raise ValueError("Flow input and text mean shapes must match")
            if cond is not None:
                raise ValueError("Native padding-safe decoder has no adapter conditioning")
            x, mu = _zero_padding(x, valid), _zero_padding(mu, valid)
            if not torch.isfinite(x).all() or not torch.isfinite(mu).all():
                raise ValueError("Valid flow input and text mean must be finite")
            t = self.time_mlp(self.time_embeddings(t))
            x = torch.cat((x, mu), dim=1)
            if spks is not None:
                x = torch.cat((x, spks.unsqueeze(-1).expand(-1, -1, x.shape[-1])), dim=1)

            hiddens, masks = [], []
            current = valid
            for index, (resnet, blocks, downsample) in enumerate(self.down_blocks):
                x = self._resnet(resnet, x, current, t)
                x = self._attention(x, blocks, current, t)
                hiddens.append(x)
                masks.append(current)
                x = downsample(x)
                # Native's final down block is stride one, not a downsample.
                if index < len(self.down_blocks) - 1:
                    current = current[..., ::2]
                x = _zero_padding(x, current)

            for resnet, blocks in self.mid_blocks:
                x = self._resnet(resnet, x, current, t)
                x = self._attention(x, blocks, current, t)

            for resnet, blocks, upsample in self.up_blocks:
                current, skip = masks.pop(), hiddens.pop()
                # Stride-two transposed convolutions produce one extra right
                # frame for odd skip lengths. Crop before the next convolution.
                x = x[..., :skip.shape[-1]]
                x = self._resnet(resnet, torch.cat((x, skip), dim=1), current, t)
                x = self._attention(x, blocks, current, t)
                x = upsample(x)

            x = self.final_block._forward_validated(x, valid)
            return _zero_padding(self.final_proj(x), valid)

    return PaddingSafeBlock1D, PaddingSafeDecoder


@lru_cache(maxsize=1)
def native_types():
    """Return decoder types plus PaddingSafeCFM with native training dependencies."""
    PaddingSafeBlock1D, PaddingSafeDecoder = native_decoder_types()
    from matcha.models.components.flow_matching import CFM

    class PaddingSafeCFM(CFM):
        def __init__(self, in_channels, out_channel, cfm_params, decoder_params,
                     n_spks=1, spk_emb_dim=64):
            super().__init__(in_channels, out_channel, cfm_params, decoder_params, n_spks, spk_emb_dim)
            with torch.random.fork_rng(devices=[]):
                estimator = PaddingSafeDecoder(
                    in_channels=in_channels + (spk_emb_dim if n_spks > 1 else 0),
                    out_channels=out_channel, **decoder_params)
            estimator.load_state_dict(self.estimator.state_dict(), strict=True)
            self.estimator = estimator

        def compute_loss(self, x1, mask, mu, spks=None, cond=None):
            valid = boolean_sequence_mask(mask, x1, prefix=True)
            if mu.shape != x1.shape:
                raise ValueError("Flow target and text mean shapes must match")
            x1 = _zero_padding(x1, valid)
            if not torch.isfinite(x1).all():
                raise ValueError("Valid flow target must be finite")
            t = torch.rand([mu.shape[0], 1, 1], device=mu.device, dtype=mu.dtype)
            z = torch.randn_like(x1)
            y = (1 - (1 - self.sigma_min) * t) * z + t * x1
            u = x1 - (1 - self.sigma_min) * z
            prediction = self.estimator(y, valid, mu, t.squeeze(), spks, cond)
            return masked_flow_loss(prediction, u, valid), y

    return PaddingSafeBlock1D, PaddingSafeDecoder, PaddingSafeCFM


def create(config):
    """Build FP32 native-compatible acoustic model; strict state_dict loading only.

    Config is the unchanged capacity-192/full-context config, including its parent
    architecture marker. Persist ARCHITECTURE_ID outside it to distinguish this
    experimental forward contract. This function does not load a checkpoint or
    create/restore an optimizer; native optimizer configuration is preserved.
    """
    from scripts import train_v3mini_unet_capacity_192 as native

    values = native.native_config(config)
    _, _, PaddingSafeCFM = native_types()
    from omegaconf import OmegaConf
    from matcha.models.matcha_tts import MatchaTTS

    for key in ("encoder", "decoder", "cfm"):
        values[key] = OmegaConf.create(values[key])
    model = MatchaTTS(**values, optimizer=partial(torch.optim.Adam, lr=1e-4))
    original = model.decoder
    with torch.random.fork_rng(devices=[]):
        replacement = PaddingSafeCFM(
            2 * values["encoder"].encoder_params.n_feats,
            values["encoder"].encoder_params.n_feats,
            values["cfm"], values["decoder"], values["n_spks"], values["spk_emb_dim"])
    replacement.load_state_dict(original.state_dict(), strict=True)
    model.decoder = replacement
    model.architecture_id = ARCHITECTURE_ID
    model.float()
    if sum(p.numel() for p in model.parameters()) != ACOUSTIC_PARAMETERS:
        raise ValueError("Padding-safe acoustic parameter count must be exactly 10,780,425")
    native.parameter_budget(model)
    return model