File size: 10,345 Bytes
cfbcb75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Host (torch, CPU) side of the DiffRhythm 1.2 DiT port to one Tenstorrent P100a.

Upstream (DiffRhythm model/dit.py DiT.forward) as used by CFM.sample, per call:
  c = time_embed(t) + start_time_embed(start) + duration_time_embed(duration)                    [512]
  text_embed = TextEmbedding(text)  (embedding + sinusoid + 4 ConvNeXtV2 blocks; zeros in for drop_text)  [S, 512]
  h = proj(cat(x, cond, text_embed, style, c))  -> [S, 2048];  h = ConvPos(h) + h   (two k31 groups-16 convs + Mish)
  16 x LlamaDecoderLayer (RMSNorm, 32 x 64 heads, RoPE theta 1e4, SwiGLU 8192; no mask, bidirectional);
       after each of the first 8: h += silu(text_fusion_linears[i](text_embed))
  out = proj_out(LayerNorm(h) * (1 + scale) + shift),  scale, shift = linear(silu(c)).chunk(2)
The CFG branch (drop_audio_cond + drop_text) zeroes cond and the text tokens and uses the negative style.

Split: the timestep-invariant text embedding and the small time MLPs stay in the DiffRhythm process on the host
(upstream modules, exact); everything weight-heavy runs on the card. The input projection is split by input:
  h = x @ Wx + [cond @ Wcond + text_embed @ Wtext]  (per song and branch)  + [style @ Ws + c @ Wc + b]  (per call row)
RefDecoder runs this formulation in float32 so it can be checked against upstream without a device.
"""
from __future__ import annotations

from dataclasses import dataclass
from typing import Dict

import torch

from .common import (NEG, TILE, apply_rope_adjacent, interleave_pairs_permutation, linear_to_mm,
                     permute_heads_rows, rms, round_up, swiglu_interleave)

SEQ_BUCKET = 256  # frames padded to multiples of this: 96-285 s songs (2067-6136 frames) -> 16 shapes
LATENT_RATE = 44100 / 2048  # latent frames per second (get_lrc_token: int(seconds * 44100 / 2048))
MIN_FRAMES, MAX_FRAMES = int(96 * LATENT_RATE), int(285 * LATENT_RATE)  # the 96-285 s the port was exercised over


def seq_buckets():
    """Every padded length a DiffRhythm song can have (warm_cache.py compiles them ahead of time)."""
    return sorted({round_up(n, SEQ_BUCKET) for n in range(MIN_FRAMES, MAX_FRAMES + 1)})


@dataclass(frozen=True)
class Config:
    dim: int = 2048
    depth: int = 16
    heads: int = 32
    head_dim: int = 64
    intermediate: int = 8192
    text_dim: int = 512
    cond_dim: int = 512
    mel_dim: int = 64
    fusion_layers: int = 8
    conv_kernel: int = 31
    conv_groups: int = 16
    rope_theta: float = 10000.0
    eps: float = 1e-6


class Checkpoint:
    """transformer.* of DiffRhythm's cfm_model.pt (fp16 on disk), read as float32."""

    def __init__(self, path):
        sd = torch.load(str(path), weights_only=True, map_location="cpu")["model_state_dict"]
        self._sd = {k[len("transformer."):]: v for k, v in sd.items() if k.startswith("transformer.")}
        self.cfg = Config()

    def get(self, name: str) -> torch.Tensor:
        return self._sd[name].float()


@dataclass
class LayerWeights:
    wqkv: torch.Tensor  # [2048, 3 * 2048], q/k rows permuted to adjacent pairs
    wo: torch.Tensor
    w_gateup: torch.Tensor  # [2048, 2 * 8192] tile-interleaved
    w_down: torch.Tensor
    ln1: torch.Tensor
    ln2: torch.Tensor


