File size: 13,389 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
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
# SPDX-License-Identifier: Apache-2.0
"""Oobleck audio VAE decoders on ONE Blackhole (P100a), TTNN: ACE-Step's (diffusers AutoencoderOobleck, 48 kHz
stereo, strides 10, 6, 4, 4, 2) and DiffRhythm's (stable-audio-tools, TorchScript, 44.1 kHz stereo, strides
8, 8, 4, 4, 2). Same architecture, different weight files:

  conv1 (64 -> 2048, k7)
  5 x block: Snake -> ConvTranspose1d (k = 2s, stride s) -> 3 x residual unit (dilation 1, 3, 9):
             x + conv_k1(Snake(conv_k7_dilated(Snake(x))))
  Snake -> conv2 (128 -> 2, k7, no bias)
Snake(x) = x + 1 / (exp(beta) + 1e-9) * sin(exp(alpha) * x)^2 per channel. Weight norm is folded on the host
(diffusers) or already folded (the TorchScript export).

Activations are [1, 1, L, C] (channels last), as ttnn.conv1d / conv_transpose2d (height 1) expect.
"""
from __future__ import annotations

import json
import math
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional

import torch
import ttnn

MEM = ttnn.DRAM_MEMORY_CONFIG


def fold_weight_norm(g: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
    """torch weight_norm(dim=0): w = g * v / ||v|| over every dim but the first."""
    norm = v.flatten(1).norm(dim=1).view(-1, *([1] * (v.dim() - 1)))
    return g * v / norm


class VaeWeights:
    """decoder.* of checkpoints/vae, float32, weight norm folded."""

    def __init__(self, vae_dir):
        from safetensors import safe_open

        self.dir = Path(vae_dir)
        self.config = json.loads((self.dir / "config.json").read_text())
        self._f = safe_open(str(self.dir / "diffusion_pytorch_model.safetensors"), framework="pt")
        self.keys = set(self._f.keys())

    def get(self, name):
        return self._f.get_tensor(f"decoder.{name}").float()

    def conv(self, name):
        w = fold_weight_norm(self.get(f"{name}.weight_g"), self.get(f"{name}.weight_v"))
        b = self.get(f"{name}.bias") if f"decoder.{name}.bias" in self.keys else None
        return w, b

    def snake(self, name):
        alpha = torch.exp(self.get(f"{name}.alpha")).flatten()
        inv_beta = 1.0 / (torch.exp(self.get(f"{name}.beta")).flatten() + 1e-9)
        return alpha, inv_beta


class StableAudioVaeWeights:
    """decoder.* of DiffRhythm's vae_model.pt (a stable-audio-tools OobleckDecoder exported with TorchScript, weight
    norm already folded), under VaeWeights' diffusers names."""

    def __init__(self, path):
        sd = torch.jit.load(str(path), map_location="cpu").state_dict()
        self._sd = {k[len("decoder."):]: v for k, v in sd.items() if k.startswith("decoder.")}
        strides = [self._sd[f"layers.{i + 1}.layers.1.weight"].shape[-1] // 2 for i in range(5)]
        self.config = {"downsampling_ratios": strides}

    @staticmethod
    def _key(name):
        parts = name.split(".")
        if name == "conv1":
            return "layers.0"
        if name == "snake1":
            return "layers.6"
        if name == "conv2":
            return "layers.7"
        block = f"layers.{int(parts[1]) + 1}"
        if parts[2] == "snake1":
            return f"{block}.layers.0"
        if parts[2] == "conv_t1":
            return f"{block}.layers.1"
        unit = f"{block}.layers.{int(parts[2][len('res_unit'):]) + 1}"
        return f"{unit}.layers.{ {'snake1': 0, 'conv1': 1, 'snake2': 2, 'conv2': 3}[parts[3]] }"

    def conv(self, name):
        k = self._key(name)
        return self._sd[f"{k}.weight"].float(), (self._sd[f"{k}.bias"].float() if f"{k}.bias" in self._sd else None)

    def snake(self, name):
        k = self._key(name)
        alpha = torch.exp(self._sd[f"{k}.alpha"].float()).flatten()
        inv_beta = 1.0 / (torch.exp(self._sd[f"{k}.beta"].float()).flatten() + 1e-9)
        return alpha, inv_beta


def vae_weights(path):
    """A diffusers VAE directory (ACE-Step) or a stable-audio TorchScript file (DiffRhythm)."""
    path = Path(path)
    if path.is_dir() and (path / "config.json").exists():
        return VaeWeights(path)
    return StableAudioVaeWeights(path / "vae_model.pt" if path.is_dir() else path)


@dataclass
class Conv:
    weight: object  # ttnn host tensor, replaced by the prepared device tensor after the first call
    bias: object
    in_ch: int
    out_ch: int
    kernel: int
    pad: int
    dilation: int = 1
    stride: int = 1
    transpose: bool = False


@dataclass
class Snake:
    alpha: ttnn.Tensor
    inv_beta: ttnn.Tensor


@dataclass
class ResUnit:
    snake1: Snake
    conv1: Conv
    snake2: Snake
    conv2: Conv


@dataclass
class Block:
    snake: Snake
    up: Conv
    units: List[ResUnit]


@dataclass
class Precision:
    fidelity: ttnn.MathFidelity = ttnn.MathFidelity.HiFi4  # HiFi2/LoFi are no faster here: the convs are not math-bound
    act_dtype: ttnn.DataType = ttnn.bfloat16
    # vae_conv_sweep.py: activation blocks of 64 rows make the 48 kHz k7 convs 4.9x and the output conv 6x faster
    # than the default block height, same output; double buffering and bfp8 weights were slower. Applied to the
    # layers with <= 256 channels (the long, late ones); the wide early layers keep the default (the override fails
    # or is slower there).
    act_block_h: int = 64
    transpose_act_block_h: int = 64
    act_block_h_max_channels: int = 256
    config_tensors_in_dram: bool = True
    fused_snake: bool = True


class OobleckTT:
    def __init__(self, dev, vae_path, prec: Optional[Precision] = None):
        self.dev = dev
        self.prec = prec or Precision()
        w = vae_weights(vae_path)
        self.upsample = math.prod(w.config["downsampling_ratios"])
        self.conv1 = self._conv(*w.conv("conv1"), pad=3)
        self.blocks = []
        for i in range(len(w.config["downsampling_ratios"])):
            p = f"block.{i}"
            wt, bt = w.conv(f"{p}.conv_t1")  # ConvTranspose1d weight [in, out, 2s]
            stride = wt.shape[-1] // 2
            units = []
            for j, dilation in enumerate((1, 3, 9)):
                u = f"{p}.res_unit{j + 1}"
                units.append(ResUnit(self._snake(*w.snake(f"{u}.snake1")),
                                     self._conv(*w.conv(f"{u}.conv1"), pad=3 * dilation, dilation=dilation),
                                     self._snake(*w.snake(f"{u}.snake2")), self._conv(*w.conv(f"{u}.conv2"), pad=0)))
            self.blocks.append(Block(self._snake(*w.snake(f"{p}.snake1")),
                                     self._conv(wt, bt, pad=math.ceil(stride / 2), stride=stride, transpose=True), units))
        self.snake_out = self._snake(*w.snake("snake1"))
        self.conv2 = self._conv(*w.conv("conv2"), pad=3)
        self.ck = ttnn.init_device_compute_kernel_config(dev.arch(), math_fidelity=self.prec.fidelity,
                                                         math_approx_mode=False, fp32_dest_acc_en=True,
                                                         packer_l1_acc=False)
        self.profile = None  # a list to record (where, op, length, channels, seconds) per op, synchronizing each
        self._where = ""

    def _timed(self, op, length, channels, fn, *args):
        if self.profile is None:
            return fn(*args)
        import time

        ttnn.synchronize_device(self.dev)
        started = time.perf_counter()
        out = fn(*args)
        ttnn.synchronize_device(self.dev)
        self.profile.append((self._where, op, length, channels, time.perf_counter() - started))
        return out

    # ------------------------------------------------------------------ weights
    def _conv(self, w, b, pad, dilation=1, stride=1, transpose=False):
        if transpose:  # [in, out, k] -> [in, out, 1, k]
            in_ch, out_ch, k = w.shape
        else:  # [out, in, k] -> [out, in, 1, k]
            out_ch, in_ch, k = w.shape
        weight = ttnn.from_torch(w.unsqueeze(2).contiguous(), dtype=ttnn.float32)
        bias = ttnn.from_torch(b.reshape(1, 1, 1, -1), dtype=ttnn.float32) if b is not None else None
        return Conv(weight, bias, in_ch, out_ch, k, pad, dilation, stride, transpose)

    def _snake(self, alpha, inv_beta):
        row = lambda v: ttnn.from_torch(v.float().reshape(1, 1, 1, -1), dtype=self.prec.act_dtype,
                                        layout=ttnn.TILE_LAYOUT, device=self.dev, memory_config=MEM)
        return Snake(row(alpha), row(inv_beta))

    # ------------------------------------------------------------------ ops
    def _run_conv(self, x, c: Conv, length: int):
        block_h = self.prec.transpose_act_block_h if c.transpose else self.prec.act_block_h
        if max(c.in_ch, c.out_ch) > self.prec.act_block_h_max_channels:
            block_h = 0
        # smaller activation blocks need larger per-core config tensors; keep them in DRAM, not the 96 KiB L1_SMALL
        cfg = ttnn.Conv2dConfig(weights_dtype=ttnn.bfloat16, config_tensors_in_dram=self.prec.config_tensors_in_dram)
        if block_h:
            cfg.act_block_h_override = block_h
        common = dict(device=self.dev, in_channels=c.in_ch, out_channels=c.out_ch, batch_size=1, bias_tensor=c.bias,
                      dtype=self.prec.act_dtype, compute_config=self.ck, conv_config=cfg,
                      return_weights_and_bias=True)
        if c.transpose:
            out, out_len, (c.weight, c.bias) = ttnn.conv_transpose2d(
                input_tensor=x, weight_tensor=c.weight, input_height=1, input_width=length, kernel_size=(1, c.kernel),
                stride=(1, c.stride), padding=(0, c.pad), output_padding=(0, 0), dilation=(1, 1), groups=1,
                return_output_dim=True, **common)
            out_len = out_len[1] if isinstance(out_len, (tuple, list)) else out_len
        else:
            out, out_len, (c.weight, c.bias) = ttnn.conv1d(
                input_tensor=x, weight_tensor=c.weight, input_length=length, kernel_size=c.kernel, stride=1,
                padding=c.pad, dilation=c.dilation, groups=1, return_output_dim=True, **common)
        if out.layout != ttnn.TILE_LAYOUT:
            out = ttnn.to_layout(out, ttnn.TILE_LAYOUT)
        return ttnn.reshape(out, [1, 1, out_len, c.out_ch]), out_len

    def _snake_op(self, x, s: Snake):
        if self.prec.fused_snake:  # sin(alpha x)^2 in one multiply, then x + inv_beta * that in one addcmul: 2 passes
            unary = lambda op: ttnn.UnaryWithParam(op)
            sq = ttnn.multiply(x, s.alpha, activations=[unary(ttnn.UnaryOpType.SIN), unary(ttnn.UnaryOpType.SQUARE)],
                               memory_config=MEM)
            out = ttnn.addcmul(x, sq, s.inv_beta, value=1.0, memory_config=MEM)
            ttnn.deallocate(sq)
            return out
        ax = ttnn.multiply(x, s.alpha, memory_config=MEM)
        sn = ttnn.sin(ax, memory_config=MEM)
        ttnn.deallocate(ax)
        sq = ttnn.multiply(sn, sn, memory_config=MEM)
        ttnn.deallocate(sn)
        t = ttnn.multiply(sq, s.inv_beta, memory_config=MEM)
        ttnn.deallocate(sq)
        out = ttnn.add(x, t, memory_config=MEM)
        ttnn.deallocate(t)
        return out

    def _unit(self, x, u: ResUnit, length):
        c = u.conv1.out_ch
        h = self._timed("snake", length, c, self._snake_op, x, u.snake1)
        y, _ = self._timed(f"conv_k7_d{u.conv1.dilation}", length, c, self._run_conv, h, u.conv1, length)
        ttnn.deallocate(h)
        h = self._timed("snake", length, c, self._snake_op, y, u.snake2)
        ttnn.deallocate(y)
        y, _ = self._timed("conv_k1", length, c, self._run_conv, h, u.conv2, length)
        ttnn.deallocate(h)
        out = self._timed("add", length, c, lambda a, b: ttnn.add(a, b, memory_config=MEM), x, y)
        ttnn.deallocate(y)
        ttnn.deallocate(x)
        return out

    # ------------------------------------------------------------------ public
    def decode(self, latents: torch.Tensor, taps: Optional[list] = None) -> torch.Tensor:
        """[64, T] latents -> [2, T * upsample] float32 audio (1920 for ACE-Step, 2048 for DiffRhythm)."""
        length = latents.shape[-1]
        x = ttnn.from_torch(latents.t().contiguous().reshape(1, 1, length, -1).float(), dtype=self.prec.act_dtype,
                            layout=ttnn.TILE_LAYOUT, device=self.dev, memory_config=MEM)
        tap = (lambda t, n: taps.append(ttnn.to_torch(t)[0, 0, :n].float())) if taps is not None else (lambda t, n: None)
        self._where = "in"
        h, length = self._timed("conv_in", length, self.conv1.out_ch, self._run_conv, x, self.conv1, length)
        ttnn.deallocate(x)
        tap(h, length)
        for i, b in enumerate(self.blocks):
            self._where = f"block{i}"
            s = self._timed("snake", length, b.up.in_ch, self._snake_op, h, b.snake)
            ttnn.deallocate(h)
            h, length = self._timed(f"conv_t_s{b.up.stride}", length, b.up.out_ch, self._run_conv, s, b.up, length)
            ttnn.deallocate(s)
            for u in b.units:
                h = self._unit(h, u, length)
            tap(h, length)
        self._where = "out"
        s = self._timed("snake", length, self.snake_out.alpha.shape[-1], self._snake_op, h, self.snake_out)
        ttnn.deallocate(h)
        out, length = self._timed("conv_out", length, 2, self._run_conv, s, self.conv2, length)
        ttnn.deallocate(s)
        audio = ttnn.to_torch(out)[0, 0, :length, :2].float().t().contiguous()
        ttnn.deallocate(out)
        return audio