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]
|