def layer_weights(ckpt: Checkpoint, i: int) -> LayerWeights:
    c = ckpt.cfg
    g = lambda k: ckpt.get(f"transformer_blocks.{i}.{k}")
    perm = interleave_pairs_permutation(c.head_dim)
    wq = permute_heads_rows(g("self_attn.q_proj.weight"), c.heads, c.head_dim, perm)
    wk = permute_heads_rows(g("self_attn.k_proj.weight"), c.heads, c.head_dim, perm)
    return LayerWeights(
        wqkv=torch.cat([linear_to_mm(wq), linear_to_mm(wk), linear_to_mm(g("self_attn.v_proj.weight"))], dim=1),
        wo=linear_to_mm(g("self_attn.o_proj.weight")),
        w_gateup=swiglu_interleave(g("mlp.gate_proj.weight"), g("mlp.up_proj.weight")),
        w_down=linear_to_mm(g("mlp.down_proj.weight")),
        ln1=g("input_layernorm.weight"), ln2=g("post_attention_layernorm.weight"))


@dataclass
class InputWeights:
    """input_embed.proj split by input (matmul layout [in, out])."""

    wx: torch.Tensor  # [64, 2048]
    wcond: torch.Tensor  # [64, 2048]
    wtext: torch.Tensor  # [512, 2048]
    wstyle: torch.Tensor  # [512, 2048]
    wc: torch.Tensor  # [512, 2048]
    b: torch.Tensor


def input_weights(ckpt: Checkpoint) -> InputWeights:
    c = ckpt.cfg
    w = linear_to_mm(ckpt.get("input_embed.proj.weight"))  # [1664, 2048]
    edges = [0, c.mel_dim, 2 * c.mel_dim, 2 * c.mel_dim + c.text_dim, 2 * c.mel_dim + c.text_dim + c.cond_dim]
    parts = [w[a:b] for a, b in zip(edges, edges[1:] + [w.shape[0]])]
    return InputWeights(*[p.contiguous() for p in parts], ckpt.get("input_embed.proj.bias"))


@dataclass(frozen=True)
class Geometry:
    frames: int

    @property
    def seq_pad(self) -> int:
        return round_up(self.frames, SEQ_BUCKET)


def key_mask(geo: Geometry) -> torch.Tensor:
    """Additive [S_pad, S_pad] mask excluding padded keys (upstream attends over exactly S frames)."""
    valid = torch.arange(geo.seq_pad)[None, :] < geo.frames
    return torch.where(valid, 0.0, NEG).expand(geo.seq_pad, -1).contiguous()


def row_mask(geo: Geometry) -> torch.Tensor:
    """[S_pad, 1]: 1 on real frames, 0 on padding (the position convs must see zeros past the song, like upstream)."""
    return (torch.arange(geo.seq_pad) < geo.frames).float()[:, None]


def rope_tables(seq_pad: int, cfg: Config, dtype=torch.float32):
    inv = 1.0 / (cfg.rope_theta ** (torch.arange(0, cfg.head_dim, 2, dtype=torch.float64) / cfg.head_dim))
    ang = torch.outer(torch.arange(seq_pad, dtype=torch.float64), inv).float()
    return torch.cos(ang).repeat_interleave(2, -1).to(dtype), torch.sin(ang).repeat_interleave(2, -1).to(dtype)


def pad_rows(t: torch.Tensor, rows: int) -> torch.Tensor:
    out = torch.zeros(rows, t.shape[-1], dtype=t.dtype)
    out[: t.shape[0]] = t
    return out


@dataclass
class CallRows:
    """What changes per call: the style/time part of the input projection and the output modulation."""

    input_row: torch.Tensor  # [2048]
    out_scale: torch.Tensor  # [2048]
    out_shift: torch.Tensor  # [2048]


def call_rows(ckpt_or_iw, norm_linear_w, norm_linear_b, style: torch.Tensor, c: torch.Tensor) -> CallRows:
    """style [512], c [512] (time + start + duration embeddings, from upstream on the host)."""
    iw = ckpt_or_iw
    input_row = style @ iw.wstyle + c @ iw.wc + iw.b
    emb = torch.nn.functional.silu(c) @ norm_linear_w.t() + norm_linear_b
    scale, shift = emb.chunk(2, dim=-1)
    return CallRows(input_row, scale, shift)


class RefDecoder:
    """float32 CPU DiT in the TT formulation."""

    def __init__(self, ckpt: Checkpoint):
        self.ckpt, self.cfg = ckpt, ckpt.cfg
        c = self.cfg
        self.layers = [layer_weights(ckpt, i) for i in range(c.depth)]
        self.iw = input_weights(ckpt)
        self.conv = [(ckpt.get(f"input_embed.conv_pos_embed.conv1d.{i}.weight"),
                      ckpt.get(f"input_embed.conv_pos_embed.conv1d.{i}.bias")) for i in (0, 2)]
        self.fusion = [(linear_to_mm(ckpt.get(f"text_fusion_linears.{i}.0.weight")),
                        ckpt.get(f"text_fusion_linears.{i}.0.bias")) for i in range(c.fusion_layers)]
        self.w_out, self.b_out = linear_to_mm(ckpt.get("proj_out.weight")), ckpt.get("proj_out.bias")
        self.norm_w, self.norm_b = ckpt.get("norm_out.linear.weight"), ckpt.get("norm_out.linear.bias")

    def rows(self, style: torch.Tensor, c: torch.Tensor) -> CallRows:
        return call_rows(self.iw, self.norm_w, self.norm_b, style, c)

    def branch(self, text_embed: torch.Tensor, cond, geo: Geometry) -> Dict[str, object]:
        """Per song and CFG branch: [cond @ Wcond + text @ Wtext] and the 8 text-fusion residuals, padded."""
        t = pad_rows(text_embed.float(), geo.seq_pad)
        base = t @ self.iw.wtext
        if cond is not None:
            base = base + pad_rows(cond.float(), geo.seq_pad) @ self.iw.wcond
        fusion = [torch.nn.functional.silu(t @ w + b) for w, b in self.fusion]
        return {"base": base, "fusion": fusion}

    def conv_pos(self, h: torch.Tensor, mask: torch.Tensor = None) -> torch.Tensor:
        """Two k31 convs with Mish. Upstream's sequence ends at the song, so each conv sees zeros past it: the rows
        past the song are zeroed before every conv (the first conv's Mish output is not zero there)."""
        c = self.cfg
        y = h
        for w, b in self.conv:
            if mask is not None:
                y = y * mask
            y = torch.nn.functional.mish(torch.nn.functional.conv1d(y.t()[None], w, b, padding=c.conv_kernel // 2,
                                                                    groups=c.conv_groups))[0].t()
        return y

    def forward(self, x_t: torch.Tensor, branch: Dict[str, object], rows: CallRows, geo: Geometry, taps: list = None):
        """[S, 64] noisy latents -> [S, 64] prediction."""
        c = self.cfg
        mask = row_mask(geo)
        h = (pad_rows(x_t.float(), geo.seq_pad) @ self.iw.wx + branch["base"] + rows.input_row) * mask
        h = self.conv_pos(h, mask) + h
        cos, sin = rope_tables(geo.seq_pad, c)
        kmask = key_mask(geo)
        for i, w in enumerate(self.layers):
            a = rms(h, w.ln1, c.eps)
            q, k, v = (a @ w.wqkv).split(c.dim, -1)
            q = apply_rope_adjacent(q.view(-1, c.heads, c.head_dim).transpose(0, 1), cos, sin)
            k = apply_rope_adjacent(k.view(-1, c.heads, c.head_dim).transpose(0, 1), cos, sin)
            v = v.view(-1, c.heads, c.head_dim).transpose(0, 1)
            scores = q @ k.transpose(-1, -2) * c.head_dim ** -0.5 + kmask
            att = (torch.softmax(scores, -1) @ v).transpose(0, 1).reshape(geo.seq_pad, -1)
            h = h + att @ w.wo
            m = rms(h, w.ln2, c.eps)
            gu = (m @ w.w_gateup).view(geo.seq_pad, -1, 2, TILE)
            h = h + (torch.nn.functional.silu(gu[:, :, 0]) * gu[:, :, 1]).reshape(geo.seq_pad, -1) @ w.w_down
            if i < c.fusion_layers:
                h = h + branch["fusion"][i]
            if taps is not None:
                taps.append(h.clone())
        h = torch.nn.functional.layer_norm(h, (c.dim,), eps=c.eps) * (1 + rows.out_scale) + rows.out_shift
        return (h @ self.w_out + self.b_out)[: geo.frames]