sunandbean commited on
Commit
cfbcb75
·
verified ·
1 Parent(s): 02e541c

Add files using upload-large-folder tool

Browse files
Files changed (50) hide show
  1. README.md +232 -0
  2. code/PYTHON.md +89 -0
  3. code/config/accelerate_config.yaml +18 -0
  4. code/config/default.ini +85 -0
  5. code/config/diffrhythm-1b.json +13 -0
  6. code/dataset/dataset.py +171 -0
  7. code/dataset/train.scp +4 -0
  8. code/g2p/g2p_generation.py +133 -0
  9. code/g2p/sources/bpmf_2_pinyin.txt +41 -0
  10. code/g2p/sources/pinyin_2_bpmf.txt +429 -0
  11. code/infer/infer.py +228 -0
  12. code/infer/infer_utils.py +459 -0
  13. code/model/__init__.py +6 -0
  14. code/model/cfm.py +318 -0
  15. code/model/dit.py +224 -0
  16. code/model/modules.py +660 -0
  17. code/model/trainer.py +334 -0
  18. code/model/utils.py +200 -0
  19. code/pyproject.toml +24 -0
  20. code/tt_diffrhythm/__init__.py +18 -0
  21. code/tt_diffrhythm/common.py +113 -0
  22. code/tt_diffrhythm/diffrhythm_dit.py +401 -0
  23. code/tt_diffrhythm/diffrhythm_host.py +224 -0
  24. code/tt_diffrhythm/local.py +142 -0
  25. code/tt_diffrhythm/oobleck_vae.py +296 -0
  26. code/tt_diffrhythm/remote.py +264 -0
  27. code/tt_diffrhythm/server.py +334 -0
  28. code/tt_diffrhythm/shapes.py +5 -0
  29. code/tt_diffrhythm/warm_cache.py +69 -0
  30. code/tt_diffrhythm/wire.py +18 -0
  31. code/tt_diffrhythm/worker.py +127 -0
  32. image/blobs/sha256/0cc125e439ef6882a01bae57bece67f14ec2ad123aec1fb12e86bc50175a4b44 +28 -0
  33. image/blobs/sha256/340cf5e2c047e0fc940f2973810428f58250242238231682812dec6f6d54f3ac +0 -0
  34. image/blobs/sha256/44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a +1 -0
  35. image/blobs/sha256/4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1 +0 -0
  36. image/blobs/sha256/6acee8cd93aa0889cc3a8b09f1b2564234ee82aa7c31e87ab61591552d15e894 +0 -0
  37. image/blobs/sha256/81aae6dd123609902a2f56719e50ef0bcf3aa00aff71fc66b02648b3f4aea2f2 +121 -0
  38. image/blobs/sha256/8f09ce5e10995b3fa6362dae9945d9402a5ccabce3d5bf20df0a53c75d9f94ab +0 -0
  39. image/blobs/sha256/8f94a918ad7d7da27e2b025adda011abaa34d892741d6b28221015c75a4ac5ba +26 -0
  40. image/blobs/sha256/9b3433e39d3d40a2f64d1f1e8cb155387651684c5f5c9473bd733c72056dee58 +1 -0
  41. image/blobs/sha256/c2a425aad5e9144add8f2a619c6e3d452d30ddbb5d4f4d3e45caab4f7bb3d233 +0 -0
  42. image/blobs/sha256/c6bd350dd2ed57d0ea34248e5b27d985a6786330d2d3ec29f5563e03ca9f00da +0 -0
  43. image/blobs/sha256/e600ba821186d2d540ea813a478dda1e629c539eef283f062f31309cb8c3af47 +0 -0
  44. image/blobs/sha256/eae56eb3c9610901618ab9c30e2ba8cdcf6981cdd3af7ed5755aba2cc46db6e1 +1 -0
  45. image/blobs/sha256/ee5adbd6eba4dfd9ba7e59692c46379b3d60a88f8445ec6a045d4aeb336f5bb9 +0 -0
  46. image/index.json +1 -0
  47. image/manifest.json +1 -0
  48. image/oci-layout +1 -0
  49. requirements.lock +148 -0
  50. tt_kernel_manifest.json +130 -0
README.md ADDED
@@ -0,0 +1,232 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ tags:
3
+ - blackhole
4
+ - p100a
5
+ - tenstorrent
6
+ - ttnn
7
+ - tt-metal
8
+ - tt-model-catalog
9
+ - tt-nn
10
+ - text-to-audio
11
+ - music-generation
12
+ - diffrhythm
13
+ - tt-dit-server
14
+ - tt-model-cache
15
+ - tt-model-container
16
+ pipeline_tag: text-to-audio
17
+ license: apache-2.0
18
+ license_link: https://huggingface.co/ASLP-lab/DiffRhythm-1_2-full
19
+ base_model:
20
+ - ASLP-lab/DiffRhythm-1_2-full
21
+ ---
22
+
23
+ # diffrhythm-1.2-p100a
24
+
25
+ DiffRhythm 1.2 Full, ported to one Tenstorrent Blackhole p100a.
26
+ Weights: [ASLP-lab/DiffRhythm-1_2-full](https://huggingface.co/ASLP-lab/DiffRhythm-1_2-full) · Upstream code: [ASLP-lab/DiffRhythm](https://github.com/ASLP-lab/DiffRhythm) · Port: [SunandBean/tt-diffrhythm](https://github.com/SunandBean/tt-diffrhythm)
27
+
28
+ Runs on **p100a** (mesh 1×1). The **CFM transformer and the stable-audio VAE decoder run on the card**; the style encoder, the sampler and the post-processing stay on the CPU.
29
+
30
+ **What this port adds** — the CFM transformer and the stable-audio-tools Oobleck decoder on the card, sharing the worker, transport and host helpers with the sibling [tt-acestep](https://github.com/SunandBean/tt-acestep) port.
31
+ **What it builds on** — DiffRhythm's own pipeline (Apache-2.0), which runs unchanged on the CPU; only those two stages move.
32
+
33
+ **A 96-second song in 10.0 s** end to end — the CFM transformer 7.7 s across 62 calls, the audio VAE decode 2.0 s. The same song takes 9.7–10.1 s on an RTX 5070 Ti.
34
+
35
+ ## Listen
36
+
37
+ Exactly what the container returned on run 1 of the measurement below — `POST /predict` with
38
+ `{"prompt": "warm lo-fi hip hop, mellow rhodes piano, soft vinyl crackle, relaxed drums",
39
+ "instrumental": true, "duration": 96, "seed": 42}`, generated in **10.05 s** on the card. The file is
40
+ the response's WAV bytes, unmodified: 96.0 s stereo 44.1 kHz 16-bit, sha256 `7e89a573e6d94e4f`.
41
+
42
+ <audio controls src="https://huggingface.co/sunandbean/diffrhythm-1.2-p100a/resolve/main/media/diffrhythm_96s_instrumental.wav"></audio>
43
+
44
+ [`media/diffrhythm_96s_instrumental.wav`](media/diffrhythm_96s_instrumental.wav) (16 MB)
45
+
46
+ ## How it is wired
47
+
48
+ Two stages move to the card and nothing else changes: `remote.py` replaces `cfm.transformer` with a `RemoteDiffRhythmDiT` and decodes through a `RemoteVae`, so the upstream `CFM.sample` loop runs unmodified.
49
+
50
+ Both shims reach the card through exactly one method, `worker.call(msg)`, and there are two things that answer it.
51
+
52
+ **Two processes** (`TTWorker`), when DiffRhythm and tt-metal cannot share an interpreter — which is the deployment this port came from, where DiffRhythm resolved `torch 2.10.0+cu128` and `numpy>=2` against ttnn's `torch 2.11.0+cpu` and `numpy 1.x`:
53
+
54
+ ```
55
+ DiffRhythm venv (CPU) TT image (p100a)
56
+ ───────────────────── ────────────────
57
+ style encoder ──socket──► worker.py
58
+ CFM sampler loop DiffRhythmDiT (on the card)
59
+ post-processing OobleckTT (on the card)
60
+ ```
61
+
62
+ Tensors cross as raw little-endian bytes with a dtype and a shape (`wire.py`), because the two sides cannot share a pickle of a torch tensor.
63
+
64
+ **One process** (`LocalWorker`), when one environment satisfies both — which is what the container below is. `LocalWorker` answers the same `call(msg)` in-process, so the shims, their window arithmetic and their drop-in signatures are reused unchanged rather than reimplemented.
65
+
66
+ Messages still carry `wire`-encoded tensors in that case even though both ends are one process. That is deliberate: it is what lets the shims run against either worker, and the encoding is a float32 copy of a few MB against card work measured in seconds.
67
+
68
+ ## Quickstart
69
+
70
+ Prerequisite: a tt-metal / ttnn environment for the worker, and DiffRhythm installed in its own environment for the host side.
71
+
72
+ ```bash
73
+ hf download sunandbean/diffrhythm-1.2-p100a --include "code/*" --local-dir diffrhythm-1.2-p100a && cd diffrhythm-1.2-p100a
74
+ pip install -e code/
75
+ ```
76
+
77
+ Host side, inside the DiffRhythm environment:
78
+
79
+ ```python
80
+ from tt_diffrhythm import TTWorker, RemoteDiffRhythmDiT, RemoteVae
81
+
82
+ worker = TTWorker(cfm_model_pt, model="diffrhythm", vae=vae_model_pt).start()
83
+ worker.wait_ready()
84
+ cfm.transformer = RemoteDiffRhythmDiT(worker, cfm.transformer)
85
+ ... # upstream CFM.sample runs unchanged
86
+ audio = RemoteVae(worker, upsample=2048)(latents)
87
+ worker.close()
88
+ ```
89
+
90
+ Worker side, inside the tt-metal image:
91
+
92
+ ```bash
93
+ python -m tt_diffrhythm.worker --socket /ipc/worker.sock \
94
+ --checkpoint /checkpoint/cfm_model.pt --vae /vae/vae_model.pt
95
+ python -m tt_diffrhythm.warm_cache # compile every sequence bucket ahead of time
96
+ ```
97
+
98
+ | | |
99
+ |---|---|
100
+ | **Duration** | 96–285 seconds |
101
+ | **Lyrics** | Plain lyrics with estimated line timing, or a timestamped LRC |
102
+ | **Guidance** | CFG: `pred + 4 (pred − null)` — every step runs two DiT calls |
103
+ | **Shapes** | Song lengths fall into sequence buckets (`diffrhythm_host.seq_buckets`), both CFG branches, plus the VAE windows. `warm_cache` compiles all of them. |
104
+
105
+ ## Performance
106
+
107
+ Measured on an idle p100a. A 96-second song is 2067 latent frames, padded to a sequence of 2560.
108
+
109
+ | | |
110
+ |---|---:|
111
+ | **DiT, whole 96-second song** | **8.9 s** (7.7 s at HiFi2) |
112
+ | One DiT call at sequence 2560 | 121 ms |
113
+ | Per call during the device check | 0.143–0.186 s |
114
+
115
+ Where a 121 ms call goes (`dit_profile.py`, 156.8 ms profiled):
116
+
117
+ | Op | Time | Calls |
118
+ |---|---:|---:|
119
+ | matmul | 66.6 ms | 66 |
120
+ | SDPA | 21.9 ms | 16 |
121
+ | add | 12.0 ms | 47 |
122
+ | RMS norm | 7.7 ms | 32 |
123
+ | slice | 7.3 ms | 62 |
124
+ | rotary embedding | 5.2 ms | 32 |
125
+
126
+ The audio VAE decoder, against a CPU decode of the same latents:
127
+
128
+ | Latent frames | CPU | p100a (first / warm) |
129
+ |---:|---:|---:|
130
+ | 512 | 10.67 s | 33.94 / **0.33** s |
131
+ | 1024 | — | 0.81 s |
132
+
133
+ The first decode at a shape pays the kernel compilation; afterwards it is 30× faster than the CPU.
134
+
135
+ ### From the published container
136
+
137
+ The same image anyone can `tt-model pull`, reached over its own HTTP server on an idle card
138
+ (2026-10-09). Three 96-second instrumentals, seed 42, from a cold start:
139
+
140
+ | | run 1 | run 2 | run 3 |
141
+ |---|---:|---:|---:|
142
+ | **Generation** | **10.05 s** | 10.06 s | 10.04 s |
143
+ | CFM transformer, 62 calls | 7.68 s | 7.68 s | 7.69 s |
144
+ | VAE decode | 2.03 s | 2.05 s | 2.01 s |
145
+
146
+ All three returned the same 96.0 s stereo 44.1 kHz WAV. Load to ready was 50 s, of which the VAE
147
+ warm-up is 3.1 s; device DRAM after load is 2.9 GB of 29.9 — the card is nowhere near full, and
148
+ the port holds that headroom deliberately so the sequence buckets can grow to 285 seconds.
149
+
150
+ The whole request takes about 30 s of wall time, because the response carries the song: 17 MB of
151
+ WAV, base64-encoded. Generation is the 10 s above; the rest is serialising and shipping it.
152
+
153
+ ### Against an RTX 5070 Ti
154
+
155
+ DiffRhythm's own GPU path with the deployment's stage-by-stage placement (one model in VRAM at a
156
+ time, which is what keeps it inside 16 GB), same prompt and duration, a fresh seed per run:
157
+
158
+ | | p100a (container) | RTX 5070 Ti |
159
+ |---|---:|---:|
160
+ | 96-second instrumental | 10.04–10.06 s | **9.73–10.05 s** |
161
+ | First run | 10.05 s | 12.35 s |
162
+ | Peak device memory | 2.9 GB of 29.9 | 2.5 GB of 15.5 |
163
+
164
+ The same, within noise. The card reaches it with the CFM transformer and the audio VAE decoder in
165
+ TTNN and everything else on the CPU; the GPU reaches it by moving three models in and out of VRAM.
166
+
167
+ ### Accuracy
168
+
169
+ **The torch reference in this port against upstream DiffRhythm** (float32, per CFM call): PCC **0.9999997–0.9999999**. For scale, upstream float32 against the GPU fp16 run is 0.999995–0.999999 — so the reimplementation is closer to upstream than upstream is to its own GPU run.
170
+
171
+ **The card against this port's reference**: minimum PCC **0.99994** across the whole 96-second song.
172
+
173
+ **Final latents against the GPU fp16 run**, which is what actually matters after CFG amplification:
174
+
175
+ | | Final latent PCC | Relative RMS |
176
+ |---|---:|---:|
177
+ | CPU float32 | 0.99997 | 0.008 |
178
+ | **p100a (HiFi4, the default)** | **0.9954** | 0.096 |
179
+ | p100a (HiFi2) | 0.9931 | 0.118 |
180
+ | CPU bf16 | 0.99129 | 0.132 |
181
+
182
+ **The card beats a CPU bf16 run.** Decoded audio against the GPU: PCC 0.9721 and 2.06 dB spectral distance, against 0.9691 and 3.07 dB for CPU bf16 (CPU float32 is 0.9996 / 0.51 dB). The audio VAE decoder itself is PCC 0.99963 with 0.93 dB spectral distance against the CPU.
183
+
184
+ CFG is why the margin matters: `pred + 4 (pred − null)` multiplies the difference between the two branches by four, so a small per-call error is amplified before it reaches the audio. `DRPrecision` therefore keeps the activations, the residual and the output in fp32 and defaults to HiFi4 matmuls — HiFi2 is about 14% faster and measurably worse.
185
+
186
+ ## Caveats
187
+
188
+ - **Single card only.** Mesh 1×1.
189
+ - **Seeds do not reproduce across backends.** A song made on the p100a cannot be reproduced bit-for-bit on a GPU with the same seed.
190
+ - **Two environments, or one.** Where DiffRhythm's environment and the tt-metal runtime cannot be merged, the host side runs inside DiffRhythm's own environment and the worker inside a tt-metal image. Where they can — which the container below is — `LocalWorker` runs both in one process. Which one you get is a property of the environment, not of the port.
191
+ - **CFG doubles the work.** Every sampler step is two DiT calls, one conditional and one null.
192
+ - The first song of each shape pays the kernel compilation, and so does the first VAE decode at a shape (33.9 s against 0.33 s warm). Run `warm_cache` after changing the code or the image.
193
+ - A render holds the card for the length of the song. On a shared card that blocks everything else.
194
+
195
+ ## Licensing
196
+
197
+ - Weights: [ASLP-lab/DiffRhythm-1_2-full](https://huggingface.co/ASLP-lab/DiffRhythm-1_2-full), Apache-2.0. Not redistributed here.
198
+ - Port code (`code/tt_diffrhythm/`): Apache-2.0, from [SunandBean/tt-diffrhythm](https://github.com/SunandBean/tt-diffrhythm).
199
+ - `code/{model,infer,g2p,config,dataset}/` are **DiffRhythm itself** (Apache-2.0), at [`28ad63c`](https://github.com/ASLP-lab/DiffRhythm/commit/28ad63c0f096fe2ee258bcabbcf081d5d9366afd), unmodified. They ship here because the container runs them: upstream is a research checkout with no packaging, so it is vendored as source rather than resolved by pip. `code/models/` is [tt-metal](https://github.com/tenstorrent/tt-metal)'s `models/common` and `models/tt_dit` (Apache-2.0), for the same reason.
200
+ - `code/tt_diffrhythm/common.py` and the worker, transport and audio VAE modules are shared with the sibling [tt-acestep](https://github.com/SunandBean/tt-acestep) port (Apache-2.0); `oobleck_vae.py` implements both DiffRhythm's stable-audio-tools decoder and ACE-Step's diffusers Oobleck decoder.
201
+
202
+ ## Run it as a container
203
+
204
+ Everything below is generated by `tt-model package` from the manifest this image was built from.
205
+
206
+ ```bash
207
+ tt-model pull sunandbean/diffrhythm-1.2-p100a --with-weights
208
+ tt-model serve sunandbean/diffrhythm-1.2-p100a
209
+ ```
210
+
211
+ `pull --with-weights` downloads the Docker image and the [`ASLP-lab/DiffRhythm-1_2-full`](https://huggingface.co/ASLP-lab/DiffRhythm-1_2-full) weights (into your HF cache; they are not in the image). `serve` starts the model's own HTTP server on port 20000 (or the next free port, if that one is busy); the first start compiles kernels for your device, which takes several minutes, and the server is ready when it logs `Application startup complete`.
212
+
213
+ ### Request shape
214
+
215
+ `POST /predict` takes `{"prompt": str, "lyrics": str, "instrumental": bool,
216
+ "duration": int, "seed": int}` and returns a base64 WAV with `timing_s` and the `lrc` the
217
+ song was actually sung to.
218
+
219
+ `prompt` is the style description. Vocals need `lyrics`: real LRC is used as given, and
220
+ plain lines get approximate starts derived from their word counts -- a convenience, not
221
+ learned alignment. `duration` is 96-285 seconds; use the ACE-Step port for longer songs.
222
+ The schedule is fixed at **32 steps**; any other `inference_steps` is a 422.
223
+
224
+ ### Provenance
225
+
226
+ The exact sources the image was built from — the model code under `code/` is byte-identical to what is inside the image (`pyproject.toml`, `PYTHON.md` and `examples/` are packaging files this repo adds, and are not in the image):
227
+
228
+ | component | built from |
229
+ | --- | --- |
230
+ | tt-metal | [`58648d44443223041148125843ca7c7d4780463e`](https://github.com/tenstorrent/tt-metal/commit/58648d44443223041148125843ca7c7d4780463e) |
231
+ | `code/` digest | `6fe96af2d7e7a1b8` (sha256, first 16 hex digits) |
232
+ | built | 2026-10-09T14:54:37+00:00 by tt-model 0.1.0 |
code/PYTHON.md ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tt_diffrhythm — Python reference
2
+
3
+ DiffRhythm 1.2 Full on one Tenstorrent Blackhole p100a. The CFM transformer and the stable-audio VAE
4
+ decoder run on the card; everything else stays on the CPU in DiffRhythm's own environment.
5
+
6
+ ## Two processes
7
+
8
+ DiffRhythm's environment and the tt-metal runtime need different torch and numpy builds, so they run
9
+ as two processes talking over a Unix socket in a directory bind-mounted into both. Tensors cross as
10
+ raw little-endian bytes with a dtype and a shape (`wire.py`).
11
+
12
+ ## Host side (inside the DiffRhythm environment)
13
+
14
+ ### `TTWorker(checkpoint, image=None, log=None, reply_timeout_s=120.0, model="diffrhythm", vae=None)`
15
+
16
+ `checkpoint` is `cfm_model.pt` and `vae` is `vae_model.pt`; Hugging Face cache links are resolved.
17
+ `image` is the tt-metal / ttnn image the port was built against; it has no portable default, so set
18
+ it here or through `MUSIC_TT_IMAGE`. `.start()` starts the worker container on the card,
19
+ `.wait_ready(timeout_s)` blocks until the weights are on the device, `.close()` stops the container and
20
+ waits for it to exit, so the card is free only after that.
21
+
22
+ ### `RemoteDiffRhythmDiT(worker, transformer)`
23
+
24
+ A drop-in for `cfm.transformer` in `CFM.sample` (batch 1): same call, same output. Assign it and the
25
+ upstream sampler runs unchanged.
26
+
27
+ ### `RemoteVae(worker, upsample=2048)`
28
+
29
+ `[B, 64, T]` latents to `[B, 2, T * upsample]` float32 audio on the CPU. It picks the largest VAE
30
+ window from `shapes.VAE_WINDOWS` that fits the song.
31
+
32
+ ## Worker side (inside the tt-metal image)
33
+
34
+ ```bash
35
+ python -m tt_diffrhythm.worker --socket /ipc/worker.sock \
36
+ --checkpoint /checkpoint/cfm_model.pt --vae /vae/vae_model.pt
37
+ ```
38
+
39
+ Protocol (plain dicts, tensors via `wire`):
40
+
41
+ | | |
42
+ |---|---|
43
+ | `-> {"op": "ready", ...}` | once the weights are on the device |
44
+ | `<- {"op": "prepare", "text_embed": {drop_text: [T, 512]}, "cond": [T, 64]}` | `-> {"op": "prepared", ...}` |
45
+ | `<- {"op": "forward", "x": [T, 64], "drop_text": bool, "style": [512], "c": [512]}` | `-> {"op": "velocity", "v": [T, 64]}` |
46
+
47
+ ## Direct use on the card
48
+
49
+ ```python
50
+ from tt_diffrhythm import DiffRhythmDiT, DRPrecision, OobleckTT, open_device, close_device
51
+ from tt_diffrhythm.diffrhythm_host import Checkpoint
52
+
53
+ dev = open_device()
54
+ dit = DiffRhythmDiT(dev, Checkpoint("cfm_model.pt")) # DRPrecision() by default
55
+ vae = OobleckTT(dev, "vae_model.pt")
56
+ ...
57
+ close_device(dev)
58
+ ```
59
+
60
+ ### `DRPrecision`
61
+
62
+ Extends the shared `Precision` with the choices CFG forces. `pred + 4 (pred − null)` multiplies the
63
+ difference between the two branches by four, so the activations, the residual and the output stay in
64
+ fp32 and matmuls default to **HiFi4**. HiFi2 is about 14% faster and measurably worse
65
+ (final latent PCC 0.9931 against 0.9954).
66
+
67
+ ## Kernel cache
68
+
69
+ ```bash
70
+ python -m tt_diffrhythm.warm_cache
71
+ ```
72
+
73
+ Compiles every sequence bucket of a 96–285 s song, both CFG branches, and the VAE windows in
74
+ `shapes.VAE_WINDOWS`. The first decode at a VAE shape costs 33.9 s against 0.33 s warm, so this is
75
+ worth running after any change to the code or the image.
76
+
77
+ ## Modules
78
+
79
+ | Module | Role |
80
+ |---|---|
81
+ | `remote.py` | host side: `TTWorker`, `RemoteDiffRhythmDiT`, `RemoteVae`, the worker container |
82
+ | `worker.py` | worker side: loads the CFM transformer and the VAE and answers one host pipeline |
83
+ | `wire.py` | the tensor transport between the two torch builds |
84
+ | `shapes.py` | shapes both sides need, with no torch or ttnn import |
85
+ | `diffrhythm_dit.py` | the CFM transformer on the card, and `DRPrecision` |
86
+ | `diffrhythm_host.py` | checkpoint access, the torch reference and the sequence buckets |
87
+ | `oobleck_vae.py` | the audio VAE decoder on the card |
88
+ | `common.py` | device and host helpers shared with the tt-acestep port |
89
+ | `warm_cache.py` | ahead-of-time kernel compilation for every bucket |
code/config/accelerate_config.yaml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ compute_environment: LOCAL_MACHINE
2
+ debug: false
3
+ distributed_type: MULTI_GPU
4
+ downcast_bf16: 'no'
5
+ enable_cpu_affinity: false
6
+ machine_rank: 0
7
+ main_training_function: main
8
+ mixed_precision: fp16
9
+ num_machines: 1
10
+ rdzv_backend: static
11
+ same_network: true
12
+ num_processes: 1
13
+ tpu_env: []
14
+ tpu_use_cluster: false
15
+ tpu_use_sudo: false
16
+ use_cpu: false
17
+ main_process_port: 12342
18
+
code/config/default.ini ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ [DEFAULTS]
3
+
4
+ #name of the run
5
+ exp_name = diffrhythm-test
6
+
7
+ # the batch size
8
+ batch_size = 8
9
+
10
+ # the chunk size
11
+ max_frames = 3000
12
+ min_frames = 10
13
+
14
+ # number of CPU workers for the DataLoader
15
+ num_workers = 4
16
+
17
+ # the random seed
18
+ seed = 42
19
+
20
+ # Batches for gradient accumulation
21
+ accum_batches = 1
22
+
23
+ # Number of steps between checkpoints
24
+ checkpoint_every = 10000
25
+
26
+ # trainer checkpoint file to restart training from
27
+ ckpt_path = ''
28
+
29
+ # model checkpoint file to start a new training run from
30
+ pretrained_ckpt_path = ''
31
+
32
+ # Checkpoint path for the pretransform model if needed
33
+ pretransform_ckpt_path = ''
34
+
35
+ # configuration model specifying model hyperparameters
36
+ model_config = ''
37
+
38
+ # configuration for datasets
39
+ dataset_config = ''
40
+
41
+ # directory to save the checkpoints in
42
+ save_dir = ''
43
+
44
+ # grad norm
45
+ max_grad_norm = 1.0
46
+
47
+ # grad accu
48
+ grad_accumulation_steps = 1
49
+
50
+ # lr 1e-5
51
+ learning_rate = 7.5e-5
52
+
53
+ # epoch
54
+ epochs = 110
55
+
56
+ # warmup steps
57
+ num_warmup_updates = 20
58
+
59
+ # save checkpoint per steps
60
+ save_per_updates = 5000
61
+
62
+ # save last checkpoint per steps
63
+ last_per_steps = 5000
64
+
65
+ file_path = "dataset/train.scp"
66
+
67
+ audio_drop_prob = 0.3
68
+
69
+ cond_drop_prob = 0.0
70
+
71
+ style_drop_prob = 0.1
72
+
73
+ lrc_drop_prob = 0.1
74
+
75
+ use_style_prompt = 1
76
+
77
+ reset_lr = 0
78
+
79
+ resumable_with_seed = 666
80
+
81
+ downsample_rate = 2048
82
+
83
+ grad_ckpt = 0
84
+
85
+ sampling_rate = 44100
code/config/diffrhythm-1b.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "diffrhythm",
3
+ "model": {
4
+ "dim": 2048,
5
+ "depth": 16,
6
+ "heads": 32,
7
+ "ff_mult": 4,
8
+ "text_dim": 512,
9
+ "conv_layers": 4,
10
+ "mel_dim": 64,
11
+ "text_num_embeds": 363
12
+ }
13
+ }
code/dataset/dataset.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025 ASLP-LAB
2
+ # 2025 Huakang Chen (huakang@mail.nwpu.edu.cn)
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import torch
17
+ import random
18
+
19
+ class DiffusionDataset(torch.utils.data.Dataset):
20
+ def __init__(self, file_path, max_frames=2048, min_frames=512, sampling_rate=44100, downsample_rate=2048, precision='fp16'):
21
+
22
+ self.max_frames = max_frames
23
+ self.min_frames = min_frames
24
+ self.sampling_rate = sampling_rate
25
+ self.downsample_rate = 2048
26
+ self.max_secs = max_frames / (sampling_rate / downsample_rate)
27
+
28
+ self.file_path = file_path
29
+ with open(file_path, 'r') as f:
30
+ self.file_lst = [line.strip() for line in f.readlines()]
31
+
32
+ self.pad_token_id = 0
33
+ self.comma_token_id = 1
34
+ self.period_token_id = 2
35
+ self.start_token_id = 355
36
+
37
+ if precision == 'fp16':
38
+ self.feature_dtype = torch.float16
39
+ elif precision == 'bf16':
40
+ self.feature_dtype = torch.bfloat16
41
+ elif precision == 'fp32':
42
+ self.feature_dtype = torch.float32
43
+
44
+ random.seed(42)
45
+ random.shuffle(self.file_lst)
46
+
47
+ def load_item(self, item, field):
48
+ try:
49
+ item, reader_idx = item[field]
50
+ item = self.lance_connections[reader_idx].get_datas_by_rowids([item._rowid])[0]
51
+ except Exception as e:
52
+ return None
53
+ return item
54
+
55
+ def get_triple(self, item):
56
+ utt, lrc_path, latent_path, style_path = item.split("|")
57
+
58
+ time_lrc = torch.load(lrc_path, map_location='cpu')
59
+ input_times = time_lrc['time']
60
+ input_lrcs = time_lrc['lrc']
61
+ lrc_with_time = list(zip(input_times, input_lrcs))
62
+
63
+ latent = torch.load(latent_path, map_location='cpu') # [b, d, t]
64
+ latent = latent.squeeze(0)
65
+
66
+ prompt = torch.load(style_path, map_location='cpu') # [b, d]
67
+ prompt = prompt.squeeze(0)
68
+
69
+ max_start_frame = max(0, latent.shape[-1] - self.max_frames)
70
+ start_frame = random.randint(0, max_start_frame)
71
+ start_time = start_frame * self.downsample_rate / self.sampling_rate
72
+ normalized_start_time = start_frame / latent.shape[-1]
73
+ latent = latent[:, start_frame:]
74
+
75
+ lrc_with_time = [(time_start - start_time, line) for (time_start, line) in lrc_with_time if (time_start - start_time) >= 0] # empty for pure music
76
+ lrc_with_time = [(time_start, line) for (time_start, line) in lrc_with_time if time_start < self.max_secs] # drop time longer than max_secs
77
+
78
+ if len(lrc_with_time) >= 1:
79
+ latent_end_time = lrc_with_time[-1][0]
80
+ else:
81
+ raise
82
+
83
+ if self.max_frames == 2048:
84
+ lrc_with_time = lrc_with_time[:-1] if len(lrc_with_time) >= 1 else lrc_with_time # drop last, can be empty
85
+
86
+ lrc = torch.zeros((self.max_frames,), dtype=torch.long)
87
+
88
+ tokens_count = 0
89
+ last_end_pos = 0
90
+ for time_start, line in lrc_with_time:
91
+ tokens = [token if token != self.period_token_id else self.comma_token_id for token in line] + [self.period_token_id]
92
+ tokens = torch.tensor(tokens, dtype=torch.long)
93
+ num_tokens = tokens.shape[0]
94
+
95
+ gt_frame_start = int(time_start * self.sampling_rate / self.downsample_rate)
96
+
97
+ frame_shift = 0
98
+
99
+ frame_start = max(gt_frame_start - frame_shift, last_end_pos)
100
+ frame_len = min(num_tokens, self.max_frames - frame_start)
101
+
102
+ lrc[frame_start:frame_start + frame_len] = tokens[:frame_len]
103
+
104
+ tokens_count += num_tokens
105
+ last_end_pos = frame_start + frame_len
106
+
107
+ latent = latent[:, :int(latent_end_time * self.sampling_rate / self.downsample_rate)]
108
+
109
+ latent = latent.to(self.feature_dtype)
110
+ prompt = prompt.to(self.feature_dtype)
111
+
112
+ return prompt, lrc, latent, normalized_start_time
113
+
114
+ def __getitem__(self, index):
115
+ idx = index
116
+ while True:
117
+ try:
118
+ prompt, lrc, latent, start_time = self.get_triple(self.file_lst[idx])
119
+ if latent.shape[-1] < self.min_frames: # Too short
120
+ raise
121
+ item = {'prompt': prompt, "lrc": lrc, "latent": latent, "start_time": start_time}
122
+ return item
123
+ except Exception as e:
124
+ idx = random.randint(0, self.__len__() - 1)
125
+ continue
126
+
127
+ def __len__(self):
128
+ return len(self.file_lst)
129
+
130
+ def custom_collate_fn(self, batch):
131
+ latent_list = [item['latent'] for item in batch]
132
+ prompt_list = [item['prompt'] for item in batch]
133
+ lrc_list = [item['lrc'] for item in batch]
134
+ start_time_list = [item['start_time'] for item in batch]
135
+
136
+ latent_lengths = torch.LongTensor([latent.shape[-1] for latent in latent_list])
137
+ prompt_lengths = torch.LongTensor([prompt.shape[-1] for prompt in prompt_list])
138
+ lrc_lengths = torch.LongTensor([lrc.shape[-1] for lrc in lrc_list])
139
+
140
+ max_prompt_length = prompt_lengths.amax()
141
+
142
+ padded_prompt_list = []
143
+ for prompt in prompt_list:
144
+ padded_prompt = torch.nn.functional.pad(prompt, (0, max_prompt_length - prompt.shape[-1]))
145
+ padded_prompt_list.append(padded_prompt)
146
+
147
+ padded_latent_list = []
148
+ for latent in latent_list:
149
+ padded_latent = torch.nn.functional.pad(latent, (0, self.max_frames - latent.shape[-1]))
150
+ padded_latent_list.append(padded_latent)
151
+
152
+ padded_start_time_list = []
153
+ for start_time in start_time_list:
154
+ padded_start_time = start_time
155
+ padded_start_time_list.append(padded_start_time)
156
+
157
+ prompt_tensor = torch.stack(padded_prompt_list)
158
+ lrc_tensor = torch.stack(lrc_list)
159
+ latent_tensor = torch.stack(padded_latent_list)
160
+ start_time_tensor = torch.tensor(padded_start_time_list)
161
+
162
+ return {'prompt': prompt_tensor, 'lrc': lrc_tensor, 'latent': latent_tensor, \
163
+ "prompt_lengths": prompt_lengths, "lrc_lengths": lrc_lengths, "latent_lengths": latent_lengths, \
164
+ "start_time": start_time_tensor}
165
+
166
+
167
+ if __name__ == "__main__":
168
+ dd = DiffusionDataset("train.scp", 2048, 512)
169
+ x = dd[0]
170
+ import pdb; pdb.set_trace()
171
+ print(x)
code/dataset/train.scp ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ 2626046476|dataset/lrc/2626046476.pt|dataset/latent/2626046476.pt|dataset/style/2626046476.pt
2
+ 549162241|dataset/lrc/549162241.pt|dataset/latent/549162241.pt|dataset/style/549162241.pt
3
+ 547512549|dataset/lrc/547512549.pt|dataset/latent/547512549.pt|dataset/style/547512549.pt
4
+ 28528706|dataset/lrc/28528706.pt|dataset/latent/28528706.pt|dataset/style/28528706.pt
code/g2p/g2p_generation.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2024 Amphion.
2
+ #
3
+ # This source code is licensed under the MIT license found in the
4
+ # LICENSE file in the root directory of this source tree.
5
+
6
+ import os
7
+ import sys
8
+
9
+ from g2p.g2p import PhonemeBpeTokenizer
10
+ from g2p.utils.g2p import phonemizer_g2p
11
+ import tqdm
12
+ from typing import List
13
+ import json
14
+ import os
15
+ import re
16
+
17
+
18
+ def ph_g2p(text, language):
19
+
20
+ return phonemizer_g2p(text=text, language=language)
21
+
22
+
23
+ def g2p(text, sentence, language):
24
+
25
+ return text_tokenizer.tokenize(text=text, sentence=sentence, language=language)
26
+
27
+
28
+ def is_chinese(char):
29
+ if char >= "\u4e00" and char <= "\u9fa5":
30
+ return True
31
+ else:
32
+ return False
33
+
34
+
35
+ def is_alphabet(char):
36
+ if (char >= "\u0041" and char <= "\u005a") or (
37
+ char >= "\u0061" and char <= "\u007a"
38
+ ):
39
+ return True
40
+ else:
41
+ return False
42
+
43
+
44
+ def is_other(char):
45
+ if not (is_chinese(char) or is_alphabet(char)):
46
+ return True
47
+ else:
48
+ return False
49
+
50
+
51
+ def get_segment(text: str) -> List[str]:
52
+ # sentence --> [ch_part, en_part, ch_part, ...]
53
+ segments = []
54
+ types = []
55
+ flag = 0
56
+ temp_seg = ""
57
+ temp_lang = ""
58
+
59
+ # Determine the type of each character. type: blank, chinese, alphabet, number, unk and point.
60
+ for i, ch in enumerate(text):
61
+ if is_chinese(ch):
62
+ types.append("zh")
63
+ elif is_alphabet(ch):
64
+ types.append("en")
65
+ else:
66
+ types.append("other")
67
+
68
+ assert len(types) == len(text)
69
+
70
+ for i in range(len(types)):
71
+ # find the first char of the seg
72
+ if flag == 0:
73
+ temp_seg += text[i]
74
+ temp_lang = types[i]
75
+ flag = 1
76
+ else:
77
+ if temp_lang == "other":
78
+ if types[i] == temp_lang:
79
+ temp_seg += text[i]
80
+ else:
81
+ temp_seg += text[i]
82
+ temp_lang = types[i]
83
+ else:
84
+ if types[i] == temp_lang:
85
+ temp_seg += text[i]
86
+ elif types[i] == "other":
87
+ temp_seg += text[i]
88
+ else:
89
+ segments.append((temp_seg, temp_lang))
90
+ temp_seg = text[i]
91
+ temp_lang = types[i]
92
+ flag = 1
93
+
94
+ segments.append((temp_seg, temp_lang))
95
+ return segments
96
+
97
+
98
+ def chn_eng_g2p(text: str):
99
+ # now only en and ch
100
+ segments = get_segment(text)
101
+ all_phoneme = ""
102
+ all_tokens = []
103
+
104
+ for index in range(len(segments)):
105
+ seg = segments[index]
106
+ phoneme, token = g2p(seg[0], text, seg[1])
107
+ all_phoneme += phoneme + "|"
108
+ all_tokens += token
109
+
110
+ if seg[1] == "en" and index == len(segments) - 1 and all_phoneme[-2] == "_":
111
+ all_phoneme = all_phoneme[:-2]
112
+ all_tokens = all_tokens[:-1]
113
+ return all_phoneme, all_tokens
114
+
115
+
116
+ text_tokenizer = PhonemeBpeTokenizer()
117
+ with open("./g2p/g2p/vocab.json", "r", encoding='utf-8') as f:
118
+ json_data = f.read()
119
+ data = json.loads(json_data)
120
+ vocab = data["vocab"]
121
+
122
+ if __name__ == '__main__':
123
+ phone, token = chn_eng_g2p("你好,hello world")
124
+ phone, token = chn_eng_g2p("你好,hello world, Bonjour, 테스트 해 보겠습니다, 五月雨緑")
125
+ print(phone)
126
+ print(token)
127
+
128
+ #phone, token = text_tokenizer.tokenize("你好,hello world, Bonjour, 테스트 해 보겠습니다, 五月雨緑", "", "auto")
129
+ phone, token = text_tokenizer.tokenize("緑", "", "auto")
130
+ #phone, token = text_tokenizer.tokenize("आइए इसका परीक्षण करें", "", "auto")
131
+ #phone, token = text_tokenizer.tokenize("आइए इसका परीक्षण करें", "", "other")
132
+ print(phone)
133
+ print(token)
code/g2p/sources/bpmf_2_pinyin.txt ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ b ㄅ
2
+ p ㄆ
3
+ m ㄇ
4
+ f ㄈ
5
+ d ㄉ
6
+ t ㄊ
7
+ n ㄋ
8
+ l ㄌ
9
+ g ㄍ
10
+ k ㄎ
11
+ h ㄏ
12
+ j ㄐ
13
+ q ㄑ
14
+ x ㄒ
15
+ zh ㄓ
16
+ ch ㄔ
17
+ sh ㄕ
18
+ r ㄖ
19
+ z ㄗ
20
+ c ㄘ
21
+ s ㄙ
22
+ i ㄧ
23
+ u ㄨ
24
+ v ㄩ
25
+ a ㄚ
26
+ o ㄛ
27
+ e ㄜ
28
+ e ㄝ
29
+ ai ㄞ
30
+ ei ㄟ
31
+ ao ㄠ
32
+ ou ㄡ
33
+ an ㄢ
34
+ en ㄣ
35
+ ang ㄤ
36
+ eng ㄥ
37
+ er ㄦ
38
+ 2 ˊ
39
+ 3 ˇ
40
+ 4 ˋ
41
+ 0 ˙
code/g2p/sources/pinyin_2_bpmf.txt ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ a ㄚ
2
+ ai ㄞ
3
+ an ㄢ
4
+ ang ㄤ
5
+ ao ㄠ
6
+ ba ㄅㄚ
7
+ bai ㄅㄞ
8
+ ban ㄅㄢ
9
+ bang ㄅㄤ
10
+ bao ㄅㄠ
11
+ bei ㄅㄟ
12
+ ben ㄅㄣ
13
+ beng ㄅㄥ
14
+ bi ㄅㄧ
15
+ bian ㄅㄧㄢ
16
+ biang ㄅㄧㄤ
17
+ biao ㄅㄧㄠ
18
+ bie ㄅㄧㄝ
19
+ bin ㄅㄧㄣ
20
+ bing ㄅㄧㄥ
21
+ bo ㄅㄛ
22
+ bu ㄅㄨ
23
+ ca ㄘㄚ
24
+ cai ㄘㄞ
25
+ can ㄘㄢ
26
+ cang ㄘㄤ
27
+ cao ㄘㄠ
28
+ ce ㄘㄜ
29
+ cen ㄘㄣ
30
+ ceng ㄘㄥ
31
+ cha ㄔㄚ
32
+ chai ㄔㄞ
33
+ chan ㄔㄢ
34
+ chang ㄔㄤ
35
+ chao ㄔㄠ
36
+ che ㄔㄜ
37
+ chen ㄔㄣ
38
+ cheng ㄔㄥ
39
+ chi ㄔ
40
+ chong ㄔㄨㄥ
41
+ chou ㄔㄡ
42
+ chu ㄔㄨ
43
+ chua ㄔㄨㄚ
44
+ chuai ㄔㄨㄞ
45
+ chuan ㄔㄨㄢ
46
+ chuang ㄔㄨㄤ
47
+ chui ㄔㄨㄟ
48
+ chun ㄔㄨㄣ
49
+ chuo ㄔㄨㄛ
50
+ ci ㄘ
51
+ cong ㄘㄨㄥ
52
+ cou ㄘㄡ
53
+ cu ㄘㄨ
54
+ cuan ㄘㄨㄢ
55
+ cui ㄘㄨㄟ
56
+ cun ㄘㄨㄣ
57
+ cuo ㄘㄨㄛ
58
+ da ㄉㄚ
59
+ dai ㄉㄞ
60
+ dan ㄉㄢ
61
+ dang ㄉㄤ
62
+ dao ㄉㄠ
63
+ de ㄉㄜ
64
+ dei ㄉㄟ
65
+ den ㄉㄣ
66
+ deng ㄉㄥ
67
+ di ㄉㄧ
68
+ dia ㄉㄧㄚ
69
+ dian ㄉㄧㄢ
70
+ diao ㄉㄧㄠ
71
+ die ㄉㄧㄝ
72
+ din ㄉㄧㄣ
73
+ ding ㄉㄧㄥ
74
+ diu ㄉㄧㄡ
75
+ dong ㄉㄨㄥ
76
+ dou ㄉㄡ
77
+ du ㄉㄨ
78
+ duan ㄉㄨㄢ
79
+ dui ㄉㄨㄟ
80
+ dun ㄉㄨㄣ
81
+ duo ㄉㄨㄛ
82
+ e ㄜ
83
+ ei ㄟ
84
+ en ㄣ
85
+ eng ㄥ
86
+ er ㄦ
87
+ fa ㄈㄚ
88
+ fan ㄈㄢ
89
+ fang ㄈㄤ
90
+ fei ㄈㄟ
91
+ fen ㄈㄣ
92
+ feng ㄈㄥ
93
+ fo ㄈㄛ
94
+ fou ㄈㄡ
95
+ fu ㄈㄨ
96
+ ga ㄍㄚ
97
+ gai ㄍㄞ
98
+ gan ㄍㄢ
99
+ gang ㄍㄤ
100
+ gao ㄍㄠ
101
+ ge ㄍㄜ
102
+ gei ㄍㄟ
103
+ gen ㄍㄣ
104
+ geng ㄍㄥ
105
+ gong ㄍㄨㄥ
106
+ gou ㄍㄡ
107
+ gu ㄍㄨ
108
+ gua ㄍㄨㄚ
109
+ guai ㄍㄨㄞ
110
+ guan ㄍㄨㄢ
111
+ guang ㄍㄨㄤ
112
+ gui ㄍㄨㄟ
113
+ gun ㄍㄨㄣ
114
+ guo ㄍㄨㄛ
115
+ ha ㄏㄚ
116
+ hai ㄏㄞ
117
+ han ㄏㄢ
118
+ hang ㄏㄤ
119
+ hao ㄏㄠ
120
+ he ㄏㄜ
121
+ hei ㄏㄟ
122
+ hen ㄏㄣ
123
+ heng ㄏㄥ
124
+ hm ㄏㄇ
125
+ hong ㄏㄨㄥ
126
+ hou ㄏㄡ
127
+ hu ㄏㄨ
128
+ hua ㄏㄨㄚ
129
+ huai ㄏㄨㄞ
130
+ huan ㄏㄨㄢ
131
+ huang ㄏㄨㄤ
132
+ hui ㄏㄨㄟ
133
+ hun ㄏㄨㄣ
134
+ huo ㄏㄨㄛ
135
+ ji ㄐㄧ
136
+ jia ㄐㄧㄚ
137
+ jian ㄐㄧㄢ
138
+ jiang ㄐㄧㄤ
139
+ jiao ㄐㄧㄠ
140
+ jie ㄐㄧㄝ
141
+ jin ㄐㄧㄣ
142
+ jing ㄐㄧㄥ
143
+ jiong ㄐㄩㄥ
144
+ jiu ㄐㄧㄡ
145
+ ju ㄐㄩ
146
+ jv ㄐㄩ
147
+ juan ㄐㄩㄢ
148
+ jvan ㄐㄩㄢ
149
+ jue ㄐㄩㄝ
150
+ jve ㄐㄩㄝ
151
+ jun ㄐㄩㄣ
152
+ ka ㄎㄚ
153
+ kai ㄎㄞ
154
+ kan ㄎㄢ
155
+ kang ㄎㄤ
156
+ kao ㄎㄠ
157
+ ke ㄎㄜ
158
+ kei ㄎㄟ
159
+ ken ㄎㄣ
160
+ keng ㄎㄥ
161
+ kong ㄎㄨㄥ
162
+ kou ㄎㄡ
163
+ ku ㄎㄨ
164
+ kua ㄎㄨㄚ
165
+ kuai ㄎㄨㄞ
166
+ kuan ㄎㄨㄢ
167
+ kuang ㄎㄨㄤ
168
+ kui ㄎㄨㄟ
169
+ kun ㄎㄨㄣ
170
+ kuo ㄎㄨㄛ
171
+ la ㄌㄚ
172
+ lai ㄌㄞ
173
+ lan ㄌㄢ
174
+ lang ㄌㄤ
175
+ lao ㄌㄠ
176
+ le ㄌㄜ
177
+ lei ㄌㄟ
178
+ leng ㄌㄥ
179
+ li ㄌㄧ
180
+ lia ㄌㄧㄚ
181
+ lian ㄌㄧㄢ
182
+ liang ㄌㄧㄤ
183
+ liao ㄌㄧㄠ
184
+ lie ㄌㄧㄝ
185
+ lin ㄌㄧㄣ
186
+ ling ㄌㄧㄥ
187
+ liu ㄌㄧㄡ
188
+ lo ㄌㄛ
189
+ long ㄌㄨㄥ
190
+ lou ㄌㄡ
191
+ lu ㄌㄨ
192
+ luan ㄌㄨㄢ
193
+ lue ㄌㄩㄝ
194
+ lun ㄌㄨㄣ
195
+ luo ㄌㄨㄛ
196
+ lv ㄌㄩ
197
+ lve ㄌㄩㄝ
198
+ m ㄇㄨ
199
+ ma ㄇㄚ
200
+ mai ㄇㄞ
201
+ man ㄇㄢ
202
+ mang ㄇㄤ
203
+ mao ㄇㄠ
204
+ me ㄇㄜ
205
+ mei ㄇㄟ
206
+ men ㄇㄣ
207
+ meng ㄇㄥ
208
+ mi ㄇㄧ
209
+ mian ㄇㄧㄢ
210
+ miao ㄇㄧㄠ
211
+ mie ㄇㄧㄝ
212
+ min ㄇㄧㄣ
213
+ ming ㄇㄧㄥ
214
+ miu ㄇㄧㄡ
215
+ mo ㄇㄛ
216
+ mou ㄇㄡ
217
+ mu ㄇㄨ
218
+ n ㄣ
219
+ na ㄋㄚ
220
+ nai ㄋㄞ
221
+ nan ㄋㄢ
222
+ nang ㄋㄤ
223
+ nao ㄋㄠ
224
+ ne ㄋㄜ
225
+ nei ㄋㄟ
226
+ nen ㄋㄣ
227
+ neng ㄋㄥ
228
+ ng ㄣ
229
+ ni ㄋㄧ
230
+ nian ㄋㄧㄢ
231
+ niang ㄋㄧㄤ
232
+ niao ㄋㄧㄠ
233
+ nie ㄋㄧㄝ
234
+ nin ㄋㄧㄣ
235
+ ning ㄋㄧㄥ
236
+ niu ㄋㄧㄡ
237
+ nong ㄋㄨㄥ
238
+ nou ㄋㄡ
239
+ nu ㄋㄨ
240
+ nuan ㄋㄨㄢ
241
+ nue ㄋㄩㄝ
242
+ nun ㄋㄨㄣ
243
+ nuo ㄋㄨㄛ
244
+ nv ㄋㄩ
245
+ nve ㄋㄩㄝ
246
+ o ㄛ
247
+ ou ㄡ
248
+ pa ㄆㄚ
249
+ pai ㄆㄞ
250
+ pan ㄆㄢ
251
+ pang ㄆㄤ
252
+ pao ㄆㄠ
253
+ pei ㄆㄟ
254
+ pen ㄆㄣ
255
+ peng ㄆㄥ
256
+ pi ㄆㄧ
257
+ pian ㄆㄧㄢ
258
+ piao ㄆㄧㄠ
259
+ pie ㄆㄧㄝ
260
+ pin ㄆㄧㄣ
261
+ ping ㄆㄧㄥ
262
+ po ㄆㄛ
263
+ pou ㄆㄡ
264
+ pu ㄆㄨ
265
+ qi ㄑㄧ
266
+ qia ㄑㄧㄚ
267
+ qian ㄑㄧㄢ
268
+ qiang ㄑㄧㄤ
269
+ qiao ㄑㄧㄠ
270
+ qie ㄑㄧㄝ
271
+ qin ㄑㄧㄣ
272
+ qing ㄑㄧㄥ
273
+ qiong ㄑㄩㄥ
274
+ qiu ㄑㄧㄡ
275
+ qu ㄑㄩ
276
+ quan ㄑㄩㄢ
277
+ qvan ㄑㄩㄢ
278
+ que ㄑㄩㄝ
279
+ qun ㄑㄩㄣ
280
+ ran ㄖㄢ
281
+ rang ㄖㄤ
282
+ rao ㄖㄠ
283
+ re ㄖㄜ
284
+ ren ㄖㄣ
285
+ reng ㄖㄥ
286
+ ri ㄖ
287
+ rong ㄖㄨㄥ
288
+ rou ㄖㄡ
289
+ ru ㄖㄨ
290
+ rua ㄖㄨㄚ
291
+ ruan ㄖㄨㄢ
292
+ rui ㄖㄨㄟ
293
+ run ㄖㄨㄣ
294
+ ruo ㄖㄨㄛ
295
+ sa ㄙㄚ
296
+ sai ㄙㄞ
297
+ san ㄙㄢ
298
+ sang ㄙㄤ
299
+ sao ㄙㄠ
300
+ se ㄙㄜ
301
+ sen ㄙㄣ
302
+ seng ㄙㄥ
303
+ sha ㄕㄚ
304
+ shai ㄕㄞ
305
+ shan ㄕㄢ
306
+ shang ㄕㄤ
307
+ shao ㄕㄠ
308
+ she ㄕㄜ
309
+ shei ㄕㄟ
310
+ shen ㄕㄣ
311
+ sheng ㄕㄥ
312
+ shi ㄕ
313
+ shou ㄕㄡ
314
+ shu ㄕㄨ
315
+ shua ㄕㄨㄚ
316
+ shuai ㄕㄨㄞ
317
+ shuan ㄕㄨㄢ
318
+ shuang ㄕㄨㄤ
319
+ shui ㄕㄨㄟ
320
+ shun ㄕㄨㄣ
321
+ shuo ㄕㄨㄛ
322
+ si ㄙ
323
+ song ㄙㄨㄥ
324
+ sou ㄙㄡ
325
+ su ㄙㄨ
326
+ suan ㄙㄨㄢ
327
+ sui ㄙㄨㄟ
328
+ sun ㄙㄨㄣ
329
+ suo ㄙㄨㄛ
330
+ ta ㄊㄚ
331
+ tai ㄊㄞ
332
+ tan ㄊㄢ
333
+ tang ㄊㄤ
334
+ tao ㄊㄠ
335
+ te ㄊㄜ
336
+ tei ㄊㄟ
337
+ teng ㄊㄥ
338
+ ti ㄊㄧ
339
+ tian ㄊㄧㄢ
340
+ tiao ㄊㄧㄠ
341
+ tie ㄊㄧㄝ
342
+ ting ㄊㄧㄥ
343
+ tong ㄊㄨㄥ
344
+ tou ㄊㄡ
345
+ tsuo ㄘㄨㄛ
346
+ tu ㄊㄨ
347
+ tuan ㄊㄨㄢ
348
+ tui ㄊㄨㄟ
349
+ tun ㄊㄨㄣ
350
+ tuo ㄊㄨㄛ
351
+ tzan ㄗㄢ
352
+ wa ㄨㄚ
353
+ wai ㄨㄞ
354
+ wan ㄨㄢ
355
+ wang ㄨㄤ
356
+ wei ㄨㄟ
357
+ wen ㄨㄣ
358
+ weng ㄨㄥ
359
+ wo ㄨㄛ
360
+ wong ㄨㄥ
361
+ wu ㄨ
362
+ xi ㄒㄧ
363
+ xia ㄒㄧㄚ
364
+ xian ㄒㄧㄢ
365
+ xiang ㄒㄧㄤ
366
+ xiao ㄒㄧㄠ
367
+ xie ㄒㄧㄝ
368
+ xin ㄒㄧㄣ
369
+ xing ㄒㄧㄥ
370
+ xiong ㄒㄩㄥ
371
+ xiu ㄒㄧㄡ
372
+ xu ㄒㄩ
373
+ xuan ㄒㄩㄢ
374
+ xue ㄒㄩㄝ
375
+ xun ㄒㄩㄣ
376
+ ya ㄧㄚ
377
+ yai ㄧㄞ
378
+ yan ㄧㄢ
379
+ yang ㄧㄤ
380
+ yao ㄧㄠ
381
+ ye ㄧㄝ
382
+ yi ㄧ
383
+ yin ㄧㄣ
384
+ ying ㄧㄥ
385
+ yo ㄧㄛ
386
+ yong ㄩㄥ
387
+ you ㄧㄡ
388
+ yu ㄩ
389
+ yuan ㄩㄢ
390
+ yue ㄩㄝ
391
+ yve ㄩㄝ
392
+ yun ㄩㄣ
393
+ za ㄗㄚ
394
+ zai ㄗㄞ
395
+ zan ㄗㄢ
396
+ zang ㄗㄤ
397
+ zao ㄗㄠ
398
+ ze ㄗㄜ
399
+ zei ㄗㄟ
400
+ zen ㄗㄣ
401
+ zeng ㄗㄥ
402
+ zha ㄓㄚ
403
+ zhai ㄓㄞ
404
+ zhan ㄓㄢ
405
+ zhang ㄓㄤ
406
+ zhao ㄓㄠ
407
+ zhe ㄓㄜ
408
+ zhei ㄓㄟ
409
+ zhen ㄓㄣ
410
+ zheng ㄓㄥ
411
+ zhi ㄓ
412
+ zhong ㄓㄨㄥ
413
+ zhou ㄓㄡ
414
+ zhu ㄓㄨ
415
+ zhua ㄓㄨㄚ
416
+ zhuai ㄓㄨㄞ
417
+ zhuan ㄓㄨㄢ
418
+ zhuang ㄓㄨㄤ
419
+ zhui ㄓㄨㄟ
420
+ zhun ㄓㄨㄣ
421
+ zhuo ㄓㄨㄛ
422
+ zi ㄗ
423
+ zong ㄗㄨㄥ
424
+ zou ㄗㄡ
425
+ zu ㄗㄨ
426
+ zuan ㄗㄨㄢ
427
+ zui ㄗㄨㄟ
428
+ zun ㄗㄨㄣ
429
+ zuo ㄗㄨㄛ
code/infer/infer.py ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025 ASLP-LAB
2
+ # 2025 Huakang Chen (huakang@mail.nwpu.edu.cn)
3
+ # 2025 Guobin Ma (guobin.ma@gmail.com)
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ import argparse
18
+ import os
19
+ import time
20
+ import random
21
+
22
+ import torch
23
+ import torchaudio
24
+ from einops import rearrange
25
+
26
+ print("Current working directory:", os.getcwd())
27
+
28
+ from infer_utils import (
29
+ decode_audio,
30
+ get_lrc_token,
31
+ get_negative_style_prompt,
32
+ get_reference_latent,
33
+ get_style_prompt,
34
+ prepare_model,
35
+ )
36
+
37
+
38
+ def inference(
39
+ cfm_model,
40
+ vae_model,
41
+ cond,
42
+ text,
43
+ duration,
44
+ style_prompt,
45
+ negative_style_prompt,
46
+ start_time,
47
+ pred_frames,
48
+ batch_infer_num,
49
+ song_duration,
50
+ chunked=False,
51
+ ):
52
+ with torch.inference_mode():
53
+ latents, _ = cfm_model.sample(
54
+ cond=cond,
55
+ text=text,
56
+ duration=duration,
57
+ style_prompt=style_prompt,
58
+ max_duration=duration,
59
+ song_duration=song_duration,
60
+ negative_style_prompt=negative_style_prompt,
61
+ steps=32,
62
+ cfg_strength=4.0,
63
+ start_time=start_time,
64
+ latent_pred_segments=pred_frames,
65
+ batch_infer_num=batch_infer_num
66
+ )
67
+
68
+ outputs = []
69
+ for latent in latents:
70
+ latent = latent.to(torch.float32)
71
+ latent = latent.transpose(1, 2) # [b d t]
72
+
73
+ output = decode_audio(latent, vae_model, chunked=chunked)
74
+
75
+ # Rearrange audio batch to a single sequence
76
+ output = rearrange(output, "b d n -> d (b n)")
77
+ # Peak normalize, clip, convert to int16, and save to file
78
+ output = (
79
+ output.to(torch.float32)
80
+ .div(torch.max(torch.abs(output)))
81
+ .clamp(-1, 1)
82
+ .mul(32767)
83
+ .to(torch.int16)
84
+ .cpu()
85
+ )
86
+ outputs.append(output)
87
+
88
+ return outputs
89
+
90
+
91
+ if __name__ == "__main__":
92
+ parser = argparse.ArgumentParser()
93
+ parser.add_argument(
94
+ "--lrc-path",
95
+ type=str,
96
+ help="lyrics of target song",
97
+ ) # lyrics of target song
98
+ parser.add_argument(
99
+ "--ref-prompt",
100
+ type=str,
101
+ help="reference prompt as style prompt for target song",
102
+ required=False,
103
+ ) # reference prompt as style prompt for target song
104
+ parser.add_argument(
105
+ "--ref-audio-path",
106
+ type=str,
107
+ help="reference audio as style prompt for target song",
108
+ required=False,
109
+ ) # reference audio as style prompt for target song
110
+ parser.add_argument(
111
+ "--chunked",
112
+ action="store_true",
113
+ help="whether to use chunked decoding",
114
+ ) # whether to use chunked decoding
115
+ parser.add_argument(
116
+ "--audio-length",
117
+ type=int,
118
+ default=95,
119
+ # choices=[95, 285],
120
+ help="length of generated song, upported values are exactly 95 or any value between 96 and 285 (inclusive).",
121
+ ) # length of target song
122
+ # parser.add_argument(
123
+ # "--repo-id", type=str, default="ASLP-lab/DiffRhythm-base", help="target model"
124
+ # )
125
+ parser.add_argument(
126
+ "--output-dir",
127
+ type=str,
128
+ default="infer/example/output",
129
+ help="output directory fo generated song",
130
+ ) # output directory of target song
131
+ parser.add_argument(
132
+ "--edit",
133
+ action="store_true",
134
+ help="whether to open edit mode",
135
+ ) # edit flag
136
+ parser.add_argument(
137
+ "--ref-song",
138
+ type=str,
139
+ required=False,
140
+ help="reference prompt as latent prompt for editing",
141
+ ) # reference prompt as latent prompt for editing
142
+ parser.add_argument(
143
+ "--edit-segments",
144
+ type=str,
145
+ required=False,
146
+ help="Time segments to edit (in seconds). Format: `[[start1,end1],...]`. "
147
+ "Use `-1` for audio start/end (e.g., `[[-1,25], [50.0,-1]]`)."
148
+ ) # edit segments of target song
149
+ parser.add_argument(
150
+ "--batch-infer-num",
151
+ type=int,
152
+ default=1,
153
+ required=False,
154
+ help="number of songs per batch",
155
+ ) # number of songs per batch
156
+ args = parser.parse_args()
157
+
158
+ assert (
159
+ args.ref_prompt or args.ref_audio_path
160
+ ), "either ref_prompt or ref_audio_path should be provided"
161
+ assert not (
162
+ args.ref_prompt and args.ref_audio_path
163
+ ), "only one of them should be provided"
164
+ if args.edit:
165
+ assert (
166
+ args.ref_song and args.edit_segments
167
+ ), "reference song and edit segments should be provided for editing"
168
+
169
+ device = "cpu"
170
+ if torch.cuda.is_available():
171
+ device = "cuda"
172
+ elif torch.mps.is_available():
173
+ device = "mps"
174
+
175
+ audio_length = args.audio_length
176
+ if audio_length == 95:
177
+ max_frames = 2048
178
+ elif 95 < audio_length <= 285:
179
+ max_frames = 6144
180
+ else:
181
+ raise ValueError(
182
+ f"Invalid audio_length: {audio_length}. "
183
+ "Supported values are exactly 95 or any value between 96 and 285 (inclusive)."
184
+ )
185
+
186
+ cfm, tokenizer, muq, vae = prepare_model(max_frames, device)
187
+
188
+ if args.lrc_path:
189
+ with open(args.lrc_path, "r", encoding='utf-8') as f:
190
+ lrc = f.read()
191
+ else:
192
+ lrc = ""
193
+ lrc_prompt, start_time, end_frame, song_duration = get_lrc_token(max_frames, lrc, tokenizer, audio_length, device)
194
+
195
+ if args.ref_audio_path:
196
+ style_prompt = get_style_prompt(muq, args.ref_audio_path)
197
+ else:
198
+ style_prompt = get_style_prompt(muq, prompt=args.ref_prompt)
199
+
200
+ negative_style_prompt = get_negative_style_prompt(device)
201
+
202
+ latent_prompt, pred_frames = get_reference_latent(device, max_frames, args.edit, args.edit_segments, args.ref_song, vae)
203
+
204
+ s_t = time.time()
205
+ generated_songs = inference(
206
+ cfm_model=cfm,
207
+ vae_model=vae,
208
+ cond=latent_prompt,
209
+ text=lrc_prompt,
210
+ duration=end_frame,
211
+ style_prompt=style_prompt,
212
+ negative_style_prompt=negative_style_prompt,
213
+ start_time=start_time,
214
+ pred_frames=pred_frames,
215
+ chunked=args.chunked,
216
+ batch_infer_num=args.batch_infer_num,
217
+ song_duration=song_duration
218
+ )
219
+ e_t = time.time() - s_t
220
+ print(f"inference cost {e_t:.2f} seconds")
221
+
222
+ generated_song = random.sample(generated_songs, 1)[0]
223
+
224
+ output_dir = args.output_dir
225
+ os.makedirs(output_dir, exist_ok=True)
226
+
227
+ output_path = os.path.join(output_dir, "output.wav")
228
+ torchaudio.save(output_path, generated_song, sample_rate=44100)
code/infer/infer_utils.py ADDED
@@ -0,0 +1,459 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025 ASLP-LAB
2
+ # 2025 Huakang Chen (huakang@mail.nwpu.edu.cn)
3
+ # 2025 Guobin Ma (guobin.ma@gmail.com)
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ import torch
18
+ import librosa
19
+ import torchaudio
20
+ import random
21
+ import json
22
+ from muq import MuQMuLan
23
+ from mutagen.mp3 import MP3
24
+ import os
25
+ import numpy as np
26
+ from huggingface_hub import hf_hub_download
27
+
28
+ from sys import path
29
+ path.append(os.getcwd())
30
+
31
+ from model import DiT, CFM
32
+
33
+ def vae_sample(mean, scale):
34
+ stdev = torch.nn.functional.softplus(scale) + 1e-4
35
+ var = stdev * stdev
36
+ logvar = torch.log(var)
37
+ latents = torch.randn_like(mean) * stdev + mean
38
+
39
+ kl = (mean * mean + var - logvar - 1).sum(1).mean()
40
+
41
+ return latents, kl
42
+
43
+ def normalize_audio(y, target_dbfs=0):
44
+ max_amplitude = torch.max(torch.abs(y))
45
+
46
+ target_amplitude = 10.0**(target_dbfs / 20.0)
47
+ scale_factor = target_amplitude / max_amplitude
48
+
49
+ normalized_audio = y * scale_factor
50
+
51
+ return normalized_audio
52
+
53
+ def set_audio_channels(audio, target_channels):
54
+ if target_channels == 1:
55
+ # Convert to mono
56
+ audio = audio.mean(1, keepdim=True)
57
+ elif target_channels == 2:
58
+ # Convert to stereo
59
+ if audio.shape[1] == 1:
60
+ audio = audio.repeat(1, 2, 1)
61
+ elif audio.shape[1] > 2:
62
+ audio = audio[:, :2, :]
63
+ return audio
64
+
65
+ class PadCrop(torch.nn.Module):
66
+ def __init__(self, n_samples, randomize=True):
67
+ super().__init__()
68
+ self.n_samples = n_samples
69
+ self.randomize = randomize
70
+
71
+ def __call__(self, signal):
72
+ n, s = signal.shape
73
+ start = 0 if (not self.randomize) else torch.randint(0, max(0, s - self.n_samples) + 1, []).item()
74
+ end = start + self.n_samples
75
+ output = signal.new_zeros([n, self.n_samples])
76
+ output[:, :min(s, self.n_samples)] = signal[:, start:end]
77
+ return output
78
+
79
+ def prepare_audio(audio, in_sr, target_sr, target_length, target_channels, device):
80
+
81
+ audio = audio.to(device)
82
+
83
+ if in_sr != target_sr:
84
+ resample_tf = torchaudio.functional.Resample(in_sr, target_sr).to(device)
85
+ audio = resample_tf(audio)
86
+ if target_length is None:
87
+ target_length = audio.shape[-1]
88
+ audio = PadCrop(target_length, randomize=False)(audio)
89
+
90
+ # Add batch dimension
91
+ if audio.dim() == 1:
92
+ audio = audio.unsqueeze(0).unsqueeze(0)
93
+ elif audio.dim() == 2:
94
+ audio = audio.unsqueeze(0)
95
+
96
+ audio = set_audio_channels(audio, target_channels)
97
+
98
+ return audio
99
+
100
+ def decode_audio(latents, vae_model, chunked=False, overlap=32, chunk_size=128):
101
+ downsampling_ratio = 2048
102
+ io_channels = 2
103
+ if not chunked:
104
+ return vae_model.decode_export(latents)
105
+ else:
106
+ # chunked decoding
107
+ hop_size = chunk_size - overlap
108
+ total_size = latents.shape[2]
109
+ batch_size = latents.shape[0]
110
+ chunks = []
111
+ i = 0
112
+ for i in range(0, total_size - chunk_size + 1, hop_size):
113
+ chunk = latents[:, :, i : i + chunk_size]
114
+ chunks.append(chunk)
115
+ if i + chunk_size != total_size:
116
+ # Final chunk
117
+ chunk = latents[:, :, -chunk_size:]
118
+ chunks.append(chunk)
119
+ chunks = torch.stack(chunks)
120
+ num_chunks = chunks.shape[0]
121
+ # samples_per_latent is just the downsampling ratio
122
+ samples_per_latent = downsampling_ratio
123
+ # Create an empty waveform, we will populate it with chunks as decode them
124
+ y_size = total_size * samples_per_latent
125
+ y_final = torch.zeros((batch_size, io_channels, y_size)).to(latents.device)
126
+ for i in range(num_chunks):
127
+ x_chunk = chunks[i, :]
128
+ # decode the chunk
129
+ y_chunk = vae_model.decode_export(x_chunk)
130
+ # figure out where to put the audio along the time domain
131
+ if i == num_chunks - 1:
132
+ # final chunk always goes at the end
133
+ t_end = y_size
134
+ t_start = t_end - y_chunk.shape[2]
135
+ else:
136
+ t_start = i * hop_size * samples_per_latent
137
+ t_end = t_start + chunk_size * samples_per_latent
138
+ # remove the edges of the overlaps
139
+ ol = (overlap // 2) * samples_per_latent
140
+ chunk_start = 0
141
+ chunk_end = y_chunk.shape[2]
142
+ if i > 0:
143
+ # no overlap for the start of the first chunk
144
+ t_start += ol
145
+ chunk_start += ol
146
+ if i < num_chunks - 1:
147
+ # no overlap for the end of the last chunk
148
+ t_end -= ol
149
+ chunk_end -= ol
150
+ # paste the chunked audio into our y_final output audio
151
+ y_final[:, :, t_start:t_end] = y_chunk[:, :, chunk_start:chunk_end]
152
+ return y_final
153
+
154
+ def encode_audio(audio, vae_model, chunked=False, overlap=32, chunk_size=128):
155
+ downsampling_ratio = 2048
156
+ latent_dim = 128
157
+ if not chunked:
158
+ # default behavior. Encode the entire audio in parallel
159
+ return vae_model.encode_export(audio)
160
+ else:
161
+ # CHUNKED ENCODING
162
+ # samples_per_latent is just the downsampling ratio (which is also the upsampling ratio)
163
+ samples_per_latent = downsampling_ratio
164
+ total_size = audio.shape[2] # in samples
165
+ batch_size = audio.shape[0]
166
+ chunk_size *= samples_per_latent # converting metric in latents to samples
167
+ overlap *= samples_per_latent # converting metric in latents to samples
168
+ hop_size = chunk_size - overlap
169
+ chunks = []
170
+ for i in range(0, total_size - chunk_size + 1, hop_size):
171
+ chunk = audio[:,:,i:i+chunk_size]
172
+ chunks.append(chunk)
173
+ if i+chunk_size != total_size:
174
+ # Final chunk
175
+ chunk = audio[:,:,-chunk_size:]
176
+ chunks.append(chunk)
177
+ chunks = torch.stack(chunks)
178
+ num_chunks = chunks.shape[0]
179
+ # Note: y_size might be a different value from the latent length used in diffusion training
180
+ # because we can encode audio of varying lengths
181
+ # However, the audio should've been padded to a multiple of samples_per_latent by now.
182
+ y_size = total_size // samples_per_latent
183
+ # Create an empty latent, we will populate it with chunks as we encode them
184
+ y_final = torch.zeros((batch_size,latent_dim,y_size)).to(audio.device)
185
+ for i in range(num_chunks):
186
+ x_chunk = chunks[i,:]
187
+ # encode the chunk
188
+ y_chunk = vae_model.encode_export(x_chunk)
189
+ # figure out where to put the audio along the time domain
190
+ if i == num_chunks-1:
191
+ # final chunk always goes at the end
192
+ t_end = y_size
193
+ t_start = t_end - y_chunk.shape[2]
194
+ else:
195
+ t_start = i * hop_size // samples_per_latent
196
+ t_end = t_start + chunk_size // samples_per_latent
197
+ # remove the edges of the overlaps
198
+ ol = overlap//samples_per_latent//2
199
+ chunk_start = 0
200
+ chunk_end = y_chunk.shape[2]
201
+ if i > 0:
202
+ # no overlap for the start of the first chunk
203
+ t_start += ol
204
+ chunk_start += ol
205
+ if i < num_chunks-1:
206
+ # no overlap for the end of the last chunk
207
+ t_end -= ol
208
+ chunk_end -= ol
209
+ # paste the chunked audio into our y_final output audio
210
+ y_final[:,:,t_start:t_end] = y_chunk[:,:,chunk_start:chunk_end]
211
+ return y_final
212
+
213
+ def prepare_model(max_frames, device, repo_id="ASLP-lab/DiffRhythm-1_2"):
214
+ # prepare cfm model
215
+ if max_frames == 2048:
216
+ repo_id = "ASLP-lab/DiffRhythm-1_2"
217
+ else:
218
+ repo_id = "ASLP-lab/DiffRhythm-1_2-full"
219
+
220
+ dit_ckpt_path = hf_hub_download(
221
+ repo_id=repo_id, filename="cfm_model.pt", cache_dir="./pretrained"
222
+ )
223
+
224
+ dit_config_path = "./config/diffrhythm-1b.json"
225
+ with open(dit_config_path) as f:
226
+ model_config = json.load(f)
227
+ dit_model_cls = DiT
228
+ cfm = CFM(
229
+ transformer=dit_model_cls(**model_config["model"], max_frames=max_frames),
230
+ num_channels=model_config["model"]["mel_dim"],
231
+ max_frames=max_frames
232
+ )
233
+ cfm = cfm.to(device)
234
+ cfm = load_checkpoint(cfm, dit_ckpt_path, device=device, use_ema=False)
235
+
236
+ # prepare tokenizer
237
+ tokenizer = CNENTokenizer()
238
+
239
+ # prepare muq
240
+ muq = MuQMuLan.from_pretrained("OpenMuQ/MuQ-MuLan-large", cache_dir="./pretrained")
241
+ muq = muq.to(device).eval()
242
+
243
+ # prepare vae
244
+ vae_ckpt_path = hf_hub_download(
245
+ repo_id="ASLP-lab/DiffRhythm-vae",
246
+ filename="vae_model.pt",
247
+ cache_dir="./pretrained",
248
+ )
249
+ vae = torch.jit.load(vae_ckpt_path, map_location="cpu").to(device)
250
+
251
+ return cfm, tokenizer, muq, vae
252
+
253
+
254
+ # for song edit, will be added in the future
255
+ def get_reference_latent(device, max_frames, edit, pred_segments, ref_song, vae_model):
256
+ sampling_rate = 44100
257
+ downsample_rate = 2048
258
+ io_channels = 2
259
+ if edit:
260
+ input_audio, in_sr = torchaudio.load(ref_song)
261
+ input_audio = prepare_audio(input_audio, in_sr=in_sr, target_sr=sampling_rate, target_length=None, target_channels=io_channels, device=device)
262
+ input_audio = normalize_audio(input_audio, -6)
263
+
264
+ with torch.no_grad():
265
+ latent = encode_audio(input_audio, vae_model, chunked=True) # [b d t]
266
+ mean, scale = latent.chunk(2, dim=1)
267
+ prompt, _ = vae_sample(mean, scale)
268
+ prompt = prompt.transpose(1, 2) # [b t d]
269
+
270
+ pred_segments = json.loads(pred_segments)
271
+
272
+ pred_frames = []
273
+ for st, et in pred_segments:
274
+ sf = 0 if st == -1 else int(st * sampling_rate / downsample_rate)
275
+ ef = max_frames if et == -1 else int(et * sampling_rate / downsample_rate)
276
+ pred_frames.append((sf, ef))
277
+
278
+ return prompt, pred_frames
279
+ else:
280
+ prompt = torch.zeros(1, max_frames, 64).to(device)
281
+ pred_frames = [(0, max_frames)]
282
+ return prompt, pred_frames
283
+
284
+
285
+ def get_negative_style_prompt(device):
286
+ file_path = "infer/example/vocal.npy"
287
+ vocal_stlye = np.load(file_path)
288
+
289
+ vocal_stlye = torch.from_numpy(vocal_stlye).to(device) # [1, 512]
290
+ vocal_stlye = vocal_stlye.half()
291
+
292
+ return vocal_stlye
293
+
294
+
295
+ @torch.no_grad()
296
+ def get_style_prompt(model, wav_path=None, prompt=None):
297
+ mulan = model
298
+
299
+ if prompt is not None:
300
+ return mulan(texts=prompt).half()
301
+
302
+ ext = os.path.splitext(wav_path)[-1].lower()
303
+ if ext == ".mp3":
304
+ meta = MP3(wav_path)
305
+ audio_len = meta.info.length
306
+ elif ext in [".wav", ".flac"]:
307
+ audio_len = librosa.get_duration(path=wav_path)
308
+ else:
309
+ raise ValueError("Unsupported file format: {}".format(ext))
310
+
311
+ if audio_len < 10:
312
+ print(
313
+ f"Warning: The audio file {wav_path} is too short ({audio_len:.2f} seconds). Expected at least 10 seconds."
314
+ )
315
+
316
+ assert audio_len >= 10
317
+
318
+ mid_time = audio_len // 2
319
+ start_time = mid_time - 5
320
+ wav, _ = librosa.load(wav_path, sr=24000, offset=start_time, duration=10)
321
+
322
+ wav = torch.tensor(wav).unsqueeze(0).to(model.device)
323
+
324
+ with torch.no_grad():
325
+ audio_emb = mulan(wavs=wav) # [1, 512]
326
+
327
+ audio_emb = audio_emb
328
+ audio_emb = audio_emb.half()
329
+
330
+ return audio_emb
331
+
332
+
333
+ def parse_lyrics(lyrics: str):
334
+ lyrics_with_time = []
335
+ lyrics = lyrics.strip()
336
+ for line in lyrics.split("\n"):
337
+ try:
338
+ time, lyric = line[1:9], line[10:]
339
+ lyric = lyric.strip()
340
+ mins, secs = time.split(":")
341
+ secs = int(mins) * 60 + float(secs)
342
+ lyrics_with_time.append((secs, lyric))
343
+ except:
344
+ continue
345
+ return lyrics_with_time
346
+
347
+
348
+ class CNENTokenizer:
349
+ def __init__(self):
350
+ with open("./g2p/g2p/vocab.json", "r", encoding='utf-8') as file:
351
+ self.phone2id: dict = json.load(file)["vocab"]
352
+ self.id2phone = {v: k for (k, v) in self.phone2id.items()}
353
+ from g2p.g2p_generation import chn_eng_g2p
354
+
355
+ self.tokenizer = chn_eng_g2p
356
+
357
+ def encode(self, text):
358
+ phone, token = self.tokenizer(text)
359
+ token = [x + 1 for x in token]
360
+ return token
361
+
362
+ def decode(self, token):
363
+ return "|".join([self.id2phone[x - 1] for x in token])
364
+
365
+
366
+ def get_lrc_token(max_frames, text, tokenizer, max_secs, device):
367
+
368
+ lyrics_shift = 0
369
+ sampling_rate = 44100
370
+ downsample_rate = 2048
371
+ # max_secs = max_frames / (sampling_rate / downsample_rate)
372
+
373
+ comma_token_id = 1
374
+ period_token_id = 2
375
+
376
+ lrc_with_time = parse_lyrics(text)
377
+
378
+ modified_lrc_with_time = []
379
+ for i in range(len(lrc_with_time)):
380
+ time, line = lrc_with_time[i]
381
+ line_token = tokenizer.encode(line)
382
+ modified_lrc_with_time.append((time, line_token))
383
+ lrc_with_time = modified_lrc_with_time
384
+
385
+ lrc_with_time = [
386
+ (time_start, line)
387
+ for (time_start, line) in lrc_with_time
388
+ if time_start < max_secs
389
+ ]
390
+ if max_frames == 2048:
391
+ lrc_with_time = lrc_with_time[:-1] if len(lrc_with_time) >= 1 else lrc_with_time
392
+
393
+ end_frame = max_frames if max_frames == 2048 else int(max_secs * (sampling_rate / downsample_rate))
394
+ end_frame = min(end_frame, max_frames)
395
+
396
+ normalized_start_time = 0.0
397
+
398
+ normalized_duration = end_frame / max_frames
399
+
400
+ lrc = torch.zeros((end_frame,), dtype=torch.long)
401
+
402
+ tokens_count = 0
403
+ last_end_pos = 0
404
+ for time_start, line in lrc_with_time:
405
+ tokens = [
406
+ token if token != period_token_id else comma_token_id for token in line
407
+ ] + [period_token_id]
408
+ tokens = torch.tensor(tokens, dtype=torch.long)
409
+ num_tokens = tokens.shape[0]
410
+
411
+ gt_frame_start = int(time_start * sampling_rate / downsample_rate)
412
+
413
+ frame_shift = random.randint(int(-lyrics_shift), int(lyrics_shift))
414
+
415
+ frame_start = max(gt_frame_start - frame_shift, last_end_pos)
416
+ frame_len = min(num_tokens, end_frame - frame_start)
417
+
418
+ lrc[frame_start : frame_start + frame_len] = tokens[:frame_len]
419
+
420
+ tokens_count += num_tokens
421
+ last_end_pos = frame_start + frame_len
422
+
423
+ lrc_emb = lrc.unsqueeze(0).to(device)
424
+
425
+ normalized_start_time = torch.tensor(normalized_start_time).unsqueeze(0).to(device)
426
+ normalized_start_time = normalized_start_time.half()
427
+
428
+ normalized_duration = torch.tensor(normalized_duration).unsqueeze(0).to(device)
429
+ normalized_duration = normalized_duration.half()
430
+
431
+ return lrc_emb, normalized_start_time, end_frame, normalized_duration
432
+
433
+
434
+ def load_checkpoint(model, ckpt_path, device, use_ema=True):
435
+ model = model.half()
436
+
437
+ ckpt_type = ckpt_path.split(".")[-1]
438
+ if ckpt_type == "safetensors":
439
+ from safetensors.torch import load_file
440
+
441
+ checkpoint = load_file(ckpt_path)
442
+ else:
443
+ checkpoint = torch.load(ckpt_path, weights_only=True)
444
+
445
+ if use_ema:
446
+ if ckpt_type == "safetensors":
447
+ checkpoint = {"ema_model_state_dict": checkpoint}
448
+ checkpoint["model_state_dict"] = {
449
+ k.replace("ema_model.", ""): v
450
+ for k, v in checkpoint["ema_model_state_dict"].items()
451
+ if k not in ["initted", "step"]
452
+ }
453
+ model.load_state_dict(checkpoint["model_state_dict"], strict=False)
454
+ else:
455
+ if ckpt_type == "safetensors":
456
+ checkpoint = {"model_state_dict": checkpoint}
457
+ model.load_state_dict(checkpoint["model_state_dict"], strict=False)
458
+
459
+ return model.to(device)
code/model/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from model.cfm import CFM
2
+ from model.dit import DiT
3
+ from model.trainer import Trainer
4
+
5
+
6
+ __all__ = ["CFM"]
code/model/cfm.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025 ASLP-LAB
2
+ # 2025 Ziqian Ning (ningziqian@mail.nwpu.edu.cn)
3
+ # 2025 Huakang Chen (huakang@mail.nwpu.edu.cn)
4
+ # 2025 Guobin Ma (guobin.ma@mail.nwpu.edu.cn)
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ """ This implementation is adapted from github repo:
19
+ https://github.com/SWivid/F5-TTS.
20
+ """
21
+
22
+ from __future__ import annotations
23
+ from typing import Callable
24
+ from random import random
25
+
26
+ import torch
27
+ from torch import nn
28
+ import torch
29
+ import torch.nn.functional as F
30
+ from torch.nn.utils.rnn import pad_sequence
31
+
32
+ from torchdiffeq import odeint
33
+
34
+ from model.utils import (
35
+ exists,
36
+ list_str_to_idx,
37
+ list_str_to_tensor,
38
+ lens_to_mask,
39
+ mask_from_frac_lengths,
40
+ )
41
+
42
+ def custom_mask_from_start_end_indices(
43
+ seq_len: int["b"], # noqa: F821
44
+ latent_pred_segments,
45
+ device,
46
+ max_seq_len
47
+ ):
48
+ max_seq_len = max_seq_len
49
+ seq = torch.arange(max_seq_len, device=device).long()
50
+
51
+ res_mask = torch.zeros(max_seq_len, device=device, dtype=torch.bool)
52
+
53
+ for start, end in latent_pred_segments:
54
+ start = start.unsqueeze(0)
55
+ end = end.unsqueeze(0)
56
+ start_mask = seq[None, :] >= start[:, None]
57
+ end_mask = seq[None, :] < end[:, None]
58
+ res_mask = res_mask | (start_mask & end_mask)
59
+
60
+ return res_mask
61
+
62
+ class CFM(nn.Module):
63
+ def __init__(
64
+ self,
65
+ transformer: nn.Module,
66
+ sigma=0.0,
67
+ odeint_kwargs: dict = dict(
68
+ method="euler"
69
+ ),
70
+ odeint_options: dict = dict(
71
+ min_step=0.05
72
+ ),
73
+ audio_drop_prob=0.3,
74
+ cond_drop_prob=0.2,
75
+ style_drop_prob=0.1,
76
+ lrc_drop_prob=0.1,
77
+ num_channels=None,
78
+ frac_lengths_mask: tuple[float, float] = (0.7, 1.0),
79
+ vocab_char_map: dict[str:int] | None = None,
80
+ max_frames=2048
81
+ ):
82
+ super().__init__()
83
+
84
+ self.frac_lengths_mask = frac_lengths_mask
85
+
86
+ self.num_channels = num_channels
87
+
88
+ # classifier-free guidance
89
+ self.audio_drop_prob = audio_drop_prob
90
+ self.cond_drop_prob = cond_drop_prob
91
+ self.style_drop_prob = style_drop_prob
92
+ self.lrc_drop_prob = lrc_drop_prob
93
+
94
+ # transformer
95
+ self.transformer = transformer
96
+ dim = transformer.dim
97
+ self.dim = dim
98
+
99
+ # conditional flow related
100
+ self.sigma = sigma
101
+
102
+ # sampling related
103
+ self.odeint_kwargs = odeint_kwargs
104
+
105
+ self.odeint_options = odeint_options
106
+
107
+ # vocab map for tokenization
108
+ self.vocab_char_map = vocab_char_map
109
+
110
+ self.max_frames = max_frames
111
+
112
+ @property
113
+ def device(self):
114
+ return next(self.parameters()).device
115
+
116
+ @torch.no_grad()
117
+ def sample(
118
+ self,
119
+ cond: float["b n d"] | float["b nw"], # noqa: F722
120
+ text: int["b nt"] | list[str], # noqa: F722
121
+ duration: int | int["b"], # noqa: F821
122
+ *,
123
+ style_prompt = None,
124
+ style_prompt_lens = None,
125
+ negative_style_prompt = None,
126
+ lens: int["b"] | None = None, # noqa: F821
127
+ steps=32,
128
+ cfg_strength=4.0,
129
+ sway_sampling_coef=None,
130
+ seed: int | None = None,
131
+ max_duration=6144,
132
+ vocoder: Callable[[float["b d n"]], float["b nw"]] | None = None, # noqa: F722
133
+ no_ref_audio=False,
134
+ duplicate_test=False,
135
+ t_inter=0.1,
136
+ edit_mask=None,
137
+ start_time=None,
138
+ latent_pred_segments=None,
139
+ song_duration=None,
140
+ batch_infer_num=1
141
+ ):
142
+ self.eval()
143
+
144
+ if next(self.parameters()).dtype == torch.float16:
145
+ cond = cond.half()
146
+
147
+ # raw wave
148
+ if cond.shape[1] > duration:
149
+ cond = cond[:, :duration, :]
150
+
151
+ if cond.ndim == 2:
152
+ cond = self.mel_spec(cond)
153
+ cond = cond.permute(0, 2, 1)
154
+ assert cond.shape[-1] == self.num_channels
155
+
156
+ batch, cond_seq_len, device = *cond.shape[:2], cond.device
157
+ if not exists(lens):
158
+ lens = torch.full((batch,), cond_seq_len, device=device, dtype=torch.long)
159
+
160
+ # text
161
+ if isinstance(text, list):
162
+ if exists(self.vocab_char_map):
163
+ text = list_str_to_idx(text, self.vocab_char_map).to(device)
164
+ else:
165
+ text = list_str_to_tensor(text).to(device)
166
+ assert text.shape[0] == batch
167
+
168
+ # duration
169
+ cond_mask = lens_to_mask(lens)
170
+ if edit_mask is not None:
171
+ cond_mask = cond_mask & edit_mask
172
+
173
+ latent_pred_segments = torch.tensor(latent_pred_segments).to(cond.device)
174
+ fixed_span_mask = custom_mask_from_start_end_indices(cond_seq_len, latent_pred_segments, device=cond.device, max_seq_len=duration)
175
+ fixed_span_mask = fixed_span_mask.unsqueeze(-1)
176
+ step_cond = torch.where(fixed_span_mask, torch.zeros_like(cond), cond)
177
+
178
+ if isinstance(duration, int):
179
+ duration = torch.full((batch_infer_num,), duration, device=device, dtype=torch.long)
180
+
181
+ duration = duration.clamp(max=max_duration)
182
+ max_duration = duration.amax()
183
+
184
+ # duplicate test corner for inner time step oberservation
185
+ if duplicate_test:
186
+ test_cond = F.pad(cond, (0, 0, cond_seq_len, max_duration - 2 * cond_seq_len), value=0.0)
187
+
188
+ if batch > 1:
189
+ mask = lens_to_mask(duration)
190
+ else: # save memory and speed up, as single inference need no mask currently
191
+ mask = None
192
+
193
+ # test for no ref audio
194
+ if no_ref_audio:
195
+ cond = torch.zeros_like(cond)
196
+
197
+ cond = cond.repeat(batch_infer_num, 1, 1)
198
+ step_cond = step_cond.repeat(batch_infer_num, 1, 1)
199
+ text = text.repeat(batch_infer_num, 1)
200
+ style_prompt = style_prompt.repeat(batch_infer_num, 1)
201
+ negative_style_prompt = negative_style_prompt.repeat(batch_infer_num, 1)
202
+ start_time = start_time.repeat(batch_infer_num)
203
+ fixed_span_mask = fixed_span_mask.repeat(batch_infer_num, 1, 1)
204
+ song_duration = song_duration.repeat(batch_infer_num)
205
+
206
+ def fn(t, x):
207
+ # predict flow
208
+ pred = self.transformer(
209
+ x=x, cond=step_cond, text=text, time=t, drop_audio_cond=False, drop_text=False, drop_prompt=False,
210
+ style_prompt=style_prompt, start_time=start_time, duration=song_duration
211
+ )
212
+ if cfg_strength < 1e-5:
213
+ return pred
214
+
215
+ null_pred = self.transformer(
216
+ x=x, cond=step_cond, text=text, time=t, drop_audio_cond=True, drop_text=True, drop_prompt=False,
217
+ style_prompt=negative_style_prompt, start_time=start_time, duration=song_duration
218
+ )
219
+ return pred + (pred - null_pred) * cfg_strength
220
+
221
+ # noise input
222
+ # to make sure batch inference result is same with different batch size, and for sure single inference
223
+ # still some difference maybe due to convolutional layers
224
+ y0 = []
225
+ for dur in duration:
226
+ if exists(seed):
227
+ torch.manual_seed(seed)
228
+ y0.append(torch.randn(dur, self.num_channels, device=self.device, dtype=step_cond.dtype))
229
+ y0 = pad_sequence(y0, padding_value=0, batch_first=True)
230
+
231
+ t_start = 0
232
+
233
+ # duplicate test corner for inner time step oberservation
234
+ if duplicate_test:
235
+ t_start = t_inter
236
+ y0 = (1 - t_start) * y0 + t_start * test_cond
237
+ steps = int(steps * (1 - t_start))
238
+
239
+ t = torch.linspace(t_start, 1, steps, device=self.device, dtype=step_cond.dtype)
240
+ if sway_sampling_coef is not None:
241
+ t = t + sway_sampling_coef * (torch.cos(torch.pi / 2 * t) - 1 + t)
242
+
243
+ trajectory = odeint(fn, y0, t, **self.odeint_kwargs)
244
+
245
+ sampled = trajectory[-1]
246
+ out = sampled
247
+ out = torch.where(fixed_span_mask, out, cond)
248
+
249
+ if exists(vocoder):
250
+ out = out.permute(0, 2, 1)
251
+ out = vocoder(out)
252
+
253
+ out = torch.chunk(out, batch_infer_num, dim=0)
254
+ return out, trajectory
255
+
256
+ def forward(
257
+ self,
258
+ inp: float["b n d"] | float["b nw"], # mel or raw wave # noqa: F722
259
+ text: int["b nt"] | list[str], # noqa: F722
260
+ style_prompt = None,
261
+ style_prompt_lens = None,
262
+ lens: int["b"] | None = None, # noqa: F821
263
+ noise_scheduler: str | None = None,
264
+ grad_ckpt = False,
265
+ start_time = None,
266
+ ):
267
+
268
+ batch, seq_len, dtype, device, _σ1 = *inp.shape[:2], inp.dtype, self.device, self.sigma
269
+
270
+ # lens and mask
271
+ if not exists(lens):
272
+ lens = torch.full((batch,), seq_len, device=device)
273
+
274
+ mask = lens_to_mask(lens, length=seq_len) # useless here, as collate_fn will pad to max length in batch
275
+
276
+ # get a random span to mask out for training conditionally
277
+ frac_lengths = torch.zeros((batch,), device=self.device).float().uniform_(*self.frac_lengths_mask)
278
+ rand_span_mask = mask_from_frac_lengths(lens, frac_lengths, self.max_frames)
279
+
280
+ if exists(mask):
281
+ rand_span_mask = mask
282
+
283
+ # mel is x1
284
+ x1 = inp
285
+
286
+ # x0 is gaussian noise
287
+ x0 = torch.randn_like(x1)
288
+
289
+ # time step
290
+ time = torch.normal(mean=0, std=1, size=(batch,), device=self.device)
291
+ time = torch.nn.functional.sigmoid(time)
292
+ # TODO. noise_scheduler
293
+
294
+ # sample xt (��_t(x) in the paper)
295
+ t = time.unsqueeze(-1).unsqueeze(-1)
296
+ φ = (1 - t) * x0 + t * x1
297
+ flow = x1 - x0
298
+
299
+ # only predict what is within the random mask span for infilling
300
+ cond = torch.where(rand_span_mask[..., None], torch.zeros_like(x1), x1)
301
+
302
+ # transformer and cfg training with a drop rate
303
+ drop_audio_cond = random() < self.audio_drop_prob # p_drop in voicebox paper
304
+ drop_text = random() < self.lrc_drop_prob
305
+ drop_prompt = random() < self.style_drop_prob
306
+
307
+ # if want rigourously mask out padding, record in collate_fn in dataset.py, and pass in here
308
+ # adding mask will use more memory, thus also need to adjust batchsampler with scaled down threshold for long sequences
309
+ pred = self.transformer(
310
+ x=φ, cond=cond, text=text, time=time, drop_audio_cond=drop_audio_cond, drop_text=drop_text, drop_prompt=drop_prompt,
311
+ style_prompt=style_prompt, start_time=start_time
312
+ )
313
+
314
+ # flow matching loss
315
+ loss = F.mse_loss(pred, flow, reduction="none")
316
+ loss = loss[rand_span_mask]
317
+
318
+ return loss.mean(), cond, pred
code/model/dit.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025 ASLP-LAB
2
+ # 2025 Ziqian Ning (ningziqian@mail.nwpu.edu.cn)
3
+ # 2025 Huakang Chen (huakang@mail.nwpu.edu.cn)
4
+ # 2025 Yuepeng Jiang (Jiangyp@mail.nwpu.edu.cn)
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ """ This implementation is adapted from github repo:
19
+ https://github.com/SWivid/F5-TTS.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import torch
25
+ from torch import nn
26
+ import torch
27
+
28
+ from transformers.models.llama.modeling_llama import LlamaDecoderLayer, LlamaRotaryEmbedding
29
+ from transformers.models.llama import LlamaConfig
30
+
31
+ from model.modules import (
32
+ TimestepEmbedding,
33
+ ConvNeXtV2Block,
34
+ ConvPositionEmbedding,
35
+ AdaLayerNormZero_Final,
36
+ precompute_freqs_cis,
37
+ get_pos_embed_indices,
38
+ _prepare_decoder_attention_mask,
39
+ )
40
+
41
+ # Text embedding
42
+ class TextEmbedding(nn.Module):
43
+ def __init__(self, text_num_embeds, text_dim, max_pos, conv_layers=0, conv_mult=2):
44
+ super().__init__()
45
+ self.text_embed = nn.Embedding(text_num_embeds + 1, text_dim) # use 0 as filler token
46
+
47
+ if conv_layers > 0:
48
+ self.extra_modeling = True
49
+ self.precompute_max_pos = max_pos # ~44s of 24khz audio
50
+ self.register_buffer("freqs_cis", precompute_freqs_cis(text_dim, self.precompute_max_pos), persistent=False)
51
+ self.text_blocks = nn.Sequential(
52
+ *[ConvNeXtV2Block(text_dim, text_dim * conv_mult) for _ in range(conv_layers)]
53
+ )
54
+ else:
55
+ self.extra_modeling = False
56
+
57
+ def forward(self, text: int["b nt"], seq_len, drop_text=False): # noqa: F722
58
+ batch, text_len = text.shape[0], text.shape[1]
59
+
60
+ if drop_text: # cfg for text
61
+ text = torch.zeros_like(text)
62
+
63
+ text = self.text_embed(text) # b n -> b n d
64
+
65
+ # possible extra modeling
66
+ if self.extra_modeling:
67
+ # sinus pos emb
68
+ batch_start = torch.zeros((batch,), dtype=torch.long)
69
+ pos_idx = get_pos_embed_indices(batch_start, seq_len, max_pos=self.precompute_max_pos)
70
+ text_pos_embed = self.freqs_cis[pos_idx]
71
+ text = text + text_pos_embed
72
+
73
+ # convnextv2 blocks
74
+ text = self.text_blocks(text)
75
+
76
+ return text
77
+
78
+
79
+ # noised input audio and context mixing embedding
80
+ class InputEmbedding(nn.Module):
81
+ def __init__(self, mel_dim, text_dim, out_dim, cond_dim):
82
+ super().__init__()
83
+ self.proj = nn.Linear(mel_dim * 2 + text_dim + cond_dim * 2, out_dim)
84
+ self.conv_pos_embed = ConvPositionEmbedding(dim=out_dim)
85
+
86
+ def forward(self, x: float["b n d"], cond: float["b n d"], text_embed: float["b n d"], style_emb, time_emb, drop_audio_cond=False): # noqa: F722
87
+ if drop_audio_cond: # cfg for cond audio
88
+ cond = torch.zeros_like(cond)
89
+ style_emb = style_emb.unsqueeze(1).repeat(1, x.shape[1], 1)
90
+ time_emb = time_emb.unsqueeze(1).repeat(1, x.shape[1], 1)
91
+ x = self.proj(torch.cat((x, cond, text_embed, style_emb, time_emb), dim=-1))
92
+ x = self.conv_pos_embed(x) + x
93
+ return x
94
+
95
+
96
+ # Transformer backbone using Llama blocks
97
+ class DiT(nn.Module):
98
+ def __init__(
99
+ self,
100
+ *,
101
+ dim,
102
+ depth=8,
103
+ heads=8,
104
+ dim_head=64,
105
+ dropout=0.1,
106
+ ff_mult=4,
107
+ mel_dim=100,
108
+ text_num_embeds=256,
109
+ text_dim=None,
110
+ conv_layers=0,
111
+ long_skip_connection=False,
112
+ max_frames=2048
113
+ ):
114
+ super().__init__()
115
+
116
+ self.max_frames = max_frames
117
+
118
+ cond_dim = 512
119
+ self.time_embed = TimestepEmbedding(cond_dim)
120
+ self.start_time_embed = TimestepEmbedding(cond_dim)
121
+ self.duration_time_embed = TimestepEmbedding(cond_dim) if self.max_frames == 6144 else None
122
+ if text_dim is None:
123
+ text_dim = mel_dim
124
+ self.text_embed = TextEmbedding(text_num_embeds, text_dim, conv_layers=conv_layers, max_pos=self.max_frames)
125
+ self.input_embed = InputEmbedding(mel_dim, text_dim, dim, cond_dim=cond_dim)
126
+
127
+ self.dim = dim
128
+ self.depth = depth
129
+
130
+ llama_config = LlamaConfig(hidden_size=dim, intermediate_size=dim * ff_mult, hidden_act='silu', max_position_embeddings=self.max_frames)
131
+ llama_config._attn_implementation = 'sdpa'
132
+ self.transformer_blocks = nn.ModuleList(
133
+ [LlamaDecoderLayer(llama_config, layer_idx=i) for i in range(depth)]
134
+ )
135
+ self.rotary_emb = LlamaRotaryEmbedding(config=llama_config)
136
+ self.long_skip_connection = nn.Linear(dim * 2, dim, bias=False) if long_skip_connection else None
137
+
138
+ self.text_fusion_linears = nn.ModuleList(
139
+ [
140
+ nn.Sequential(
141
+ nn.Linear(cond_dim, dim),
142
+ nn.SiLU()
143
+ ) for i in range(depth // 2)
144
+ ]
145
+ )
146
+ for layer in self.text_fusion_linears:
147
+ for p in layer.parameters():
148
+ p.detach().zero_()
149
+
150
+ self.norm_out = AdaLayerNormZero_Final(dim, cond_dim) # final modulation
151
+ self.proj_out = nn.Linear(dim, mel_dim)
152
+
153
+ def forward_timestep_invariant(self, text, seq_len, drop_text, start_time):
154
+ s_t = self.start_time_embed(start_time)
155
+ text_embed = self.text_embed(text, seq_len, drop_text=drop_text)
156
+ text_residuals = []
157
+ for layer in self.text_fusion_linears:
158
+ text_residual = layer(text_embed)
159
+ text_residuals.append(text_residual)
160
+ return s_t, text_embed, text_residuals
161
+
162
+
163
+ def forward(
164
+ self,
165
+ x: float["b n d"], # nosied input audio # noqa: F722
166
+ cond: float["b n d"], # masked cond audio # noqa: F722
167
+ text: int["b nt"], # text # noqa: F722
168
+ time: float["b"] | float[""], # time step # noqa: F821 F722
169
+ drop_audio_cond, # cfg for cond audio
170
+ drop_text, # cfg for text
171
+ drop_prompt=False,
172
+ style_prompt=None, # [b d t]
173
+ start_time=None,
174
+ duration=None
175
+ ):
176
+
177
+ batch, seq_len = x.shape[0], x.shape[1]
178
+ if time.ndim == 0:
179
+ time = time.repeat(batch)
180
+
181
+ # t: conditioning time, c: context (text + masked cond audio), x: noised input audio
182
+ t = self.time_embed(time)
183
+ s_t = self.start_time_embed(start_time)
184
+ d_t = self.duration_time_embed(duration) if self.max_frames == 6144 else torch.zeros_like(s_t)
185
+ c = t + s_t + d_t
186
+ text_embed = self.text_embed(text, seq_len, drop_text=drop_text)
187
+
188
+ if drop_prompt:
189
+ style_prompt = torch.zeros_like(style_prompt)
190
+
191
+ style_embed = style_prompt # [b, 512]
192
+
193
+ x = self.input_embed(x, cond, text_embed, style_embed, c, drop_audio_cond=drop_audio_cond)
194
+
195
+ if self.long_skip_connection is not None:
196
+ residual = x
197
+
198
+ pos_ids = torch.arange(x.shape[1], device=x.device)
199
+ pos_ids = pos_ids.unsqueeze(0).repeat(x.shape[0], 1)
200
+ rotary_embed = self.rotary_emb(x, pos_ids)
201
+
202
+ attention_mask = torch.ones(
203
+ (batch, seq_len),
204
+ dtype=torch.bool,
205
+ device=x.device,
206
+ )
207
+ attention_mask = _prepare_decoder_attention_mask(
208
+ attention_mask,
209
+ (batch, seq_len),
210
+ x,
211
+ )
212
+
213
+ for i, block in enumerate(self.transformer_blocks):
214
+ x, *_ = block(x, attention_mask=attention_mask, position_embeddings=rotary_embed)
215
+ if i < self.depth // 2:
216
+ x = x + self.text_fusion_linears[i](text_embed)
217
+
218
+ if self.long_skip_connection is not None:
219
+ x = self.long_skip_connection(torch.cat((x, residual), dim=-1))
220
+
221
+ x = self.norm_out(x, c)
222
+ output = self.proj_out(x)
223
+
224
+ return output
code/model/modules.py ADDED
@@ -0,0 +1,660 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025 ASLP-LAB
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """ This implementation is adapted from github repo:
16
+ https://github.com/SWivid/F5-TTS.
17
+ """
18
+
19
+ from __future__ import annotations
20
+ from typing import Optional
21
+ import math
22
+
23
+ import torch
24
+ from torch import nn
25
+ import torch
26
+ import torch.nn.functional as F
27
+ import torchaudio
28
+
29
+ from x_transformers.x_transformers import apply_rotary_pos_emb
30
+
31
+
32
+
33
+ class FiLMLayer(nn.Module):
34
+ """
35
+ Feature-wise Linear Modulation (FiLM) layer
36
+ Reference: https://arxiv.org/abs/1709.07871
37
+ """
38
+ def __init__(self, in_channels, cond_channels):
39
+
40
+ super(FiLMLayer, self).__init__()
41
+ self.in_channels = in_channels
42
+ self.film = nn.Conv1d(cond_channels, in_channels * 2, 1)
43
+
44
+ def forward(self, x, c):
45
+ gamma, beta = torch.chunk(self.film(c.unsqueeze(2)), chunks=2, dim=1)
46
+ gamma = gamma.transpose(1, 2)
47
+ beta = beta.transpose(1, 2)
48
+ return gamma * x + beta
49
+
50
+ # raw wav to mel spec
51
+
52
+
53
+ class MelSpec(nn.Module):
54
+ def __init__(
55
+ self,
56
+ filter_length=1024,
57
+ hop_length=256,
58
+ win_length=1024,
59
+ n_mel_channels=100,
60
+ target_sample_rate=24_000,
61
+ normalize=False,
62
+ power=1,
63
+ norm=None,
64
+ center=True,
65
+ ):
66
+ super().__init__()
67
+ self.n_mel_channels = n_mel_channels
68
+
69
+ self.mel_stft = torchaudio.transforms.MelSpectrogram(
70
+ sample_rate=target_sample_rate,
71
+ n_fft=filter_length,
72
+ win_length=win_length,
73
+ hop_length=hop_length,
74
+ n_mels=n_mel_channels,
75
+ power=power,
76
+ center=center,
77
+ normalized=normalize,
78
+ norm=norm,
79
+ )
80
+
81
+ self.register_buffer("dummy", torch.tensor(0), persistent=False)
82
+
83
+ def forward(self, inp):
84
+ if len(inp.shape) == 3:
85
+ inp = inp.squeeze(1) # 'b 1 nw -> b nw'
86
+
87
+ assert len(inp.shape) == 2
88
+
89
+ if self.dummy.device != inp.device:
90
+ self.to(inp.device)
91
+
92
+ mel = self.mel_stft(inp)
93
+ mel = mel.clamp(min=1e-5).log()
94
+ return mel
95
+
96
+
97
+ # sinusoidal position embedding
98
+
99
+
100
+ class SinusPositionEmbedding(nn.Module):
101
+ def __init__(self, dim):
102
+ super().__init__()
103
+ self.dim = dim
104
+
105
+ def forward(self, x, scale=1000):
106
+ device = x.device
107
+ half_dim = self.dim // 2
108
+ emb = math.log(10000) / (half_dim - 1)
109
+ emb = torch.exp(torch.arange(half_dim, device=device).float() * -emb)
110
+ emb = scale * x.unsqueeze(1) * emb.unsqueeze(0)
111
+ emb = torch.cat((emb.sin(), emb.cos()), dim=-1)
112
+ return emb
113
+
114
+
115
+ # convolutional position embedding
116
+
117
+
118
+ class ConvPositionEmbedding(nn.Module):
119
+ def __init__(self, dim, kernel_size=31, groups=16):
120
+ super().__init__()
121
+ assert kernel_size % 2 != 0
122
+ self.conv1d = nn.Sequential(
123
+ nn.Conv1d(dim, dim, kernel_size, groups=groups, padding=kernel_size // 2),
124
+ nn.Mish(),
125
+ nn.Conv1d(dim, dim, kernel_size, groups=groups, padding=kernel_size // 2),
126
+ nn.Mish(),
127
+ )
128
+
129
+ def forward(self, x: float["b n d"], mask: bool["b n"] | None = None): # noqa: F722
130
+ if mask is not None:
131
+ mask = mask[..., None]
132
+ x = x.masked_fill(~mask, 0.0)
133
+
134
+ x = x.permute(0, 2, 1)
135
+ x = self.conv1d(x)
136
+ out = x.permute(0, 2, 1)
137
+
138
+ if mask is not None:
139
+ out = out.masked_fill(~mask, 0.0)
140
+
141
+ return out
142
+
143
+
144
+ # rotary positional embedding related
145
+
146
+
147
+ def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0, theta_rescale_factor=1.0):
148
+ # proposed by reddit user bloc97, to rescale rotary embeddings to longer sequence length without fine-tuning
149
+ # has some connection to NTK literature
150
+ # https://www.reddit.com/r/LocalLLaMA/comments/14lz7j5/ntkaware_scaled_rope_allows_llama_models_to_have/
151
+ # https://github.com/lucidrains/rotary-embedding-torch/blob/main/rotary_embedding_torch/rotary_embedding_torch.py
152
+ theta *= theta_rescale_factor ** (dim / (dim - 2))
153
+ freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim))
154
+ t = torch.arange(end, device=freqs.device) # type: ignore
155
+ freqs = torch.outer(t, freqs).float() # type: ignore
156
+ freqs_cos = torch.cos(freqs) # real part
157
+ freqs_sin = torch.sin(freqs) # imaginary part
158
+ return torch.cat([freqs_cos, freqs_sin], dim=-1)
159
+
160
+
161
+ def get_pos_embed_indices(start, length, max_pos, scale=1.0):
162
+ # length = length if isinstance(length, int) else length.max()
163
+ scale = scale * torch.ones_like(start, dtype=torch.float32) # in case scale is a scalar
164
+ pos = (
165
+ start.unsqueeze(1)
166
+ + (torch.arange(length, device=start.device, dtype=torch.float32).unsqueeze(0) * scale.unsqueeze(1)).long()
167
+ )
168
+ # avoid extra long error.
169
+ pos = torch.where(pos < max_pos, pos, max_pos - 1)
170
+ return pos
171
+
172
+
173
+ # Global Response Normalization layer (Instance Normalization ?)
174
+
175
+
176
+ class GRN(nn.Module):
177
+ def __init__(self, dim):
178
+ super().__init__()
179
+ self.gamma = nn.Parameter(torch.zeros(1, 1, dim))
180
+ self.beta = nn.Parameter(torch.zeros(1, 1, dim))
181
+
182
+ def forward(self, x):
183
+ Gx = torch.norm(x, p=2, dim=1, keepdim=True)
184
+ Nx = Gx / (Gx.mean(dim=-1, keepdim=True) + 1e-6)
185
+ return self.gamma * (x * Nx) + self.beta + x
186
+
187
+
188
+ # ConvNeXt-V2 Block https://github.com/facebookresearch/ConvNeXt-V2/blob/main/models/convnextv2.py
189
+ # ref: https://github.com/bfs18/e2_tts/blob/main/rfwave/modules.py#L108
190
+
191
+
192
+ class ConvNeXtV2Block(nn.Module):
193
+ def __init__(
194
+ self,
195
+ dim: int,
196
+ intermediate_dim: int,
197
+ dilation: int = 1,
198
+ ):
199
+ super().__init__()
200
+ padding = (dilation * (7 - 1)) // 2
201
+ self.dwconv = nn.Conv1d(
202
+ dim, dim, kernel_size=7, padding=padding, groups=dim, dilation=dilation
203
+ ) # depthwise conv
204
+ self.norm = nn.LayerNorm(dim, eps=1e-6)
205
+ self.pwconv1 = nn.Linear(dim, intermediate_dim) # pointwise/1x1 convs, implemented with linear layers
206
+ self.act = nn.GELU()
207
+ self.grn = GRN(intermediate_dim)
208
+ self.pwconv2 = nn.Linear(intermediate_dim, dim)
209
+
210
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
211
+ residual = x
212
+ x = x.transpose(1, 2) # b n d -> b d n
213
+ x = self.dwconv(x)
214
+ x = x.transpose(1, 2) # b d n -> b n d
215
+ x = self.norm(x)
216
+ x = self.pwconv1(x)
217
+ x = self.act(x)
218
+ x = self.grn(x)
219
+ x = self.pwconv2(x)
220
+ return residual + x
221
+
222
+
223
+ # AdaLayerNormZero
224
+ # return with modulated x for attn input, and params for later mlp modulation
225
+
226
+
227
+ class AdaLayerNormZero(nn.Module):
228
+ def __init__(self, dim):
229
+ super().__init__()
230
+
231
+ self.silu = nn.SiLU()
232
+ self.linear = nn.Linear(dim, dim * 6)
233
+
234
+ self.norm = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6)
235
+
236
+ def forward(self, x, emb=None):
237
+ emb = self.linear(self.silu(emb))
238
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = torch.chunk(emb, 6, dim=1)
239
+
240
+ x = self.norm(x) * (1 + scale_msa[:, None]) + shift_msa[:, None]
241
+ return x, gate_msa, shift_mlp, scale_mlp, gate_mlp
242
+
243
+
244
+ # AdaLayerNormZero for final layer
245
+ # return only with modulated x for attn input, cuz no more mlp modulation
246
+
247
+
248
+ class AdaLayerNormZero_Final(nn.Module):
249
+ def __init__(self, dim, cond_dim):
250
+ super().__init__()
251
+
252
+ self.silu = nn.SiLU()
253
+ self.linear = nn.Linear(cond_dim, dim * 2)
254
+
255
+ self.norm = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6)
256
+
257
+ def forward(self, x, emb):
258
+ emb = self.linear(self.silu(emb))
259
+ scale, shift = torch.chunk(emb, 2, dim=1)
260
+
261
+ x = self.norm(x) * (1 + scale)[:, None, :] + shift[:, None, :]
262
+ return x
263
+
264
+
265
+ # FeedForward
266
+
267
+
268
+ class FeedForward(nn.Module):
269
+ def __init__(self, dim, dim_out=None, mult=4, dropout=0.0, approximate: str = "none"):
270
+ super().__init__()
271
+ inner_dim = int(dim * mult)
272
+ dim_out = dim_out if dim_out is not None else dim
273
+
274
+ activation = nn.GELU(approximate=approximate)
275
+ #activation = nn.SiLU()
276
+ project_in = nn.Sequential(nn.Linear(dim, inner_dim), activation)
277
+ self.ff = nn.Sequential(project_in, nn.Dropout(dropout), nn.Linear(inner_dim, dim_out))
278
+
279
+ def forward(self, x):
280
+ return self.ff(x)
281
+
282
+
283
+ # Attention with possible joint part
284
+ # modified from diffusers/src/diffusers/models/attention_processor.py
285
+
286
+
287
+ class Attention(nn.Module):
288
+ def __init__(
289
+ self,
290
+ processor: JointAttnProcessor | AttnProcessor,
291
+ dim: int,
292
+ heads: int = 8,
293
+ dim_head: int = 64,
294
+ dropout: float = 0.0,
295
+ context_dim: Optional[int] = None, # if not None -> joint attention
296
+ context_pre_only=None,
297
+ ):
298
+ super().__init__()
299
+
300
+ if not hasattr(F, "scaled_dot_product_attention"):
301
+ raise ImportError("Attention equires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0.")
302
+
303
+ self.processor = processor
304
+
305
+ self.dim = dim
306
+ self.heads = heads
307
+ self.inner_dim = dim_head * heads
308
+ self.dropout = dropout
309
+
310
+ self.context_dim = context_dim
311
+ self.context_pre_only = context_pre_only
312
+
313
+ self.to_q = nn.Linear(dim, self.inner_dim)
314
+ self.to_k = nn.Linear(dim, self.inner_dim)
315
+ self.to_v = nn.Linear(dim, self.inner_dim)
316
+
317
+ if self.context_dim is not None:
318
+ self.to_k_c = nn.Linear(context_dim, self.inner_dim)
319
+ self.to_v_c = nn.Linear(context_dim, self.inner_dim)
320
+ if self.context_pre_only is not None:
321
+ self.to_q_c = nn.Linear(context_dim, self.inner_dim)
322
+
323
+ self.to_out = nn.ModuleList([])
324
+ self.to_out.append(nn.Linear(self.inner_dim, dim))
325
+ self.to_out.append(nn.Dropout(dropout))
326
+
327
+ if self.context_pre_only is not None and not self.context_pre_only:
328
+ self.to_out_c = nn.Linear(self.inner_dim, dim)
329
+
330
+ def forward(
331
+ self,
332
+ x: float["b n d"], # noised input x # noqa: F722
333
+ c: float["b n d"] = None, # context c # noqa: F722
334
+ mask: bool["b n"] | None = None, # noqa: F722
335
+ rope=None, # rotary position embedding for x
336
+ c_rope=None, # rotary position embedding for c
337
+ ) -> torch.Tensor:
338
+ if c is not None:
339
+ return self.processor(self, x, c=c, mask=mask, rope=rope, c_rope=c_rope)
340
+ else:
341
+ return self.processor(self, x, mask=mask, rope=rope)
342
+
343
+
344
+ # Attention processor
345
+
346
+
347
+ class AttnProcessor:
348
+ def __init__(self):
349
+ pass
350
+
351
+ def __call__(
352
+ self,
353
+ attn: Attention,
354
+ x: float["b n d"], # noised input x # noqa: F722
355
+ mask: bool["b n"] | None = None, # noqa: F722
356
+ rope=None, # rotary position embedding
357
+ ) -> torch.FloatTensor:
358
+ batch_size = x.shape[0]
359
+
360
+ # `sample` projections.
361
+ query = attn.to_q(x)
362
+ key = attn.to_k(x)
363
+ value = attn.to_v(x)
364
+
365
+ # apply rotary position embedding
366
+ if rope is not None:
367
+ freqs, xpos_scale = rope
368
+ q_xpos_scale, k_xpos_scale = (xpos_scale, xpos_scale**-1.0) if xpos_scale is not None else (1.0, 1.0)
369
+
370
+ query = apply_rotary_pos_emb(query, freqs, q_xpos_scale)
371
+ key = apply_rotary_pos_emb(key, freqs, k_xpos_scale)
372
+
373
+ # attention
374
+ inner_dim = key.shape[-1]
375
+ head_dim = inner_dim // attn.heads
376
+ query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
377
+ key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
378
+ value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
379
+
380
+ # mask. e.g. inference got a batch with different target durations, mask out the padding
381
+ if mask is not None:
382
+ attn_mask = mask
383
+ attn_mask = attn_mask.unsqueeze(1).unsqueeze(1) # 'b n -> b 1 1 n'
384
+ attn_mask = attn_mask.expand(batch_size, attn.heads, query.shape[-2], key.shape[-2])
385
+ else:
386
+ attn_mask = None
387
+
388
+ x = F.scaled_dot_product_attention(query, key, value, attn_mask=attn_mask, dropout_p=0.0, is_causal=False)
389
+ x = x.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim)
390
+ x = x.to(query.dtype)
391
+
392
+ # linear proj
393
+ x = attn.to_out[0](x)
394
+ # dropout
395
+ x = attn.to_out[1](x)
396
+
397
+ if mask is not None:
398
+ mask = mask.unsqueeze(-1)
399
+ x = x.masked_fill(~mask, 0.0)
400
+
401
+ return x
402
+
403
+
404
+ # Joint Attention processor for MM-DiT
405
+ # modified from diffusers/src/diffusers/models/attention_processor.py
406
+
407
+
408
+ class JointAttnProcessor:
409
+ def __init__(self):
410
+ pass
411
+
412
+ def __call__(
413
+ self,
414
+ attn: Attention,
415
+ x: float["b n d"], # noised input x # noqa: F722
416
+ c: float["b nt d"] = None, # context c, here text # noqa: F722
417
+ mask: bool["b n"] | None = None, # noqa: F722
418
+ rope=None, # rotary position embedding for x
419
+ c_rope=None, # rotary position embedding for c
420
+ ) -> torch.FloatTensor:
421
+ residual = x
422
+
423
+ batch_size = c.shape[0]
424
+
425
+ # `sample` projections.
426
+ query = attn.to_q(x)
427
+ key = attn.to_k(x)
428
+ value = attn.to_v(x)
429
+
430
+ # `context` projections.
431
+ c_query = attn.to_q_c(c)
432
+ c_key = attn.to_k_c(c)
433
+ c_value = attn.to_v_c(c)
434
+
435
+ # apply rope for context and noised input independently
436
+ if rope is not None:
437
+ freqs, xpos_scale = rope
438
+ q_xpos_scale, k_xpos_scale = (xpos_scale, xpos_scale**-1.0) if xpos_scale is not None else (1.0, 1.0)
439
+ query = apply_rotary_pos_emb(query, freqs, q_xpos_scale)
440
+ key = apply_rotary_pos_emb(key, freqs, k_xpos_scale)
441
+ if c_rope is not None:
442
+ freqs, xpos_scale = c_rope
443
+ q_xpos_scale, k_xpos_scale = (xpos_scale, xpos_scale**-1.0) if xpos_scale is not None else (1.0, 1.0)
444
+ c_query = apply_rotary_pos_emb(c_query, freqs, q_xpos_scale)
445
+ c_key = apply_rotary_pos_emb(c_key, freqs, k_xpos_scale)
446
+
447
+ # attention
448
+ query = torch.cat([query, c_query], dim=1)
449
+ key = torch.cat([key, c_key], dim=1)
450
+ value = torch.cat([value, c_value], dim=1)
451
+
452
+ inner_dim = key.shape[-1]
453
+ head_dim = inner_dim // attn.heads
454
+ query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
455
+ key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
456
+ value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
457
+
458
+ # mask. e.g. inference got a batch with different target durations, mask out the padding
459
+ if mask is not None:
460
+ attn_mask = F.pad(mask, (0, c.shape[1]), value=True) # no mask for c (text)
461
+ attn_mask = attn_mask.unsqueeze(1).unsqueeze(1) # 'b n -> b 1 1 n'
462
+ attn_mask = attn_mask.expand(batch_size, attn.heads, query.shape[-2], key.shape[-2])
463
+ else:
464
+ attn_mask = None
465
+
466
+ x = F.scaled_dot_product_attention(query, key, value, attn_mask=attn_mask, dropout_p=0.0, is_causal=False)
467
+ x = x.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim)
468
+ x = x.to(query.dtype)
469
+
470
+ # Split the attention outputs.
471
+ x, c = (
472
+ x[:, : residual.shape[1]],
473
+ x[:, residual.shape[1] :],
474
+ )
475
+
476
+ # linear proj
477
+ x = attn.to_out[0](x)
478
+ # dropout
479
+ x = attn.to_out[1](x)
480
+ if not attn.context_pre_only:
481
+ c = attn.to_out_c(c)
482
+
483
+ if mask is not None:
484
+ mask = mask.unsqueeze(-1)
485
+ x = x.masked_fill(~mask, 0.0)
486
+ # c = c.masked_fill(~mask, 0.) # no mask for c (text)
487
+
488
+ return x, c
489
+
490
+
491
+ # DiT Block
492
+
493
+
494
+ class DiTBlock(nn.Module):
495
+ def __init__(self, dim, heads, dim_head, ff_mult=4, dropout=0.1, use_style_prompt=False):
496
+ super().__init__()
497
+
498
+ self.attn_norm = AdaLayerNormZero(dim)
499
+ self.attn = Attention(
500
+ processor=AttnProcessor(),
501
+ dim=dim,
502
+ heads=heads,
503
+ dim_head=dim_head,
504
+ dropout=dropout,
505
+ )
506
+
507
+ self.ff_norm = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6)
508
+ self.ff = FeedForward(dim=dim, mult=ff_mult, dropout=dropout, approximate="tanh")
509
+
510
+ self.use_style_prompt = use_style_prompt
511
+ if use_style_prompt:
512
+ #self.film = FiLMLayer(dim, dim)
513
+ self.prompt_norm = AdaLayerNormZero_Final(dim)
514
+
515
+ def forward(self, x, t, c=None, mask=None, rope=None): # x: noised input, t: time embedding
516
+ if c is not None and self.use_style_prompt:
517
+ #x = self.film(x, c)
518
+ x = self.prompt_norm(x, c)
519
+
520
+ # pre-norm & modulation for attention input
521
+ norm, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.attn_norm(x, emb=t)
522
+
523
+ # attention
524
+ attn_output = self.attn(x=norm, mask=mask, rope=rope)
525
+
526
+ # process attention output for input x
527
+ x = x + gate_msa.unsqueeze(1) * attn_output
528
+
529
+ norm = self.ff_norm(x) * (1 + scale_mlp[:, None]) + shift_mlp[:, None]
530
+ ff_output = self.ff(norm)
531
+ x = x + gate_mlp.unsqueeze(1) * ff_output
532
+
533
+ return x
534
+
535
+
536
+ # MMDiT Block https://arxiv.org/abs/2403.03206
537
+
538
+
539
+ class MMDiTBlock(nn.Module):
540
+ r"""
541
+ modified from diffusers/src/diffusers/models/attention.py
542
+
543
+ notes.
544
+ _c: context related. text, cond, etc. (left part in sd3 fig2.b)
545
+ _x: noised input related. (right part)
546
+ context_pre_only: last layer only do prenorm + modulation cuz no more ffn
547
+ """
548
+
549
+ def __init__(self, dim, heads, dim_head, ff_mult=4, dropout=0.1, context_pre_only=False):
550
+ super().__init__()
551
+
552
+ self.context_pre_only = context_pre_only
553
+
554
+ self.attn_norm_c = AdaLayerNormZero_Final(dim) if context_pre_only else AdaLayerNormZero(dim)
555
+ self.attn_norm_x = AdaLayerNormZero(dim)
556
+ self.attn = Attention(
557
+ processor=JointAttnProcessor(),
558
+ dim=dim,
559
+ heads=heads,
560
+ dim_head=dim_head,
561
+ dropout=dropout,
562
+ context_dim=dim,
563
+ context_pre_only=context_pre_only,
564
+ )
565
+
566
+ if not context_pre_only:
567
+ self.ff_norm_c = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6)
568
+ self.ff_c = FeedForward(dim=dim, mult=ff_mult, dropout=dropout, approximate="tanh")
569
+ else:
570
+ self.ff_norm_c = None
571
+ self.ff_c = None
572
+ self.ff_norm_x = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-6)
573
+ self.ff_x = FeedForward(dim=dim, mult=ff_mult, dropout=dropout, approximate="tanh")
574
+
575
+ def forward(self, x, c, t, mask=None, rope=None, c_rope=None): # x: noised input, c: context, t: time embedding
576
+ # pre-norm & modulation for attention input
577
+ if self.context_pre_only:
578
+ norm_c = self.attn_norm_c(c, t)
579
+ else:
580
+ norm_c, c_gate_msa, c_shift_mlp, c_scale_mlp, c_gate_mlp = self.attn_norm_c(c, emb=t)
581
+ norm_x, x_gate_msa, x_shift_mlp, x_scale_mlp, x_gate_mlp = self.attn_norm_x(x, emb=t)
582
+
583
+ # attention
584
+ x_attn_output, c_attn_output = self.attn(x=norm_x, c=norm_c, mask=mask, rope=rope, c_rope=c_rope)
585
+
586
+ # process attention output for context c
587
+ if self.context_pre_only:
588
+ c = None
589
+ else: # if not last layer
590
+ c = c + c_gate_msa.unsqueeze(1) * c_attn_output
591
+
592
+ norm_c = self.ff_norm_c(c) * (1 + c_scale_mlp[:, None]) + c_shift_mlp[:, None]
593
+ c_ff_output = self.ff_c(norm_c)
594
+ c = c + c_gate_mlp.unsqueeze(1) * c_ff_output
595
+
596
+ # process attention output for input x
597
+ x = x + x_gate_msa.unsqueeze(1) * x_attn_output
598
+
599
+ norm_x = self.ff_norm_x(x) * (1 + x_scale_mlp[:, None]) + x_shift_mlp[:, None]
600
+ x_ff_output = self.ff_x(norm_x)
601
+ x = x + x_gate_mlp.unsqueeze(1) * x_ff_output
602
+
603
+ return c, x
604
+
605
+
606
+ # time step conditioning embedding
607
+
608
+
609
+ class TimestepEmbedding(nn.Module):
610
+ def __init__(self, dim, freq_embed_dim=256):
611
+ super().__init__()
612
+ self.time_embed = SinusPositionEmbedding(freq_embed_dim)
613
+ self.time_mlp = nn.Sequential(nn.Linear(freq_embed_dim, dim), nn.SiLU(), nn.Linear(dim, dim))
614
+
615
+ def forward(self, timestep: float["b"]): # noqa: F821
616
+ time_hidden = self.time_embed(timestep)
617
+ time_hidden = time_hidden.to(timestep.dtype)
618
+ time = self.time_mlp(time_hidden) # b d
619
+ return time
620
+
621
+
622
+ # attention mask realated
623
+
624
+
625
+ def _prepare_decoder_attention_mask(attention_mask, input_shape, inputs_embeds):
626
+ # create noncausal mask
627
+ # [bsz, seq_len] -> [bsz, 1, tgt_seq_len, src_seq_len]
628
+ combined_attention_mask = None
629
+
630
+ def _expand_mask(
631
+ mask: torch.Tensor, dtype: torch.dtype, tgt_len: int = None
632
+ ):
633
+ """
634
+ Expands attention_mask from `[bsz, seq_len]` to `[bsz, 1, tgt_seq_len, src_seq_len]`.
635
+ """
636
+ bsz, src_len = mask.size()
637
+ tgt_len = tgt_len if tgt_len is not None else src_len
638
+
639
+ expanded_mask = (
640
+ mask[:, None, None, :].expand(bsz, 1, tgt_len, src_len).to(dtype)
641
+ )
642
+
643
+ inverted_mask = 1.0 - expanded_mask
644
+
645
+ return inverted_mask.masked_fill(
646
+ inverted_mask.to(torch.bool), torch.finfo(dtype).min
647
+ )
648
+
649
+ if attention_mask is not None:
650
+ # [bsz, seq_len] -> [bsz, 1, tgt_seq_len, src_seq_len]
651
+ expanded_attn_mask = _expand_mask(
652
+ attention_mask, inputs_embeds.dtype, tgt_len=input_shape[-1]
653
+ ).to(inputs_embeds.device)
654
+ combined_attention_mask = (
655
+ expanded_attn_mask
656
+ if combined_attention_mask is None
657
+ else expanded_attn_mask + combined_attention_mask
658
+ )
659
+
660
+ return combined_attention_mask
code/model/trainer.py ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025 ASLP-LAB
2
+ # 2025 Ziqian Ning (ningziqian@mail.nwpu.edu.cn)
3
+ # 2025 Huakang Chen (huakang@mail.nwpu.edu.cn)
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ """ This implementation is adapted from github repo:
18
+ https://github.com/SWivid/F5-TTS.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import os
24
+ import gc
25
+ from tqdm import tqdm
26
+ import wandb
27
+
28
+ import torch
29
+ from torch.optim import AdamW
30
+ from torch.optim.lr_scheduler import LinearLR, SequentialLR, ConstantLR
31
+
32
+ from accelerate import Accelerator
33
+ from accelerate.utils import DistributedDataParallelKwargs
34
+ from dataset.dataset import DiffusionDataset
35
+
36
+ from torch.utils.data import DataLoader
37
+
38
+ from ema_pytorch import EMA
39
+
40
+ from model import CFM
41
+ from model.utils import exists, default
42
+
43
+ class Trainer:
44
+ def __init__(
45
+ self,
46
+ model: CFM,
47
+ args,
48
+ epochs,
49
+ learning_rate,
50
+ num_warmup_updates=20000,
51
+ save_per_updates=1000,
52
+ checkpoint_path=None,
53
+ batch_size=32,
54
+ batch_size_type: str = "sample",
55
+ max_samples=32,
56
+ grad_accumulation_steps=1,
57
+ max_grad_norm=1.0,
58
+ noise_scheduler: str | None = None,
59
+ duration_predictor: torch.nn.Module | None = None,
60
+ wandb_project="test_e2-tts",
61
+ wandb_run_name="test_run",
62
+ wandb_resume_id: str = None,
63
+ last_per_steps=None,
64
+ accelerate_kwargs: dict = dict(),
65
+ ema_kwargs: dict = dict(),
66
+ bnb_optimizer: bool = False,
67
+ reset_lr: bool = False,
68
+ use_style_prompt: bool = False,
69
+ grad_ckpt: bool = False
70
+ ):
71
+ self.args = args
72
+
73
+ ddp_kwargs = DistributedDataParallelKwargs(find_unused_parameters=False, )
74
+
75
+ logger = "wandb" if wandb.api.api_key else None
76
+
77
+ self.accelerator = Accelerator(
78
+ log_with=logger,
79
+ kwargs_handlers=[ddp_kwargs],
80
+ gradient_accumulation_steps=grad_accumulation_steps,
81
+ **accelerate_kwargs,
82
+ )
83
+
84
+ if logger == "wandb":
85
+ if exists(wandb_resume_id):
86
+ init_kwargs = {"wandb": {"resume": "allow", "name": wandb_run_name, "id": wandb_resume_id}}
87
+ else:
88
+ init_kwargs = {"wandb": {"resume": "allow", "name": wandb_run_name}}
89
+ self.accelerator.init_trackers(
90
+ project_name=wandb_project,
91
+ init_kwargs=init_kwargs,
92
+ config={
93
+ "epochs": epochs,
94
+ "learning_rate": learning_rate,
95
+ "num_warmup_updates": num_warmup_updates,
96
+ "batch_size": batch_size,
97
+ "batch_size_type": batch_size_type,
98
+ "max_samples": max_samples,
99
+ "grad_accumulation_steps": grad_accumulation_steps,
100
+ "max_grad_norm": max_grad_norm,
101
+ "gpus": self.accelerator.num_processes,
102
+ "noise_scheduler": noise_scheduler,
103
+ },
104
+ )
105
+
106
+ self.precision = self.accelerator.state.mixed_precision
107
+ self.precision = self.precision.replace("no", "fp32")
108
+
109
+ self.model = model
110
+
111
+ if self.is_main:
112
+ self.ema_model = EMA(model, include_online_model=False, **ema_kwargs)
113
+
114
+ self.ema_model.to(self.accelerator.device)
115
+ if self.accelerator.state.distributed_type in ["DEEPSPEED", "FSDP"]:
116
+ self.ema_model.half()
117
+
118
+ self.epochs = epochs
119
+ self.num_warmup_updates = num_warmup_updates
120
+ self.save_per_updates = save_per_updates
121
+ self.last_per_steps = default(last_per_steps, save_per_updates * grad_accumulation_steps)
122
+ self.checkpoint_path = default(checkpoint_path, "ckpts/test_e2-tts")
123
+
124
+ self.max_samples = max_samples
125
+ self.grad_accumulation_steps = grad_accumulation_steps
126
+ self.max_grad_norm = max_grad_norm
127
+
128
+ self.noise_scheduler = noise_scheduler
129
+
130
+ self.duration_predictor = duration_predictor
131
+
132
+ self.reset_lr = reset_lr
133
+
134
+ self.use_style_prompt = use_style_prompt
135
+
136
+ self.grad_ckpt = grad_ckpt
137
+
138
+ if bnb_optimizer:
139
+ import bitsandbytes as bnb
140
+
141
+ self.optimizer = bnb.optim.AdamW8bit(model.parameters(), lr=learning_rate)
142
+ else:
143
+ self.optimizer = AdamW(model.parameters(), lr=learning_rate)
144
+
145
+ if self.accelerator.state.distributed_type == "DEEPSPEED":
146
+ self.accelerator.state.deepspeed_plugin.deepspeed_config['train_micro_batch_size_per_gpu'] = batch_size
147
+
148
+ self.get_dataloader()
149
+ self.get_scheduler()
150
+
151
+ self.model, self.optimizer, self.scheduler, self.train_dataloader = self.accelerator.prepare(self.model, self.optimizer, self.scheduler, self.train_dataloader)
152
+
153
+ def get_scheduler(self):
154
+ warmup_steps = (
155
+ self.num_warmup_updates * self.accelerator.num_processes
156
+ ) # consider a fixed warmup steps while using accelerate multi-gpu ddp
157
+ total_steps = len(self.train_dataloader) * self.epochs / self.grad_accumulation_steps
158
+ decay_steps = total_steps - warmup_steps
159
+ warmup_scheduler = LinearLR(self.optimizer, start_factor=1e-8, end_factor=1.0, total_iters=warmup_steps)
160
+ decay_scheduler = LinearLR(self.optimizer, start_factor=1.0, end_factor=1e-8, total_iters=decay_steps)
161
+ self.scheduler = SequentialLR(
162
+ self.optimizer, schedulers=[warmup_scheduler, decay_scheduler], milestones=[warmup_steps]
163
+ )
164
+
165
+ def get_constant_scheduler(self):
166
+ total_steps = len(self.train_dataloader) * self.epochs / self.grad_accumulation_steps
167
+ self.scheduler = ConstantLR(self.optimizer, factor=1, total_iters=total_steps)
168
+
169
+ def get_dataloader(self):
170
+ print(self.args)
171
+ dd = DiffusionDataset(self.args.file_path, self.args.max_frames, self.args.min_frames, self.args.sampling_rate, self.args.downsample_rate, self.precision)
172
+ self.train_dataloader = DataLoader(
173
+ dataset=dd,
174
+ batch_size=self.args.batch_size,
175
+ shuffle=True,
176
+ num_workers=4,
177
+ pin_memory=True,
178
+ collate_fn=dd.custom_collate_fn,
179
+ persistent_workers=True
180
+ )
181
+
182
+
183
+ @property
184
+ def is_main(self):
185
+ return self.accelerator.is_main_process
186
+
187
+ def save_checkpoint(self, step, last=False):
188
+ self.accelerator.wait_for_everyone()
189
+ if self.is_main:
190
+ checkpoint = dict(
191
+ model_state_dict=self.accelerator.unwrap_model(self.model).state_dict(),
192
+ optimizer_state_dict=self.accelerator.unwrap_model(self.optimizer).state_dict(),
193
+ ema_model_state_dict=self.ema_model.state_dict(),
194
+ scheduler_state_dict=self.scheduler.state_dict(),
195
+ step=step,
196
+ )
197
+ if not os.path.exists(self.checkpoint_path):
198
+ os.makedirs(self.checkpoint_path)
199
+ if last:
200
+ self.accelerator.save(checkpoint, f"{self.checkpoint_path}/model_last.pt")
201
+ print(f"Saved last checkpoint at step {step}")
202
+ else:
203
+ self.accelerator.save(checkpoint, f"{self.checkpoint_path}/model_{step}.pt")
204
+
205
+ def load_checkpoint(self):
206
+ if (
207
+ not exists(self.checkpoint_path)
208
+ or not os.path.exists(self.checkpoint_path)
209
+ or not os.listdir(self.checkpoint_path)
210
+ ):
211
+ return 0
212
+
213
+ self.accelerator.wait_for_everyone()
214
+ if "model_last.pt" in os.listdir(self.checkpoint_path):
215
+ latest_checkpoint = "model_last.pt"
216
+ else:
217
+ latest_checkpoint = sorted(
218
+ [f for f in os.listdir(self.checkpoint_path) if f.endswith(".pt")],
219
+ key=lambda x: int("".join(filter(str.isdigit, x))),
220
+ )[-1]
221
+
222
+ checkpoint = torch.load(f"{self.checkpoint_path}/{latest_checkpoint}", map_location="cpu")
223
+
224
+ if self.is_main:
225
+ ema_dict = self.ema_model.state_dict()
226
+ ema_checkpoint_dict = checkpoint["ema_model_state_dict"]
227
+
228
+ filtered_ema_dict = {
229
+ k: v for k, v in ema_checkpoint_dict.items()
230
+ if k in ema_dict and ema_dict[k].shape == v.shape
231
+ }
232
+
233
+ self.ema_model.load_state_dict(filtered_ema_dict, strict=False)
234
+
235
+ model_dict = self.accelerator.unwrap_model(self.model).state_dict()
236
+ checkpoint_model_dict = checkpoint["model_state_dict"]
237
+
238
+ filtered_model_dict = {
239
+ k: v for k, v in checkpoint_model_dict.items()
240
+ if k in model_dict and model_dict[k].shape == v.shape
241
+ }
242
+
243
+ self.accelerator.unwrap_model(self.model).load_state_dict(filtered_model_dict, strict=False)
244
+
245
+ if "step" in checkpoint:
246
+ if self.scheduler and not self.reset_lr:
247
+ self.scheduler.load_state_dict(checkpoint["scheduler_state_dict"])
248
+ step = checkpoint["step"]
249
+ else:
250
+ step = 0
251
+
252
+ del checkpoint
253
+ gc.collect()
254
+ print("Checkpoint loaded at step", step)
255
+ return step
256
+
257
+ def train(self, resumable_with_seed: int = None):
258
+ train_dataloader = self.train_dataloader
259
+
260
+ start_step = self.load_checkpoint()
261
+ global_step = start_step
262
+
263
+ if resumable_with_seed > 0:
264
+ orig_epoch_step = len(train_dataloader)
265
+ skipped_epoch = int(start_step // orig_epoch_step)
266
+ skipped_batch = start_step % orig_epoch_step
267
+ skipped_dataloader = self.accelerator.skip_first_batches(train_dataloader, num_batches=skipped_batch)
268
+ else:
269
+ skipped_epoch = 0
270
+
271
+ for epoch in range(skipped_epoch, self.epochs):
272
+ self.model.train()
273
+ if resumable_with_seed > 0 and epoch == skipped_epoch:
274
+ progress_bar = tqdm(
275
+ skipped_dataloader,
276
+ desc=f"Epoch {epoch+1}/{self.epochs}",
277
+ unit="step",
278
+ disable=not self.accelerator.is_local_main_process,
279
+ initial=skipped_batch,
280
+ total=orig_epoch_step,
281
+ smoothing=0.15
282
+ )
283
+ else:
284
+ progress_bar = tqdm(
285
+ train_dataloader,
286
+ desc=f"Epoch {epoch+1}/{self.epochs}",
287
+ unit="step",
288
+ disable=not self.accelerator.is_local_main_process,
289
+ smoothing=0.15
290
+ )
291
+
292
+ for batch in progress_bar:
293
+ with self.accelerator.accumulate(self.model):
294
+ text_inputs = batch["lrc"]
295
+ mel_spec = batch["latent"].permute(0, 2, 1)
296
+ mel_lengths = batch["latent_lengths"]
297
+ style_prompt = batch["prompt"]
298
+ style_prompt_lens = batch["prompt_lengths"]
299
+ start_time = batch["start_time"]
300
+
301
+ loss, cond, pred = self.model(
302
+ mel_spec, text=text_inputs, lens=mel_lengths, noise_scheduler=self.noise_scheduler,
303
+ style_prompt=style_prompt,
304
+ style_prompt_lens=style_prompt_lens,
305
+ grad_ckpt=self.grad_ckpt, start_time=start_time
306
+ )
307
+ self.accelerator.backward(loss)
308
+
309
+ if self.max_grad_norm > 0 and self.accelerator.sync_gradients:
310
+ self.accelerator.clip_grad_norm_(self.model.parameters(), self.max_grad_norm)
311
+
312
+ self.optimizer.step()
313
+ self.scheduler.step()
314
+ self.optimizer.zero_grad()
315
+
316
+ if self.is_main:
317
+ self.ema_model.update()
318
+
319
+ global_step += 1
320
+
321
+ if self.accelerator.is_local_main_process:
322
+ self.accelerator.log({"loss": loss.item(), "lr": self.scheduler.get_last_lr()[0]}, step=global_step)
323
+
324
+ progress_bar.set_postfix(step=str(global_step), loss=loss.item())
325
+
326
+ if global_step % (self.save_per_updates * self.grad_accumulation_steps) == 0:
327
+ self.save_checkpoint(global_step)
328
+
329
+ if global_step % self.last_per_steps == 0:
330
+ self.save_checkpoint(global_step, last=True)
331
+
332
+ self.save_checkpoint(global_step, last=True)
333
+
334
+ self.accelerator.end_training()
code/model/utils.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025 ASLP-LAB
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """ This implementation is adapted from github repo:
16
+ https://github.com/SWivid/F5-TTS.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import os
22
+ import random
23
+ from collections import defaultdict
24
+ from importlib.resources import files
25
+
26
+ import torch
27
+ from torch.nn.utils.rnn import pad_sequence
28
+
29
+
30
+ # seed everything
31
+
32
+
33
+ def seed_everything(seed=0):
34
+ random.seed(seed)
35
+ os.environ["PYTHONHASHSEED"] = str(seed)
36
+ torch.manual_seed(seed)
37
+ torch.cuda.manual_seed(seed)
38
+ torch.cuda.manual_seed_all(seed)
39
+ torch.backends.cudnn.deterministic = True
40
+ torch.backends.cudnn.benchmark = False
41
+
42
+
43
+ # helpers
44
+
45
+
46
+ def exists(v):
47
+ return v is not None
48
+
49
+
50
+ def default(v, d):
51
+ return v if exists(v) else d
52
+
53
+
54
+ # tensor helpers
55
+
56
+
57
+ def lens_to_mask(t: int["b"], length: int | None = None) -> bool["b n"]: # noqa: F722 F821
58
+ if not exists(length):
59
+ length = t.amax()
60
+
61
+ seq = torch.arange(length, device=t.device)
62
+ return seq[None, :] < t[:, None]
63
+
64
+
65
+ def mask_from_start_end_indices(seq_len: int["b"], start: int["b"], end: int["b"], max_frames): # noqa: F722 F821
66
+ max_seq_len = max_frames
67
+ seq = torch.arange(max_seq_len, device=start.device).long()
68
+ start_mask = seq[None, :] >= start[:, None]
69
+ end_mask = seq[None, :] < end[:, None]
70
+ return start_mask & end_mask
71
+
72
+
73
+ def mask_from_frac_lengths(seq_len: int["b"], frac_lengths: float["b"], max_frames): # noqa: F722 F821
74
+ lengths = (frac_lengths * seq_len).long()
75
+ max_start = seq_len - lengths
76
+
77
+ rand = torch.rand_like(frac_lengths)
78
+ start = (max_start * rand).long().clamp(min=0)
79
+ end = start + lengths
80
+
81
+ return mask_from_start_end_indices(seq_len, start, end, max_frames)
82
+
83
+
84
+ def maybe_masked_mean(t: float["b n d"], mask: bool["b n"] = None) -> float["b d"]: # noqa: F722
85
+ if not exists(mask):
86
+ return t.mean(dim=1)
87
+
88
+ t = torch.where(mask[:, :, None], t, torch.tensor(0.0, device=t.device))
89
+ num = t.sum(dim=1)
90
+ den = mask.float().sum(dim=1)
91
+
92
+ return num / den.clamp(min=1.0)
93
+
94
+
95
+ # simple utf-8 tokenizer, since paper went character based
96
+ def list_str_to_tensor(text: list[str], padding_value=-1) -> int["b nt"]: # noqa: F722
97
+ list_tensors = [torch.tensor([*bytes(t, "UTF-8")]) for t in text] # ByT5 style
98
+ text = pad_sequence(list_tensors, padding_value=padding_value, batch_first=True)
99
+ return text
100
+
101
+
102
+ # char tokenizer, based on custom dataset's extracted .txt file
103
+ def list_str_to_idx(
104
+ text: list[str] | list[list[str]],
105
+ vocab_char_map: dict[str, int], # {char: idx}
106
+ padding_value=-1,
107
+ ) -> int["b nt"]: # noqa: F722
108
+ list_idx_tensors = [torch.tensor([vocab_char_map.get(c, 0) for c in t]) for t in text] # pinyin or char style
109
+ text = pad_sequence(list_idx_tensors, padding_value=padding_value, batch_first=True)
110
+ return text
111
+
112
+
113
+ # Get tokenizer
114
+
115
+
116
+ def get_tokenizer(dataset_name, tokenizer: str = "pinyin"):
117
+ """
118
+ tokenizer - "pinyin" do g2p for only chinese characters, need .txt vocab_file
119
+ - "char" for char-wise tokenizer, need .txt vocab_file
120
+ - "byte" for utf-8 tokenizer
121
+ - "custom" if you're directly passing in a path to the vocab.txt you want to use
122
+ vocab_size - if use "pinyin", all available pinyin types, common alphabets (also those with accent) and symbols
123
+ - if use "char", derived from unfiltered character & symbol counts of custom dataset
124
+ - if use "byte", set to 256 (unicode byte range)
125
+ """
126
+ if tokenizer in ["pinyin", "char"]:
127
+ tokenizer_path = os.path.join(files("diffrhythm").joinpath("../../data"), f"{dataset_name}_{tokenizer}/vocab.txt")
128
+ with open(tokenizer_path, "r", encoding="utf-8") as f:
129
+ vocab_char_map = {}
130
+ for i, char in enumerate(f):
131
+ vocab_char_map[char[:-1]] = i
132
+ vocab_size = len(vocab_char_map)
133
+ assert vocab_char_map[" "] == 0, "make sure space is of idx 0 in vocab.txt, cuz 0 is used for unknown char"
134
+
135
+ elif tokenizer == "byte":
136
+ vocab_char_map = None
137
+ vocab_size = 256
138
+
139
+ elif tokenizer == "custom":
140
+ with open(dataset_name, "r", encoding="utf-8") as f:
141
+ vocab_char_map = {}
142
+ for i, char in enumerate(f):
143
+ vocab_char_map[char[:-1]] = i
144
+ vocab_size = len(vocab_char_map)
145
+
146
+ return vocab_char_map, vocab_size
147
+
148
+
149
+ # convert char to pinyin
150
+
151
+
152
+ def convert_char_to_pinyin(text_list, polyphone=True):
153
+ final_text_list = []
154
+ god_knows_why_en_testset_contains_zh_quote = str.maketrans(
155
+ {"“": '"', "”": '"', "‘": "'", "’": "'"}
156
+ ) # in case librispeech (orig no-pc) test-clean
157
+ custom_trans = str.maketrans({";": ","}) # add custom trans here, to address oov
158
+ for text in text_list:
159
+ char_list = []
160
+ text = text.translate(god_knows_why_en_testset_contains_zh_quote)
161
+ text = text.translate(custom_trans)
162
+ for seg in jieba.cut(text):
163
+ seg_byte_len = len(bytes(seg, "UTF-8"))
164
+ if seg_byte_len == len(seg): # if pure alphabets and symbols
165
+ if char_list and seg_byte_len > 1 and char_list[-1] not in " :'\"":
166
+ char_list.append(" ")
167
+ char_list.extend(seg)
168
+ elif polyphone and seg_byte_len == 3 * len(seg): # if pure chinese characters
169
+ seg = lazy_pinyin(seg, style=Style.TONE3, tone_sandhi=True)
170
+ for c in seg:
171
+ if c not in "。,、;:?!《》【】—…":
172
+ char_list.append(" ")
173
+ char_list.append(c)
174
+ else: # if mixed chinese characters, alphabets and symbols
175
+ for c in seg:
176
+ if ord(c) < 256:
177
+ char_list.extend(c)
178
+ else:
179
+ if c not in "。,、;:?!《》【】—…":
180
+ char_list.append(" ")
181
+ char_list.extend(lazy_pinyin(c, style=Style.TONE3, tone_sandhi=True))
182
+ else: # if is zh punc
183
+ char_list.append(c)
184
+ final_text_list.append(char_list)
185
+
186
+ return final_text_list
187
+
188
+
189
+ # filter func for dirty data with many repetitions
190
+
191
+
192
+ def repetition_found(text, length=2, tolerance=10):
193
+ pattern_count = defaultdict(int)
194
+ for i in range(len(text) - length + 1):
195
+ pattern = text[i : i + length]
196
+ pattern_count[pattern] += 1
197
+ for pattern, count in pattern_count.items():
198
+ if count > tolerance:
199
+ return True
200
+ return False
code/pyproject.toml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tt-diffrhythm"
7
+ version = "0.1.0"
8
+ description = "DiffRhythm 1.2 Full on one Tenstorrent Blackhole p100a"
9
+ readme = "PYTHON.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "Apache-2.0" }
12
+ # ttnn / tt-metal is not on PyPI: it comes from the tt-metal environment this runs in.
13
+ dependencies = [
14
+ "torch",
15
+ "numpy",
16
+ "safetensors",
17
+ "huggingface_hub",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ test = ["pytest"]
22
+
23
+ [tool.setuptools.packages.find]
24
+ include = ["tt_diffrhythm*"]
code/tt_diffrhythm/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """DiffRhythm 1.2 Full on one Tenstorrent Blackhole p100a.
3
+
4
+ The CFM transformer and the stable-audio VAE decoder run on the card in TTNN. The style
5
+ encoder, the sampler and the post-processing stay on the CPU inside DiffRhythm's own
6
+ environment. `LocalWorker` runs both in one process when one environment satisfies DiffRhythm
7
+ and ttnn at once; `TTWorker` keeps them apart, talking to the card through a worker process over
8
+ a Unix socket, when it does not.
9
+ """
10
+ from .common import close_device, dram_stats, open_device
11
+ from .diffrhythm_dit import DiffRhythmDiT, DRPrecision
12
+ from .local import LocalWorker
13
+ from .oobleck_vae import OobleckTT
14
+ from .remote import RemoteDiffRhythmDiT, RemoteVae, TTWorker
15
+
16
+ __all__ = ["DiffRhythmDiT", "DRPrecision", "OobleckTT", "TTWorker", "LocalWorker", "RemoteDiffRhythmDiT",
17
+ "RemoteVae", "open_device", "close_device", "dram_stats"]
18
+ __version__ = "0.1.0"
code/tt_diffrhythm/common.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Device and host helpers shared with the ACE-Step port.
3
+
4
+ Extracted verbatim from `tt-acestep` (Apache-2.0), where the same code opens the card, reports
5
+ DRAM, moves tensors, and prepares checkpoint tensors for ttnn: the matmul layout, the
6
+ adjacent-pair head permutation ttnn's rotary kernel expects, and the tiled rotation matrix.
7
+
8
+ The two ports also share `worker.py`, `remote.py`, `wire.py` and `oobleck_vae.py`; this module
9
+ exists so that this repository stands alone.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from dataclasses import dataclass
14
+
15
+ import torch
16
+ import ttnn
17
+
18
+ MEM = ttnn.DRAM_MEMORY_CONFIG
19
+
20
+
21
+ def open_device(l1_small_size: int = 98304): # convs (VAE) keep their config tensors in L1_SMALL
22
+ return ttnn.open_mesh_device(mesh_shape=ttnn.MeshShape(1, 1),
23
+ dispatch_core_config=ttnn.DispatchCoreConfig(ttnn.DispatchCoreType.WORKER),
24
+ l1_small_size=l1_small_size)
25
+
26
+
27
+ def close_device(dev):
28
+ ttnn.close_mesh_device(dev)
29
+
30
+
31
+ def dram_stats(dev) -> dict:
32
+ try:
33
+ s = ttnn.get_memory_view(dev, ttnn.BufferType.DRAM)
34
+ return {"allocated_bytes": s.total_bytes_allocated_per_bank * s.num_banks,
35
+ "free_bytes": s.total_bytes_free_per_bank * s.num_banks}
36
+ except Exception: # pragma: no cover - diagnostics only
37
+ return {}
38
+
39
+
40
+ @dataclass
41
+ class Precision:
42
+ weight_dtype: ttnn.DataType = ttnn.bfloat16
43
+ mm_fidelity: ttnn.MathFidelity = ttnn.MathFidelity.HiFi2
44
+ sdpa_fidelity: ttnn.MathFidelity = ttnn.MathFidelity.HiFi4
45
+ sdpa_fp32_acc: bool = True
46
+
47
+
48
+ def _dev(dev, t: torch.Tensor, dtype=ttnn.bfloat16):
49
+ return ttnn.from_torch(t.contiguous(), dtype=dtype, layout=ttnn.TILE_LAYOUT, device=dev, memory_config=MEM)
50
+
51
+
52
+ def _row(dev, v: torch.Tensor):
53
+ """[D] -> [1, 1, 1, D] bf16 row (norm gamma, shift, gate, bias)."""
54
+ return _dev(dev, v.to(torch.bfloat16).reshape(1, 1, 1, -1))
55
+
56
+
57
+ TILE = 32
58
+ SEQ_MULTIPLE = 128 # sequence lengths are padded to multiples of this at least (SDPA chunk sizes divide it)
59
+
60
+
61
+ NEG = -1e9 # additive mask value for excluded keys (finite, representable in bf16)
62
+
63
+
64
+ def linear_to_mm(w: torch.Tensor) -> torch.Tensor:
65
+ """nn.Linear weight [out, in] -> matmul weight [in, out]."""
66
+ return w.t().contiguous()
67
+
68
+
69
+ def interleave_pairs_permutation(head_dim: int) -> torch.Tensor:
70
+ """new[j] = old[p[j]]: rotate-half pairs (i, i + D/2) -> adjacent pairs (2i, 2i + 1).
71
+
72
+ Applying it to the rows of q_proj/k_proj (per head) and to q_norm/k_norm leaves every q.k product unchanged and
73
+ lets tt's rotary_embedding_llama (adjacent pairs) replace upstream's rotate_half RoPE."""
74
+ half = head_dim // 2
75
+ p = torch.empty(head_dim, dtype=torch.long)
76
+ p[0::2] = torch.arange(half)
77
+ p[1::2] = torch.arange(half) + half
78
+ return p
79
+
80
+
81
+ def permute_heads_rows(w_out_in: torch.Tensor, n_heads: int, head_dim: int, perm: torch.Tensor) -> torch.Tensor:
82
+ out, inp = w_out_in.shape
83
+ assert out == n_heads * head_dim
84
+ return w_out_in.view(n_heads, head_dim, inp)[:, perm, :].reshape(out, inp).contiguous()
85
+
86
+
87
+ def rot_transformation_mat(tile: int = TILE) -> torch.Tensor:
88
+ """[1, 1, 32, 32] T with (x @ T)[2k] = -x[2k+1], (x @ T)[2k+1] = x[2k] (adjacent-pair rotation)."""
89
+ m = torch.zeros(1, 1, tile, tile)
90
+ m[..., torch.arange(0, tile, 2), torch.arange(1, tile, 2)] = 1.0
91
+ m[..., torch.arange(1, tile, 2), torch.arange(0, tile, 2)] = -1.0
92
+ return m
93
+
94
+
95
+ def rms(x: torch.Tensor, w: torch.Tensor, eps: float) -> torch.Tensor:
96
+ return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + eps) * w
97
+
98
+
99
+ def round_up(n: int, m: int = SEQ_MULTIPLE) -> int:
100
+ return (n + m - 1) // m * m
101
+
102
+
103
+ def swiglu_interleave(gate_out_in: torch.Tensor, up_out_in: torch.Tensor, tile: int = TILE) -> torch.Tensor:
104
+ """[K, 2N] weight for minimal_matmul(fuse_swiglu=True): column tile 2p = gate tile p, 2p+1 = up tile p."""
105
+ g, u = linear_to_mm(gate_out_in), linear_to_mm(up_out_in)
106
+ K, N = g.shape
107
+ assert N % tile == 0, N
108
+ return torch.stack([g.view(K, N // tile, tile), u.view(K, N // tile, tile)], dim=2).reshape(K, 2 * N).contiguous()
109
+
110
+
111
+ def apply_rope_adjacent(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
112
+ rot = torch.stack([-x[..., 1::2], x[..., 0::2]], dim=-1).flatten(-2)
113
+ return x * cos + rot * sin
code/tt_diffrhythm/diffrhythm_dit.py ADDED
@@ -0,0 +1,401 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """DiffRhythm 1.2 DiT on ONE Blackhole (P100a), TTNN, batch 1.
3
+
4
+ Mirrors diffrhythm_host.RefDecoder op for op, with the block kernels of acestep_dit.py (fused QKV minimal_matmul,
5
+ adjacent-pair rotary_embedding_llama, fused SwiGLU, fp32-accumulated SDPA).
6
+
7
+ Per song (prepare): for each CFG branch, the timestep-invariant part of the input projection
8
+ [text_embed @ Wtext + cond @ Wcond] and the 8 text-fusion residuals silu(text_embed @ Wf + bf) are computed on the
9
+ device from the host's text embedding and kept there; plus the RoPE tables and the masks.
10
+ Per call (forward): x @ Wx + that base + the call's row (style/time), zero past the song, two grouped k31 convs with
11
+ Mish (zeroing the padding before each), 16 Llama layers (32 x 64 heads, padded keys excluded), the fusion residuals
12
+ after the first 8, then LayerNorm * (1 + scale) + shift and proj_out.
13
+
14
+ Speed (dit_profile.py, sdpa_check.py, conv_speed.py, mm_sweep.py): the attention excludes the padding with
15
+ block-diagonal windows [0, frames, S] built on the device instead of reading an [S, S] mask for every head (30 % faster
16
+ at 6144 frames); each position conv is one matmul over the 31 shifted rows (unfold) instead of ttnn's grouped conv,
17
+ which expands the 16 groups densely anyway; the matmuls use blockings measured for these shapes.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass
22
+ from typing import Dict, List, Optional
23
+
24
+ import torch
25
+ import ttnn
26
+
27
+ from . import diffrhythm_host as host
28
+ from .common import MEM, Precision, _dev, _row, close_device, dram_stats, open_device # noqa: F401
29
+ from .common import rot_transformation_mat
30
+
31
+ try:
32
+ from models.tt_dit.utils.matmul import get_matmul_config
33
+ except ImportError: # pragma: no cover - only on hosts without tt-metal models/
34
+ get_matmul_config = None
35
+
36
+
37
+ @dataclass
38
+ class DRPrecision(Precision):
39
+ """Precision on top of acestep_dit.Precision. CFG (pred + 4 (pred - null)) amplifies the error of the small
40
+ difference between the two branches, and 31 steps accumulate it, so a DiT call that matches the GPU at PCC 0.9999
41
+ still drifts over a song. Closed-loop check (device_check.py --closed-loop, 96 s song, final latents vs the GPU's;
42
+ CPU float32 0.99997, CPU all-bfloat16 0.991): all bfloat16 0.970; the defaults below 0.994-0.995 for +1.2 s per
43
+ song. Splitting the weights (bf16 hi + lo) or keeping the branch tensors in float32 gained nothing more
44
+ (experiments/tt-diffrhythm/results/precision_sweep_d96.json)."""
45
+
46
+ mm_fidelity: ttnn.MathFidelity = ttnn.MathFidelity.HiFi4
47
+ x_fp32: bool = True # upload the noisy latents in float32 (the input projection reads them)
48
+ residual_fp32: bool = True # keep the residual stream in float32 (branch outputs are added into it)
49
+ out_fp32: bool = True # the final projection writes float32
50
+ rows_fp32: bool = True # the per-call rows (style/time input row, output scale and shift) in float32: their
51
+ # rounding is the same for every frame and step, so it adds up instead of averaging out
52
+ consts_fp32: bool = True # the same for the constant per-channel rows: RMSNorm weights and proj_out's bias
53
+ windowed_attention: bool = True # padded keys excluded by on-device windows (False: an [S, S] additive mask)
54
+ unfold_conv: bool = True # position convs as one matmul over the unfolded rows (False: ttnn.conv1d, groups=16)
55
+
56
+
57
+ # minimal_matmul blocks (M, K, N, (subblock h, w)) per (K, N) on the P100a's 11x10 grid (mm_sweep.py: 5-15 % faster
58
+ # than tt-metal's 8x8x8 default); a list of (largest M, blocks) where the best depends on the length
59
+ MM_BLOCKS = {
60
+ (2048, 6144): [(None, (4, 8, 4, (4, 1)))], # qkv
61
+ (2048, 2048): [(None, (4, 4, 4, (4, 1)))], # o
62
+ (2048, 16384): [(3072, (8, 8, 8, (2, 2))), (None, (4, 8, 8, (4, 1)))], # gate/up, fused SwiGLU
63
+ (8192, 2048): [(None, (4, 8, 4, (4, 1)))], # down
64
+ }
65
+
66
+
67
+ def conv_unfold_weight(w: torch.Tensor, groups: int) -> torch.Tensor:
68
+ """Grouped conv1d weight [C, C / groups, k] -> matmul weight [k * C, C] for the unfolded input (row t holds the
69
+ input rows t - k // 2 ... t + k // 2 side by side): block-diagonal per tap, row j * C + (g * cg + i), column
70
+ g * cg + o."""
71
+ C, cg, k = w.shape
72
+ out = torch.zeros(k, C, C)
73
+ wg = w.view(groups, cg, cg, k) # g, o, i, tap
74
+ for g in range(groups):
75
+ out[:, g * cg:(g + 1) * cg, g * cg:(g + 1) * cg] = wg[g].permute(2, 1, 0) # tap, i, o
76
+ return out.reshape(k * C, C)
77
+
78
+
79
+ class Layer:
80
+ def __init__(self, dev, w: host.LayerWeights, prec: Precision):
81
+ wd = prec.weight_dtype
82
+ self.wqkv = _dev(dev, w.wqkv.to(torch.bfloat16), wd)
83
+ self.wo = _dev(dev, w.wo.to(torch.bfloat16), wd)
84
+ self.w_gateup = _dev(dev, w.w_gateup.to(torch.bfloat16), wd)
85
+ self.w_down = _dev(dev, w.w_down.to(torch.bfloat16), wd)
86
+ if getattr(prec, "consts_fp32", False):
87
+ self.ln1, self.ln2 = (_dev(dev, v.float().reshape(1, 1, 1, -1), ttnn.float32) for v in (w.ln1, w.ln2))
88
+ else:
89
+ self.ln1, self.ln2 = _row(dev, w.ln1), _row(dev, w.ln2)
90
+
91
+
92
+ @dataclass
93
+ class Song:
94
+ """Device state of one generation: per-branch base and fusion residuals, tables and masks."""
95
+
96
+ geo: host.Geometry
97
+ branches: Dict[bool, dict] # drop_text -> {"base": tensor, "fusion": [8 tensors]}
98
+ cos: ttnn.Tensor
99
+ sin: ttnn.Tensor
100
+ key_mask: Optional[ttnn.Tensor] # [S, S] additive mask, or None with windowed attention
101
+ windows: Optional[ttnn.Tensor] # cu_window_seqlens [0, frames, S] (or [0, S]) for windowed attention
102
+ row_mask: ttnn.Tensor
103
+
104
+ def tensors(self) -> List[ttnn.Tensor]:
105
+ out = [t for t in (self.cos, self.sin, self.key_mask, self.windows, self.row_mask) if t is not None]
106
+ for b in self.branches.values():
107
+ out += [b["base"], *b["fusion"]]
108
+ return out
109
+
110
+
111
+ class DiffRhythmDiT:
112
+ def __init__(self, dev, ckpt: host.Checkpoint, prec: Optional[Precision] = None, keep_ref: bool = False):
113
+ self.dev, self.cfg = dev, ckpt.cfg
114
+ self.prec = prec or DRPrecision()
115
+ self.res_dtype = ttnn.float32 if getattr(self.prec, "residual_fp32", False) else ttnn.bfloat16
116
+ self.ref = host.RefDecoder(ckpt) # host weights: layouts and the per-call rows
117
+ self.grid = dev.compute_with_storage_grid_size()
118
+ r = self.ref
119
+ self.layers = [Layer(dev, w, self.prec) for w in r.layers]
120
+ bf = lambda t: _dev(dev, t.to(torch.bfloat16))
121
+ self.wx, self.wcond, self.wtext = bf(r.iw.wx), bf(r.iw.wcond), bf(r.iw.wtext)
122
+ self.fusion = [(bf(w), _row(dev, b)) for w, b in r.fusion]
123
+ self.unfold = getattr(self.prec, "unfold_conv", False)
124
+ if self.unfold:
125
+ self.conv = [(bf(conv_unfold_weight(w, self.cfg.conv_groups)), _row(dev, b)) for w, b in r.conv]
126
+ else:
127
+ self.conv = [(ttnn.from_torch(w.unsqueeze(2).contiguous(), dtype=ttnn.float32),
128
+ ttnn.from_torch(b.reshape(1, 1, 1, -1), dtype=ttnn.float32)) for w, b in r.conv]
129
+ self.w_out, self.b_out = bf(r.w_out), _row(dev, r.b_out)
130
+ self.b_out32 = (_dev(dev, r.b_out.float().reshape(1, 1, 1, -1), ttnn.float32)
131
+ if getattr(self.prec, "consts_fp32", False) else None)
132
+ self.trans_mat = _dev(dev, rot_transformation_mat())
133
+ arch = dev.arch()
134
+ ck = lambda fid, fp32: ttnn.init_device_compute_kernel_config(
135
+ arch, math_fidelity=fid, math_approx_mode=False, fp32_dest_acc_en=fp32, packer_l1_acc=False)
136
+ self.ck_mm = ck(self.prec.mm_fidelity, True)
137
+ self.ck_norm = ck(ttnn.MathFidelity.HiFi4, True)
138
+ self.ck_sdpa = ck(self.prec.sdpa_fidelity, self.prec.sdpa_fp32_acc)
139
+ self.ck_rope = ck(ttnn.MathFidelity.HiFi4, True)
140
+ self.ck_conv = ck(ttnn.MathFidelity.HiFi4, True)
141
+ self._mm_cfg_cache: Dict[tuple, object] = {}
142
+ self.windowed = getattr(self.prec, "windowed_attention", False)
143
+ if not self.unfold:
144
+ # ttnn's conv moves its weights onto the device (in its own layout) on the first call. Done now, before
145
+ # any song, so they do not land behind a song's tensors (the VAE's later allocations, and so its compiled
146
+ # kernels, would then depend on the song length). The prepared weights serve every sequence bucket
147
+ # (bit-identical to weights prepared at each length).
148
+ S = host.seq_buckets()[0]
149
+ z = _dev(dev, torch.zeros(1, 1, S, self.cfg.dim, dtype=torch.bfloat16))
150
+ for i in range(2):
151
+ ttnn.deallocate(self._conv(z, i, S))
152
+ ttnn.deallocate(z)
153
+ if not keep_ref: # the per-call rows need only the small input/output weights on the host
154
+ r.layers = None
155
+
156
+ # ------------------------------------------------------------------ song
157
+ def prepare(self, text_embed: Dict[bool, torch.Tensor], cond: torch.Tensor) -> Song:
158
+ """text_embed {drop_text: [S, 512]} from upstream's TextEmbedding, cond [S, 64] -> device state."""
159
+ c = self.cfg
160
+ geo = host.Geometry(frames=cond.shape[0])
161
+ S = geo.seq_pad
162
+ branches = {}
163
+ cond_d = _dev(self.dev, host.pad_rows(cond.float(), S).to(torch.bfloat16).reshape(1, 1, S, -1))
164
+ for null, te in text_embed.items():
165
+ t = _dev(self.dev, host.pad_rows(te.float(), S).to(torch.bfloat16).reshape(1, 1, S, -1))
166
+ bd = ttnn.bfloat16
167
+ base = ttnn.linear(t, self.wtext, compute_kernel_config=self.ck_mm, memory_config=MEM, dtype=bd)
168
+ if not null: # the CFG branch drops the audio condition (zeros)
169
+ bc = ttnn.linear(cond_d, self.wcond, compute_kernel_config=self.ck_mm, memory_config=MEM, dtype=bd)
170
+ base = self._add_free(base, bc)
171
+ fusion = []
172
+ for w, b in self.fusion:
173
+ f = ttnn.linear(t, w, bias=b, compute_kernel_config=self.ck_mm, memory_config=MEM, dtype=bd)
174
+ fusion.append(ttnn.silu(f, memory_config=MEM))
175
+ ttnn.deallocate(f)
176
+ ttnn.deallocate(t)
177
+ branches[null] = {"base": base, "fusion": fusion}
178
+ ttnn.deallocate(cond_d)
179
+ cos, sin = host.rope_tables(S, c)
180
+ f = lambda m: _dev(self.dev, m.to(torch.bfloat16)[None, None])
181
+ rows = host.row_mask(geo).expand(S, c.dim)
182
+ if self.windowed:
183
+ bounds = [0, geo.frames, S] if geo.frames < S else [0, S]
184
+ windows = ttnn.from_torch(torch.tensor(bounds, dtype=torch.int32), dtype=ttnn.int32,
185
+ layout=ttnn.ROW_MAJOR_LAYOUT, device=self.dev)
186
+ key_mask = None
187
+ else:
188
+ windows, key_mask = None, f(host.key_mask(geo))
189
+ return Song(geo, branches, f(cos), f(sin), key_mask, windows, f(rows))
190
+
191
+ def release(self, song: Song):
192
+ for t in song.tensors():
193
+ ttnn.deallocate(t)
194
+
195
+ # ------------------------------------------------------------------ ops
196
+ def _mm_config(self, M, K, N):
197
+ for max_m, (mb, kb, nb, (sh, sw)) in MM_BLOCKS.get((K, N), []):
198
+ if max_m is None or M <= max_m:
199
+ return ttnn.MinimalMatmulConfig(M_block_size=mb, K_block_size=kb, N_block_size=nb, subblock_h=sh,
200
+ subblock_w=sw, compute_with_storage_grid_size=self.grid)
201
+ return get_matmul_config(M, K, N, self.grid)
202
+
203
+ def _mm(self, x, w, M, K, N, fuse_swiglu=False, dtype=ttnn.bfloat16):
204
+ key = (M, K, N)
205
+ if key not in self._mm_cfg_cache:
206
+ self._mm_cfg_cache[key] = self._mm_config(M, K, N)
207
+ out = ttnn.experimental.minimal_matmul(x, w, config=self._mm_cfg_cache[key], compute_kernel_config=self.ck_mm,
208
+ dtype=dtype, memory_config=MEM, fuse_swiglu=fuse_swiglu)
209
+ if len(out.shape) != 4:
210
+ out = ttnn.reshape(out, [1, 1, M, out.shape[-1]])
211
+ return out
212
+
213
+ @staticmethod
214
+ def _add_free(a, b, dtype=None):
215
+ out = ttnn.add(a, b, memory_config=MEM, dtype=dtype) if dtype is not None else ttnn.add(a, b, memory_config=MEM)
216
+ ttnn.deallocate(a)
217
+ ttnn.deallocate(b)
218
+ return out
219
+
220
+ def _residual(self, h, o):
221
+ """h + o in the residual stream's dtype; frees both."""
222
+ if self.res_dtype == ttnn.bfloat16:
223
+ return self._add_free(h, o)
224
+ if o.dtype != ttnn.float32:
225
+ o32 = ttnn.typecast(o, ttnn.float32)
226
+ ttnn.deallocate(o)
227
+ o = o32
228
+ return self._add_free(h, o)
229
+
230
+ def _bf16(self, x):
231
+ """x as bfloat16 for a matmul; frees x when it had to be converted."""
232
+ if x.dtype == ttnn.bfloat16:
233
+ return x
234
+ out = ttnn.typecast(x, ttnn.bfloat16)
235
+ ttnn.deallocate(x)
236
+ return out
237
+
238
+ def _rms(self, x, w):
239
+ n = ttnn.rms_norm(x, epsilon=self.cfg.eps, weight=w, compute_kernel_config=self.ck_norm, memory_config=MEM)
240
+ return self._bf16(n)
241
+
242
+ def _mish(self, x):
243
+ if hasattr(ttnn, "mish"):
244
+ return ttnn.mish(x, memory_config=MEM)
245
+ sp = ttnn.softplus(x, memory_config=MEM)
246
+ th = ttnn.tanh(sp, memory_config=MEM)
247
+ ttnn.deallocate(sp)
248
+ out = ttnn.multiply(x, th, memory_config=MEM)
249
+ ttnn.deallocate(th)
250
+ return out
251
+
252
+ def _conv(self, x, i, S):
253
+ if self.unfold:
254
+ return self._conv_unfold(x, i, S)
255
+ c = self.cfg
256
+ w, b = self.conv[i]
257
+ cfg = ttnn.Conv2dConfig(weights_dtype=ttnn.bfloat16, config_tensors_in_dram=True)
258
+ out, out_len, (w, b) = ttnn.conv1d(
259
+ input_tensor=x, weight_tensor=w, bias_tensor=b, device=self.dev, in_channels=c.dim, out_channels=c.dim,
260
+ batch_size=1, input_length=S, kernel_size=c.conv_kernel, stride=1, padding=c.conv_kernel // 2, dilation=1,
261
+ groups=c.conv_groups, dtype=ttnn.bfloat16, compute_config=self.ck_conv, conv_config=cfg,
262
+ return_output_dim=True, return_weights_and_bias=True)
263
+ self.conv[i] = (w, b) # prepared on the device by the first call
264
+ if out.layout != ttnn.TILE_LAYOUT:
265
+ out = ttnn.to_layout(out, ttnn.TILE_LAYOUT)
266
+ return ttnn.reshape(out, [1, 1, out_len, c.dim])
267
+
268
+ def _conv_unfold(self, x, i, S):
269
+ """[1, 1, S, C] -> the same conv as one matmul: rows padded by k // 2 zeros at both ends, the k shifted
270
+ copies side by side ([S, k * C]), times the block-diagonal weight."""
271
+ c = self.cfg
272
+ k, pad = c.conv_kernel, c.conv_kernel // 2
273
+ w, b = self.conv[i]
274
+ xr = ttnn.reshape(ttnn.to_layout(x, ttnn.ROW_MAJOR_LAYOUT), [S, c.dim])
275
+ xp = ttnn.pad(xr, [(pad, pad), (0, 0)], 0.0)
276
+ ttnn.deallocate(xr)
277
+ parts = [ttnn.slice(xp, [j, 0], [j + S, c.dim]) for j in range(k)]
278
+ ttnn.deallocate(xp)
279
+ xu = ttnn.concat(parts, dim=1, memory_config=MEM)
280
+ for t in parts:
281
+ ttnn.deallocate(t)
282
+ xt = ttnn.to_layout(ttnn.reshape(xu, [1, 1, S, k * c.dim]), ttnn.TILE_LAYOUT)
283
+ ttnn.deallocate(xu)
284
+ y = self._mm(xt, w, S, k * c.dim, c.dim)
285
+ ttnn.deallocate(xt)
286
+ out = ttnn.add(y, b, memory_config=MEM)
287
+ ttnn.deallocate(y)
288
+ return out
289
+
290
+ def _conv_pos(self, h, song: Song, S):
291
+ """Two k31 convs with Mish; the rows past the song are zeroed before each (upstream's sequence ends there)."""
292
+ y = h
293
+ for i in range(2):
294
+ m = ttnn.multiply(y, song.row_mask, memory_config=MEM)
295
+ if y is not h:
296
+ ttnn.deallocate(y)
297
+ conv = self._conv(m, i, S)
298
+ ttnn.deallocate(m)
299
+ y = self._mish(conv)
300
+ ttnn.deallocate(conv)
301
+ return y
302
+
303
+ def _sdpa(self, q, k, v, song: Song, S):
304
+ # sdpa_check.py: 256 x 256 chunks are fastest from 3072 frames on; at 2560 windowed attention prefers 128-row Q
305
+ # chunks (S is a multiple of SEQ_BUCKET = 256, so both divide it)
306
+ q_chunk = 128 if self.windowed and S <= 2560 else 256
307
+ cfg = ttnn.SDPAProgramConfig(compute_with_storage_grid_size=self.grid, q_chunk_size=q_chunk, k_chunk_size=256,
308
+ exp_approx_mode=False)
309
+ exclude = {"cu_window_seqlens": song.windows} if self.windowed else {"attn_mask": song.key_mask}
310
+ return ttnn.transformer.scaled_dot_product_attention(
311
+ q, k, v, is_causal=False, scale=self.cfg.head_dim ** -0.5, program_config=cfg,
312
+ compute_kernel_config=self.ck_sdpa, memory_config=MEM, **exclude)
313
+
314
+ def _attention(self, h, L: Layer, S, song: Song):
315
+ c = self.cfg
316
+ qkv = self._mm(h, L.wqkv, S, c.dim, 3 * c.dim)
317
+ q, k, v = ttnn.experimental.nlp_create_qkv_heads(qkv, num_heads=c.heads, num_kv_heads=c.heads,
318
+ transpose_k_heads=False, memory_config=MEM)
319
+ ttnn.deallocate(qkv)
320
+ roped = []
321
+ for t in (q, k):
322
+ roped.append(ttnn.experimental.rotary_embedding_llama(t, song.cos, song.sin, self.trans_mat,
323
+ is_decode_mode=False, compute_kernel_config=self.ck_rope))
324
+ ttnn.deallocate(t)
325
+ a = self._sdpa(roped[0], roped[1], v, song, S)
326
+ for t in (*roped, v):
327
+ ttnn.deallocate(t)
328
+ a2 = ttnn.transformer.concatenate_heads(a, memory_config=MEM)
329
+ ttnn.deallocate(a)
330
+ o = self._mm(a2, L.wo, S, c.dim, c.dim, dtype=self.res_dtype) # added into the residual stream as is
331
+ ttnn.deallocate(a2)
332
+ return o
333
+
334
+ # ------------------------------------------------------------------ public
335
+ def forward(self, x_t: torch.Tensor, drop_text: bool, style: torch.Tensor, c_emb: torch.Tensor, song: Song,
336
+ taps: Optional[list] = None) -> torch.Tensor:
337
+ """[S, 64] noisy latents, the branch, style [512] and c = time + start + duration embeddings [512]
338
+ -> [S, 64] float32 prediction."""
339
+ c, geo = self.cfg, song.geo
340
+ S = geo.seq_pad
341
+ branch = song.branches[drop_text]
342
+ rows = self.ref.rows(style.float(), c_emb.float())
343
+ if getattr(self.prec, "rows_fp32", False):
344
+ row = lambda v: _dev(self.dev, v.float().reshape(1, 1, 1, -1), ttnn.float32)
345
+ else:
346
+ row = lambda v: _row(self.dev, v)
347
+ in_row, gamma, shift = row(rows.input_row), row(1 + rows.out_scale), row(rows.out_shift)
348
+ p = self.prec
349
+ xs = host.pad_rows(x_t.float(), S).reshape(1, 1, S, -1)
350
+ if getattr(p, "x_fp32", False):
351
+ x = _dev(self.dev, xs, ttnn.float32)
352
+ else:
353
+ x = _dev(self.dev, xs.to(torch.bfloat16))
354
+ xw = ttnn.linear(x, self.wx, compute_kernel_config=self.ck_mm, memory_config=MEM, dtype=self.res_dtype)
355
+ ttnn.deallocate(x)
356
+ h0 = ttnn.add(xw, branch["base"], memory_config=MEM, dtype=self.res_dtype)
357
+ ttnn.deallocate(xw)
358
+ h1 = ttnn.add(h0, in_row, memory_config=MEM, dtype=self.res_dtype)
359
+ ttnn.deallocate(h0)
360
+ h = ttnn.multiply(h1, song.row_mask, memory_config=MEM, dtype=self.res_dtype)
361
+ ttnn.deallocate(h1)
362
+ conv_in = ttnn.typecast(h, ttnn.bfloat16) if self.res_dtype != ttnn.bfloat16 else h
363
+ pos = self._conv_pos(conv_in, song, S)
364
+ if conv_in is not h:
365
+ ttnn.deallocate(conv_in)
366
+ h = self._residual(h, pos)
367
+ tap = (lambda v: taps.append(ttnn.to_torch(v)[0, 0].float())) if taps is not None else (lambda v: None)
368
+ for i, L in enumerate(self.layers):
369
+ a = self._rms(h, L.ln1)
370
+ o = self._attention(a, L, S, song)
371
+ ttnn.deallocate(a)
372
+ h = self._residual(h, o)
373
+ m = self._rms(h, L.ln2)
374
+ gu = self._mm(m, L.w_gateup, S, c.dim, 2 * c.intermediate, fuse_swiglu=True)
375
+ ttnn.deallocate(m)
376
+ d = self._mm(gu, L.w_down, S, c.intermediate, c.dim, dtype=self.res_dtype)
377
+ ttnn.deallocate(gu)
378
+ h = self._residual(h, d)
379
+ if i < c.fusion_layers:
380
+ h2 = ttnn.add(h, branch["fusion"][i], memory_config=MEM, dtype=self.res_dtype)
381
+ ttnn.deallocate(h)
382
+ h = h2
383
+ tap(h)
384
+ n = ttnn.layer_norm(h, epsilon=c.eps, compute_kernel_config=self.ck_norm, memory_config=MEM)
385
+ ttnn.deallocate(h)
386
+ g = ttnn.multiply(n, gamma, memory_config=MEM)
387
+ ttnn.deallocate(n)
388
+ mod = self._bf16(ttnn.add(g, shift, memory_config=MEM))
389
+ ttnn.deallocate(g)
390
+ if self.b_out32 is not None:
391
+ o = ttnn.linear(mod, self.w_out, compute_kernel_config=self.ck_mm, memory_config=MEM, dtype=ttnn.float32)
392
+ out = ttnn.add(o, self.b_out32, memory_config=MEM)
393
+ ttnn.deallocate(o)
394
+ else:
395
+ out = ttnn.linear(mod, self.w_out, bias=self.b_out, compute_kernel_config=self.ck_mm, memory_config=MEM,
396
+ dtype=ttnn.float32 if getattr(p, "out_fp32", False) else ttnn.bfloat16)
397
+ ttnn.deallocate(mod)
398
+ host_out = ttnn.to_torch(out)[0, 0, :geo.frames, : c.mel_dim].float()
399
+ for t in (out, in_row, gamma, shift):
400
+ ttnn.deallocate(t)
401
+ return host_out
code/tt_diffrhythm/diffrhythm_host.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Host (torch, CPU) side of the DiffRhythm 1.2 DiT port to one Tenstorrent P100a.
2
+
3
+ Upstream (DiffRhythm model/dit.py DiT.forward) as used by CFM.sample, per call:
4
+ c = time_embed(t) + start_time_embed(start) + duration_time_embed(duration) [512]
5
+ text_embed = TextEmbedding(text) (embedding + sinusoid + 4 ConvNeXtV2 blocks; zeros in for drop_text) [S, 512]
6
+ h = proj(cat(x, cond, text_embed, style, c)) -> [S, 2048]; h = ConvPos(h) + h (two k31 groups-16 convs + Mish)
7
+ 16 x LlamaDecoderLayer (RMSNorm, 32 x 64 heads, RoPE theta 1e4, SwiGLU 8192; no mask, bidirectional);
8
+ after each of the first 8: h += silu(text_fusion_linears[i](text_embed))
9
+ out = proj_out(LayerNorm(h) * (1 + scale) + shift), scale, shift = linear(silu(c)).chunk(2)
10
+ The CFG branch (drop_audio_cond + drop_text) zeroes cond and the text tokens and uses the negative style.
11
+
12
+ Split: the timestep-invariant text embedding and the small time MLPs stay in the DiffRhythm process on the host
13
+ (upstream modules, exact); everything weight-heavy runs on the card. The input projection is split by input:
14
+ h = x @ Wx + [cond @ Wcond + text_embed @ Wtext] (per song and branch) + [style @ Ws + c @ Wc + b] (per call row)
15
+ RefDecoder runs this formulation in float32 so it can be checked against upstream without a device.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ from dataclasses import dataclass
20
+ from typing import Dict
21
+
22
+ import torch
23
+
24
+ from .common import (NEG, TILE, apply_rope_adjacent, interleave_pairs_permutation, linear_to_mm,
25
+ permute_heads_rows, rms, round_up, swiglu_interleave)
26
+
27
+ SEQ_BUCKET = 256 # frames padded to multiples of this: 96-285 s songs (2067-6136 frames) -> 16 shapes
28
+ LATENT_RATE = 44100 / 2048 # latent frames per second (get_lrc_token: int(seconds * 44100 / 2048))
29
+ MIN_FRAMES, MAX_FRAMES = int(96 * LATENT_RATE), int(285 * LATENT_RATE) # the 96-285 s the port was exercised over
30
+
31
+
32
+ def seq_buckets():
33
+ """Every padded length a DiffRhythm song can have (warm_cache.py compiles them ahead of time)."""
34
+ return sorted({round_up(n, SEQ_BUCKET) for n in range(MIN_FRAMES, MAX_FRAMES + 1)})
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class Config:
39
+ dim: int = 2048
40
+ depth: int = 16
41
+ heads: int = 32
42
+ head_dim: int = 64
43
+ intermediate: int = 8192
44
+ text_dim: int = 512
45
+ cond_dim: int = 512
46
+ mel_dim: int = 64
47
+ fusion_layers: int = 8
48
+ conv_kernel: int = 31
49
+ conv_groups: int = 16
50
+ rope_theta: float = 10000.0
51
+ eps: float = 1e-6
52
+
53
+
54
+ class Checkpoint:
55
+ """transformer.* of DiffRhythm's cfm_model.pt (fp16 on disk), read as float32."""
56
+
57
+ def __init__(self, path):
58
+ sd = torch.load(str(path), weights_only=True, map_location="cpu")["model_state_dict"]
59
+ self._sd = {k[len("transformer."):]: v for k, v in sd.items() if k.startswith("transformer.")}
60
+ self.cfg = Config()
61
+
62
+ def get(self, name: str) -> torch.Tensor:
63
+ return self._sd[name].float()
64
+
65
+
66
+ @dataclass
67
+ class LayerWeights:
68
+ wqkv: torch.Tensor # [2048, 3 * 2048], q/k rows permuted to adjacent pairs
69
+ wo: torch.Tensor
70
+ w_gateup: torch.Tensor # [2048, 2 * 8192] tile-interleaved
71
+ w_down: torch.Tensor
72
+ ln1: torch.Tensor
73
+ ln2: torch.Tensor
74
+
75
+
76
+ def layer_weights(ckpt: Checkpoint, i: int) -> LayerWeights:
77
+ c = ckpt.cfg
78
+ g = lambda k: ckpt.get(f"transformer_blocks.{i}.{k}")
79
+ perm = interleave_pairs_permutation(c.head_dim)
80
+ wq = permute_heads_rows(g("self_attn.q_proj.weight"), c.heads, c.head_dim, perm)
81
+ wk = permute_heads_rows(g("self_attn.k_proj.weight"), c.heads, c.head_dim, perm)
82
+ return LayerWeights(
83
+ wqkv=torch.cat([linear_to_mm(wq), linear_to_mm(wk), linear_to_mm(g("self_attn.v_proj.weight"))], dim=1),
84
+ wo=linear_to_mm(g("self_attn.o_proj.weight")),
85
+ w_gateup=swiglu_interleave(g("mlp.gate_proj.weight"), g("mlp.up_proj.weight")),
86
+ w_down=linear_to_mm(g("mlp.down_proj.weight")),
87
+ ln1=g("input_layernorm.weight"), ln2=g("post_attention_layernorm.weight"))
88
+
89
+
90
+ @dataclass
91
+ class InputWeights:
92
+ """input_embed.proj split by input (matmul layout [in, out])."""
93
+
94
+ wx: torch.Tensor # [64, 2048]
95
+ wcond: torch.Tensor # [64, 2048]
96
+ wtext: torch.Tensor # [512, 2048]
97
+ wstyle: torch.Tensor # [512, 2048]
98
+ wc: torch.Tensor # [512, 2048]
99
+ b: torch.Tensor
100
+
101
+
102
+ def input_weights(ckpt: Checkpoint) -> InputWeights:
103
+ c = ckpt.cfg
104
+ w = linear_to_mm(ckpt.get("input_embed.proj.weight")) # [1664, 2048]
105
+ 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]
106
+ parts = [w[a:b] for a, b in zip(edges, edges[1:] + [w.shape[0]])]
107
+ return InputWeights(*[p.contiguous() for p in parts], ckpt.get("input_embed.proj.bias"))
108
+
109
+
110
+ @dataclass(frozen=True)
111
+ class Geometry:
112
+ frames: int
113
+
114
+ @property
115
+ def seq_pad(self) -> int:
116
+ return round_up(self.frames, SEQ_BUCKET)
117
+
118
+
119
+ def key_mask(geo: Geometry) -> torch.Tensor:
120
+ """Additive [S_pad, S_pad] mask excluding padded keys (upstream attends over exactly S frames)."""
121
+ valid = torch.arange(geo.seq_pad)[None, :] < geo.frames
122
+ return torch.where(valid, 0.0, NEG).expand(geo.seq_pad, -1).contiguous()
123
+
124
+
125
+ def row_mask(geo: Geometry) -> torch.Tensor:
126
+ """[S_pad, 1]: 1 on real frames, 0 on padding (the position convs must see zeros past the song, like upstream)."""
127
+ return (torch.arange(geo.seq_pad) < geo.frames).float()[:, None]
128
+
129
+
130
+ def rope_tables(seq_pad: int, cfg: Config, dtype=torch.float32):
131
+ inv = 1.0 / (cfg.rope_theta ** (torch.arange(0, cfg.head_dim, 2, dtype=torch.float64) / cfg.head_dim))
132
+ ang = torch.outer(torch.arange(seq_pad, dtype=torch.float64), inv).float()
133
+ return torch.cos(ang).repeat_interleave(2, -1).to(dtype), torch.sin(ang).repeat_interleave(2, -1).to(dtype)
134
+
135
+
136
+ def pad_rows(t: torch.Tensor, rows: int) -> torch.Tensor:
137
+ out = torch.zeros(rows, t.shape[-1], dtype=t.dtype)
138
+ out[: t.shape[0]] = t
139
+ return out
140
+
141
+
142
+ @dataclass
143
+ class CallRows:
144
+ """What changes per call: the style/time part of the input projection and the output modulation."""
145
+
146
+ input_row: torch.Tensor # [2048]
147
+ out_scale: torch.Tensor # [2048]
148
+ out_shift: torch.Tensor # [2048]
149
+
150
+
151
+ def call_rows(ckpt_or_iw, norm_linear_w, norm_linear_b, style: torch.Tensor, c: torch.Tensor) -> CallRows:
152
+ """style [512], c [512] (time + start + duration embeddings, from upstream on the host)."""
153
+ iw = ckpt_or_iw
154
+ input_row = style @ iw.wstyle + c @ iw.wc + iw.b
155
+ emb = torch.nn.functional.silu(c) @ norm_linear_w.t() + norm_linear_b
156
+ scale, shift = emb.chunk(2, dim=-1)
157
+ return CallRows(input_row, scale, shift)
158
+
159
+
160
+ class RefDecoder:
161
+ """float32 CPU DiT in the TT formulation."""
162
+
163
+ def __init__(self, ckpt: Checkpoint):
164
+ self.ckpt, self.cfg = ckpt, ckpt.cfg
165
+ c = self.cfg
166
+ self.layers = [layer_weights(ckpt, i) for i in range(c.depth)]
167
+ self.iw = input_weights(ckpt)
168
+ self.conv = [(ckpt.get(f"input_embed.conv_pos_embed.conv1d.{i}.weight"),
169
+ ckpt.get(f"input_embed.conv_pos_embed.conv1d.{i}.bias")) for i in (0, 2)]
170
+ self.fusion = [(linear_to_mm(ckpt.get(f"text_fusion_linears.{i}.0.weight")),
171
+ ckpt.get(f"text_fusion_linears.{i}.0.bias")) for i in range(c.fusion_layers)]
172
+ self.w_out, self.b_out = linear_to_mm(ckpt.get("proj_out.weight")), ckpt.get("proj_out.bias")
173
+ self.norm_w, self.norm_b = ckpt.get("norm_out.linear.weight"), ckpt.get("norm_out.linear.bias")
174
+
175
+ def rows(self, style: torch.Tensor, c: torch.Tensor) -> CallRows:
176
+ return call_rows(self.iw, self.norm_w, self.norm_b, style, c)
177
+
178
+ def branch(self, text_embed: torch.Tensor, cond, geo: Geometry) -> Dict[str, object]:
179
+ """Per song and CFG branch: [cond @ Wcond + text @ Wtext] and the 8 text-fusion residuals, padded."""
180
+ t = pad_rows(text_embed.float(), geo.seq_pad)
181
+ base = t @ self.iw.wtext
182
+ if cond is not None:
183
+ base = base + pad_rows(cond.float(), geo.seq_pad) @ self.iw.wcond
184
+ fusion = [torch.nn.functional.silu(t @ w + b) for w, b in self.fusion]
185
+ return {"base": base, "fusion": fusion}
186
+
187
+ def conv_pos(self, h: torch.Tensor, mask: torch.Tensor = None) -> torch.Tensor:
188
+ """Two k31 convs with Mish. Upstream's sequence ends at the song, so each conv sees zeros past it: the rows
189
+ past the song are zeroed before every conv (the first conv's Mish output is not zero there)."""
190
+ c = self.cfg
191
+ y = h
192
+ for w, b in self.conv:
193
+ if mask is not None:
194
+ y = y * mask
195
+ y = torch.nn.functional.mish(torch.nn.functional.conv1d(y.t()[None], w, b, padding=c.conv_kernel // 2,
196
+ groups=c.conv_groups))[0].t()
197
+ return y
198
+
199
+ def forward(self, x_t: torch.Tensor, branch: Dict[str, object], rows: CallRows, geo: Geometry, taps: list = None):
200
+ """[S, 64] noisy latents -> [S, 64] prediction."""
201
+ c = self.cfg
202
+ mask = row_mask(geo)
203
+ h = (pad_rows(x_t.float(), geo.seq_pad) @ self.iw.wx + branch["base"] + rows.input_row) * mask
204
+ h = self.conv_pos(h, mask) + h
205
+ cos, sin = rope_tables(geo.seq_pad, c)
206
+ kmask = key_mask(geo)
207
+ for i, w in enumerate(self.layers):
208
+ a = rms(h, w.ln1, c.eps)
209
+ q, k, v = (a @ w.wqkv).split(c.dim, -1)
210
+ q = apply_rope_adjacent(q.view(-1, c.heads, c.head_dim).transpose(0, 1), cos, sin)
211
+ k = apply_rope_adjacent(k.view(-1, c.heads, c.head_dim).transpose(0, 1), cos, sin)
212
+ v = v.view(-1, c.heads, c.head_dim).transpose(0, 1)
213
+ scores = q @ k.transpose(-1, -2) * c.head_dim ** -0.5 + kmask
214
+ att = (torch.softmax(scores, -1) @ v).transpose(0, 1).reshape(geo.seq_pad, -1)
215
+ h = h + att @ w.wo
216
+ m = rms(h, w.ln2, c.eps)
217
+ gu = (m @ w.w_gateup).view(geo.seq_pad, -1, 2, TILE)
218
+ h = h + (torch.nn.functional.silu(gu[:, :, 0]) * gu[:, :, 1]).reshape(geo.seq_pad, -1) @ w.w_down
219
+ if i < c.fusion_layers:
220
+ h = h + branch["fusion"][i]
221
+ if taps is not None:
222
+ taps.append(h.clone())
223
+ h = torch.nn.functional.layer_norm(h, (c.dim,), eps=c.eps) * (1 + rows.out_scale) + rows.out_shift
224
+ return (h @ self.w_out + self.b_out)[: geo.frames]
code/tt_diffrhythm/local.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """The card in THIS process, behind the same `.call(msg)` surface `TTWorker` gives.
3
+
4
+ `RemoteDiffRhythmDiT` and `RemoteVae` reach the card through exactly one method -- `worker.call(msg)`
5
+ -- so swapping the transport is enough to run everything in one process: the shims, their window
6
+ arithmetic and their drop-in signatures are reused unchanged rather than reimplemented here.
7
+
8
+ Why there are two transports at all: DiffRhythm resolves `torch 2.10.0+cu128` and `numpy>=2`, while
9
+ ttnn's extension modules are built against tt-metal's own `torch 2.11.0+cpu` and `numpy 1.x`. Two
10
+ environments cannot be one process, so the deployment this port came from ran DiffRhythm on the host
11
+ and the card in a container, joined by a Unix socket (`remote.py` / `worker.py`). A container built
12
+ for this model alone has no such split -- one environment satisfies both -- and then the socket is
13
+ pure cost. Use `LocalWorker` there, `TTWorker` when the two environments must stay apart.
14
+
15
+ from tt_diffrhythm import LocalWorker, RemoteDiffRhythmDiT, RemoteVae
16
+
17
+ worker = LocalWorker(cfm_model_pt, vae=vae_model_pt).start()
18
+ cfm.transformer = RemoteDiffRhythmDiT(worker, cfm.transformer)
19
+ waveform = RemoteVae(worker, upsample=worker.upsample)(latent)
20
+ ...
21
+ worker.close()
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import time
26
+ from pathlib import Path
27
+ from typing import Optional
28
+
29
+ import torch
30
+
31
+ from . import diffrhythm_dit, oobleck_vae, wire
32
+ from .shapes import VAE_WINDOWS
33
+ from .worker import DiffRhythm
34
+
35
+ MODELS = {"diffrhythm": DiffRhythm}
36
+
37
+
38
+ class LocalWorker:
39
+ """`TTWorker`'s surface, answered in-process. `checkpoint` is cfm_model.pt and `vae` is
40
+ vae_model.pt; both are the files DiffRhythm's own Hugging Face cache holds."""
41
+
42
+ def __init__(self, checkpoint, vae, model: str = "diffrhythm", warm: bool = True):
43
+ self.checkpoint = Path(checkpoint)
44
+ self.vae_path = Path(vae)
45
+ self.model_name = model
46
+ self.warm = warm
47
+ self.dev = None
48
+ self.model = self.vae = None
49
+ self.song = None
50
+ self.stats: dict = {}
51
+ self.failed = False
52
+ self.upsample: Optional[int] = None
53
+
54
+ # ------------------------------------------------------------------ lifecycle
55
+ def start(self) -> "LocalWorker":
56
+ started = time.monotonic()
57
+ self.dev = diffrhythm_dit.open_device()
58
+ try:
59
+ self.model = MODELS[self.model_name](self.dev, str(self.checkpoint))
60
+ self.vae = oobleck_vae.OobleckTT(self.dev, str(self.vae_path))
61
+ self.upsample = self.vae.upsample
62
+ if self.warm:
63
+ # Same reason worker.main() warms here: built now, the VAE's conv kernels compile
64
+ # in one device state and every later decode reuses the in-process programs. Built
65
+ # after a song's DiT steps instead, the state -- and so the kernels -- depends on
66
+ # the song length and recompiles for 20-40 s.
67
+ t0 = time.monotonic()
68
+ for window in VAE_WINDOWS:
69
+ self.vae.decode(torch.zeros(64, window))
70
+ self.stats["vae_warm_s"] = round(time.monotonic() - t0, 2)
71
+ self.stats["load_s"] = round(time.monotonic() - started, 2)
72
+ self.stats["dram"] = diffrhythm_dit.dram_stats(self.dev)
73
+ except BaseException:
74
+ self.close()
75
+ raise
76
+ return self
77
+
78
+ def wait_ready(self, timeout_s: float = 0.0) -> dict:
79
+ """`TTWorker.wait_ready`'s shape. Loading already finished in `start`, so this only reports."""
80
+ if self.model is None:
81
+ raise RuntimeError("LocalWorker.start() has not run")
82
+ return {"op": "ready", "model": self.model_name, "upsample": self.upsample, **self.stats}
83
+
84
+ def died(self) -> bool:
85
+ """There is no child process to lose; a failure here raised in the caller's own stack."""
86
+ return False
87
+
88
+ def close(self) -> None:
89
+ if self.song is not None and self.model is not None:
90
+ self.model.dit.release(self.song)
91
+ self.song = None
92
+ self.model = self.vae = None
93
+ if self.dev is not None:
94
+ diffrhythm_dit.close_device(self.dev)
95
+ self.dev = None
96
+
97
+ def __enter__(self):
98
+ return self.start()
99
+
100
+ def __exit__(self, *exc):
101
+ self.close()
102
+
103
+ # ------------------------------------------------------------------ calls
104
+ def call(self, msg: dict) -> dict:
105
+ """`worker.py:serve`'s dispatch with the socket taken out and nothing else changed.
106
+
107
+ Messages still carry `wire`-encoded tensors even though both ends are now one process.
108
+ That is deliberate: it is what lets `RemoteDiffRhythmDiT` and `RemoteVae` run against this
109
+ worker unmodified, and the encoding is a float32 copy of a few MB against card work
110
+ measured in seconds."""
111
+ if self.model is None:
112
+ raise RuntimeError("LocalWorker.start() has not run")
113
+ op = msg.get("op")
114
+ started = time.monotonic()
115
+ try:
116
+ if op == "prepare":
117
+ if self.song is not None:
118
+ self.model.dit.release(self.song)
119
+ self.song = None
120
+ self.song, geometry = self.model.prepare(msg)
121
+ return {"op": "prepared", "seconds": time.monotonic() - started, "geometry": geometry}
122
+ if op == "forward":
123
+ if self.song is None:
124
+ raise RuntimeError("forward before prepare")
125
+ v = self.model.forward(msg, self.song)
126
+ return {"op": "velocity", "v": wire.encode(v), "seconds": time.monotonic() - started}
127
+ if op == "vae":
128
+ if self.song is not None:
129
+ # Sampling is over; freeing the song's tensors first gives the VAE the same
130
+ # device allocation state for every song length, so its compiled conv kernels
131
+ # are reused instead of recompiled.
132
+ self.model.dit.release(self.song)
133
+ self.song = None
134
+ audio = self.vae.decode(wire.decode(msg["z"]))
135
+ return {"op": "audio", "audio": wire.encode(audio), "seconds": time.monotonic() - started}
136
+ if op == "close":
137
+ self.close()
138
+ return {"op": "closed", "seconds": time.monotonic() - started}
139
+ except BaseException:
140
+ self.failed = True # the card may be unhealthy, same contract as TTWorker
141
+ raise
142
+ raise RuntimeError(f"unknown op {op!r}")
code/tt_diffrhythm/oobleck_vae.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """Oobleck audio VAE decoders on ONE Blackhole (P100a), TTNN: ACE-Step's (diffusers AutoencoderOobleck, 48 kHz
3
+ stereo, strides 10, 6, 4, 4, 2) and DiffRhythm's (stable-audio-tools, TorchScript, 44.1 kHz stereo, strides
4
+ 8, 8, 4, 4, 2). Same architecture, different weight files:
5
+
6
+ conv1 (64 -> 2048, k7)
7
+ 5 x block: Snake -> ConvTranspose1d (k = 2s, stride s) -> 3 x residual unit (dilation 1, 3, 9):
8
+ x + conv_k1(Snake(conv_k7_dilated(Snake(x))))
9
+ Snake -> conv2 (128 -> 2, k7, no bias)
10
+ Snake(x) = x + 1 / (exp(beta) + 1e-9) * sin(exp(alpha) * x)^2 per channel. Weight norm is folded on the host
11
+ (diffusers) or already folded (the TorchScript export).
12
+
13
+ Activations are [1, 1, L, C] (channels last), as ttnn.conv1d / conv_transpose2d (height 1) expect.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import math
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+ from typing import List, Optional
22
+
23
+ import torch
24
+ import ttnn
25
+
26
+ MEM = ttnn.DRAM_MEMORY_CONFIG
27
+
28
+
29
+ def fold_weight_norm(g: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
30
+ """torch weight_norm(dim=0): w = g * v / ||v|| over every dim but the first."""
31
+ norm = v.flatten(1).norm(dim=1).view(-1, *([1] * (v.dim() - 1)))
32
+ return g * v / norm
33
+
34
+
35
+ class VaeWeights:
36
+ """decoder.* of checkpoints/vae, float32, weight norm folded."""
37
+
38
+ def __init__(self, vae_dir):
39
+ from safetensors import safe_open
40
+
41
+ self.dir = Path(vae_dir)
42
+ self.config = json.loads((self.dir / "config.json").read_text())
43
+ self._f = safe_open(str(self.dir / "diffusion_pytorch_model.safetensors"), framework="pt")
44
+ self.keys = set(self._f.keys())
45
+
46
+ def get(self, name):
47
+ return self._f.get_tensor(f"decoder.{name}").float()
48
+
49
+ def conv(self, name):
50
+ w = fold_weight_norm(self.get(f"{name}.weight_g"), self.get(f"{name}.weight_v"))
51
+ b = self.get(f"{name}.bias") if f"decoder.{name}.bias" in self.keys else None
52
+ return w, b
53
+
54
+ def snake(self, name):
55
+ alpha = torch.exp(self.get(f"{name}.alpha")).flatten()
56
+ inv_beta = 1.0 / (torch.exp(self.get(f"{name}.beta")).flatten() + 1e-9)
57
+ return alpha, inv_beta
58
+
59
+
60
+ class StableAudioVaeWeights:
61
+ """decoder.* of DiffRhythm's vae_model.pt (a stable-audio-tools OobleckDecoder exported with TorchScript, weight
62
+ norm already folded), under VaeWeights' diffusers names."""
63
+
64
+ def __init__(self, path):
65
+ sd = torch.jit.load(str(path), map_location="cpu").state_dict()
66
+ self._sd = {k[len("decoder."):]: v for k, v in sd.items() if k.startswith("decoder.")}
67
+ strides = [self._sd[f"layers.{i + 1}.layers.1.weight"].shape[-1] // 2 for i in range(5)]
68
+ self.config = {"downsampling_ratios": strides}
69
+
70
+ @staticmethod
71
+ def _key(name):
72
+ parts = name.split(".")
73
+ if name == "conv1":
74
+ return "layers.0"
75
+ if name == "snake1":
76
+ return "layers.6"
77
+ if name == "conv2":
78
+ return "layers.7"
79
+ block = f"layers.{int(parts[1]) + 1}"
80
+ if parts[2] == "snake1":
81
+ return f"{block}.layers.0"
82
+ if parts[2] == "conv_t1":
83
+ return f"{block}.layers.1"
84
+ unit = f"{block}.layers.{int(parts[2][len('res_unit'):]) + 1}"
85
+ return f"{unit}.layers.{ {'snake1': 0, 'conv1': 1, 'snake2': 2, 'conv2': 3}[parts[3]] }"
86
+
87
+ def conv(self, name):
88
+ k = self._key(name)
89
+ return self._sd[f"{k}.weight"].float(), (self._sd[f"{k}.bias"].float() if f"{k}.bias" in self._sd else None)
90
+
91
+ def snake(self, name):
92
+ k = self._key(name)
93
+ alpha = torch.exp(self._sd[f"{k}.alpha"].float()).flatten()
94
+ inv_beta = 1.0 / (torch.exp(self._sd[f"{k}.beta"].float()).flatten() + 1e-9)
95
+ return alpha, inv_beta
96
+
97
+
98
+ def vae_weights(path):
99
+ """A diffusers VAE directory (ACE-Step) or a stable-audio TorchScript file (DiffRhythm)."""
100
+ path = Path(path)
101
+ if path.is_dir() and (path / "config.json").exists():
102
+ return VaeWeights(path)
103
+ return StableAudioVaeWeights(path / "vae_model.pt" if path.is_dir() else path)
104
+
105
+
106
+ @dataclass
107
+ class Conv:
108
+ weight: object # ttnn host tensor, replaced by the prepared device tensor after the first call
109
+ bias: object
110
+ in_ch: int
111
+ out_ch: int
112
+ kernel: int
113
+ pad: int
114
+ dilation: int = 1
115
+ stride: int = 1
116
+ transpose: bool = False
117
+
118
+
119
+ @dataclass
120
+ class Snake:
121
+ alpha: ttnn.Tensor
122
+ inv_beta: ttnn.Tensor
123
+
124
+
125
+ @dataclass
126
+ class ResUnit:
127
+ snake1: Snake
128
+ conv1: Conv
129
+ snake2: Snake
130
+ conv2: Conv
131
+
132
+
133
+ @dataclass
134
+ class Block:
135
+ snake: Snake
136
+ up: Conv
137
+ units: List[ResUnit]
138
+
139
+
140
+ @dataclass
141
+ class Precision:
142
+ fidelity: ttnn.MathFidelity = ttnn.MathFidelity.HiFi4 # HiFi2/LoFi are no faster here: the convs are not math-bound
143
+ act_dtype: ttnn.DataType = ttnn.bfloat16
144
+ # vae_conv_sweep.py: activation blocks of 64 rows make the 48 kHz k7 convs 4.9x and the output conv 6x faster
145
+ # than the default block height, same output; double buffering and bfp8 weights were slower. Applied to the
146
+ # layers with <= 256 channels (the long, late ones); the wide early layers keep the default (the override fails
147
+ # or is slower there).
148
+ act_block_h: int = 64
149
+ transpose_act_block_h: int = 64
150
+ act_block_h_max_channels: int = 256
151
+ config_tensors_in_dram: bool = True
152
+ fused_snake: bool = True
153
+
154
+
155
+ class OobleckTT:
156
+ def __init__(self, dev, vae_path, prec: Optional[Precision] = None):
157
+ self.dev = dev
158
+ self.prec = prec or Precision()
159
+ w = vae_weights(vae_path)
160
+ self.upsample = math.prod(w.config["downsampling_ratios"])
161
+ self.conv1 = self._conv(*w.conv("conv1"), pad=3)
162
+ self.blocks = []
163
+ for i in range(len(w.config["downsampling_ratios"])):
164
+ p = f"block.{i}"
165
+ wt, bt = w.conv(f"{p}.conv_t1") # ConvTranspose1d weight [in, out, 2s]
166
+ stride = wt.shape[-1] // 2
167
+ units = []
168
+ for j, dilation in enumerate((1, 3, 9)):
169
+ u = f"{p}.res_unit{j + 1}"
170
+ units.append(ResUnit(self._snake(*w.snake(f"{u}.snake1")),
171
+ self._conv(*w.conv(f"{u}.conv1"), pad=3 * dilation, dilation=dilation),
172
+ self._snake(*w.snake(f"{u}.snake2")), self._conv(*w.conv(f"{u}.conv2"), pad=0)))
173
+ self.blocks.append(Block(self._snake(*w.snake(f"{p}.snake1")),
174
+ self._conv(wt, bt, pad=math.ceil(stride / 2), stride=stride, transpose=True), units))
175
+ self.snake_out = self._snake(*w.snake("snake1"))
176
+ self.conv2 = self._conv(*w.conv("conv2"), pad=3)
177
+ self.ck = ttnn.init_device_compute_kernel_config(dev.arch(), math_fidelity=self.prec.fidelity,
178
+ math_approx_mode=False, fp32_dest_acc_en=True,
179
+ packer_l1_acc=False)
180
+ self.profile = None # a list to record (where, op, length, channels, seconds) per op, synchronizing each
181
+ self._where = ""
182
+
183
+ def _timed(self, op, length, channels, fn, *args):
184
+ if self.profile is None:
185
+ return fn(*args)
186
+ import time
187
+
188
+ ttnn.synchronize_device(self.dev)
189
+ started = time.perf_counter()
190
+ out = fn(*args)
191
+ ttnn.synchronize_device(self.dev)
192
+ self.profile.append((self._where, op, length, channels, time.perf_counter() - started))
193
+ return out
194
+
195
+ # ------------------------------------------------------------------ weights
196
+ def _conv(self, w, b, pad, dilation=1, stride=1, transpose=False):
197
+ if transpose: # [in, out, k] -> [in, out, 1, k]
198
+ in_ch, out_ch, k = w.shape
199
+ else: # [out, in, k] -> [out, in, 1, k]
200
+ out_ch, in_ch, k = w.shape
201
+ weight = ttnn.from_torch(w.unsqueeze(2).contiguous(), dtype=ttnn.float32)
202
+ bias = ttnn.from_torch(b.reshape(1, 1, 1, -1), dtype=ttnn.float32) if b is not None else None
203
+ return Conv(weight, bias, in_ch, out_ch, k, pad, dilation, stride, transpose)
204
+
205
+ def _snake(self, alpha, inv_beta):
206
+ row = lambda v: ttnn.from_torch(v.float().reshape(1, 1, 1, -1), dtype=self.prec.act_dtype,
207
+ layout=ttnn.TILE_LAYOUT, device=self.dev, memory_config=MEM)
208
+ return Snake(row(alpha), row(inv_beta))
209
+
210
+ # ------------------------------------------------------------------ ops
211
+ def _run_conv(self, x, c: Conv, length: int):
212
+ block_h = self.prec.transpose_act_block_h if c.transpose else self.prec.act_block_h
213
+ if max(c.in_ch, c.out_ch) > self.prec.act_block_h_max_channels:
214
+ block_h = 0
215
+ # smaller activation blocks need larger per-core config tensors; keep them in DRAM, not the 96 KiB L1_SMALL
216
+ cfg = ttnn.Conv2dConfig(weights_dtype=ttnn.bfloat16, config_tensors_in_dram=self.prec.config_tensors_in_dram)
217
+ if block_h:
218
+ cfg.act_block_h_override = block_h
219
+ common = dict(device=self.dev, in_channels=c.in_ch, out_channels=c.out_ch, batch_size=1, bias_tensor=c.bias,
220
+ dtype=self.prec.act_dtype, compute_config=self.ck, conv_config=cfg,
221
+ return_weights_and_bias=True)
222
+ if c.transpose:
223
+ out, out_len, (c.weight, c.bias) = ttnn.conv_transpose2d(
224
+ input_tensor=x, weight_tensor=c.weight, input_height=1, input_width=length, kernel_size=(1, c.kernel),
225
+ stride=(1, c.stride), padding=(0, c.pad), output_padding=(0, 0), dilation=(1, 1), groups=1,
226
+ return_output_dim=True, **common)
227
+ out_len = out_len[1] if isinstance(out_len, (tuple, list)) else out_len
228
+ else:
229
+ out, out_len, (c.weight, c.bias) = ttnn.conv1d(
230
+ input_tensor=x, weight_tensor=c.weight, input_length=length, kernel_size=c.kernel, stride=1,
231
+ padding=c.pad, dilation=c.dilation, groups=1, return_output_dim=True, **common)
232
+ if out.layout != ttnn.TILE_LAYOUT:
233
+ out = ttnn.to_layout(out, ttnn.TILE_LAYOUT)
234
+ return ttnn.reshape(out, [1, 1, out_len, c.out_ch]), out_len
235
+
236
+ def _snake_op(self, x, s: Snake):
237
+ if self.prec.fused_snake: # sin(alpha x)^2 in one multiply, then x + inv_beta * that in one addcmul: 2 passes
238
+ unary = lambda op: ttnn.UnaryWithParam(op)
239
+ sq = ttnn.multiply(x, s.alpha, activations=[unary(ttnn.UnaryOpType.SIN), unary(ttnn.UnaryOpType.SQUARE)],
240
+ memory_config=MEM)
241
+ out = ttnn.addcmul(x, sq, s.inv_beta, value=1.0, memory_config=MEM)
242
+ ttnn.deallocate(sq)
243
+ return out
244
+ ax = ttnn.multiply(x, s.alpha, memory_config=MEM)
245
+ sn = ttnn.sin(ax, memory_config=MEM)
246
+ ttnn.deallocate(ax)
247
+ sq = ttnn.multiply(sn, sn, memory_config=MEM)
248
+ ttnn.deallocate(sn)
249
+ t = ttnn.multiply(sq, s.inv_beta, memory_config=MEM)
250
+ ttnn.deallocate(sq)
251
+ out = ttnn.add(x, t, memory_config=MEM)
252
+ ttnn.deallocate(t)
253
+ return out
254
+
255
+ def _unit(self, x, u: ResUnit, length):
256
+ c = u.conv1.out_ch
257
+ h = self._timed("snake", length, c, self._snake_op, x, u.snake1)
258
+ y, _ = self._timed(f"conv_k7_d{u.conv1.dilation}", length, c, self._run_conv, h, u.conv1, length)
259
+ ttnn.deallocate(h)
260
+ h = self._timed("snake", length, c, self._snake_op, y, u.snake2)
261
+ ttnn.deallocate(y)
262
+ y, _ = self._timed("conv_k1", length, c, self._run_conv, h, u.conv2, length)
263
+ ttnn.deallocate(h)
264
+ out = self._timed("add", length, c, lambda a, b: ttnn.add(a, b, memory_config=MEM), x, y)
265
+ ttnn.deallocate(y)
266
+ ttnn.deallocate(x)
267
+ return out
268
+
269
+ # ------------------------------------------------------------------ public
270
+ def decode(self, latents: torch.Tensor, taps: Optional[list] = None) -> torch.Tensor:
271
+ """[64, T] latents -> [2, T * upsample] float32 audio (1920 for ACE-Step, 2048 for DiffRhythm)."""
272
+ length = latents.shape[-1]
273
+ x = ttnn.from_torch(latents.t().contiguous().reshape(1, 1, length, -1).float(), dtype=self.prec.act_dtype,
274
+ layout=ttnn.TILE_LAYOUT, device=self.dev, memory_config=MEM)
275
+ tap = (lambda t, n: taps.append(ttnn.to_torch(t)[0, 0, :n].float())) if taps is not None else (lambda t, n: None)
276
+ self._where = "in"
277
+ h, length = self._timed("conv_in", length, self.conv1.out_ch, self._run_conv, x, self.conv1, length)
278
+ ttnn.deallocate(x)
279
+ tap(h, length)
280
+ for i, b in enumerate(self.blocks):
281
+ self._where = f"block{i}"
282
+ s = self._timed("snake", length, b.up.in_ch, self._snake_op, h, b.snake)
283
+ ttnn.deallocate(h)
284
+ h, length = self._timed(f"conv_t_s{b.up.stride}", length, b.up.out_ch, self._run_conv, s, b.up, length)
285
+ ttnn.deallocate(s)
286
+ for u in b.units:
287
+ h = self._unit(h, u, length)
288
+ tap(h, length)
289
+ self._where = "out"
290
+ s = self._timed("snake", length, self.snake_out.alpha.shape[-1], self._snake_op, h, self.snake_out)
291
+ ttnn.deallocate(h)
292
+ out, length = self._timed("conv_out", length, 2, self._run_conv, s, self.conv2, length)
293
+ ttnn.deallocate(s)
294
+ audio = ttnn.to_torch(out)[0, 0, :length, :2].float().t().contiguous()
295
+ ttnn.deallocate(out)
296
+ return audio
code/tt_diffrhythm/remote.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Host side of the P100a models: start the TT worker container on the card and stand in for the DiT and
2
+ the audio VAE decoder of the host pipeline.
3
+
4
+ The ACE-Step port is the sibling repo, tt-acestep: it shares this worker and transport.
5
+
6
+ Used from the DiffRhythm host pipeline: TTWorker(cfm_model.pt, model="diffrhythm", vae=vae_model.pt),
7
+ cfm.transformer = RemoteDiffRhythmDiT(worker, cfm.transformer), decoding with RemoteVae(worker, upsample=2048).
8
+
9
+ Runs in the model's own environment; needs only the standard library and torch.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ from multiprocessing.connection import Client
14
+ import os
15
+ from pathlib import Path
16
+ import secrets
17
+ import shutil
18
+ import subprocess
19
+ import tempfile
20
+ import time
21
+
22
+ import torch
23
+
24
+ from . import wire
25
+ from .shapes import VAE_WINDOWS
26
+
27
+ ROOT = Path(__file__).resolve().parents[1]
28
+ # The tt-metal image the worker runs in. There is no portable default: set MUSIC_TT_IMAGE,
29
+ # or pass image= , to the tt-metal / ttnn image you built the port against.
30
+ DEFAULT_IMAGE = ""
31
+
32
+
33
+ class WorkerError(RuntimeError):
34
+ """The P100a worker failed (died, hung or reported an error)."""
35
+
36
+
37
+ class TTWorker:
38
+ """checkpoint is cfm_model.pt and vae is vae_model.pt (Hugging Face cache links are resolved)."""
39
+
40
+ def __init__(self, checkpoint: Path, image: str = None, log=None,
41
+ reply_timeout_s: float = 120.0, # the slowest call (an 8-minute song's DiT step or VAE window) is < 1 s
42
+ model: str = "diffrhythm", vae: Path = None):
43
+ self.checkpoint = Path(checkpoint)
44
+ self.model = model
45
+ self.vae = Path(vae) if vae is not None else None
46
+ self.image = image or os.environ.get("MUSIC_TT_IMAGE") or DEFAULT_IMAGE
47
+ if not self.image:
48
+ raise ValueError("set MUSIC_TT_IMAGE (or pass image=) to the tt-metal image the worker runs in")
49
+ self.log = log
50
+ self.reply_timeout_s = reply_timeout_s
51
+ self.proc = self.conn = self.ipc = None
52
+ self.name = f"music-tt-{os.getpid()}-{secrets.token_hex(3)}"
53
+ self.stats = {}
54
+ self.failed = False # set when the worker died, hung or reported an error: the card may be unhealthy
55
+
56
+ # ------------------------------------------------------------------ lifecycle
57
+ def start(self) -> "TTWorker":
58
+ try:
59
+ cache = ROOT / "data/tt-cache"
60
+ cache.mkdir(parents=True, exist_ok=True)
61
+ run_dir = ROOT / "data/tt-run"
62
+ run_dir.mkdir(parents=True, exist_ok=True)
63
+ self.ipc = Path(tempfile.mkdtemp(prefix="dit-", dir=run_dir))
64
+ self.authkey = secrets.token_hex(16)
65
+ mounts = [("/dev/hugepages-1G", "/dev/hugepages-1G", False), (ROOT, "/work", True),
66
+ *self._model_mounts(), (cache, "/cache", False), (self.ipc, "/ipc", False)]
67
+ cmd = ["docker", "run", "--rm", "--name", self.name, "--ipc", "host", "--device", "/dev/tenstorrent"]
68
+ for src, dst, ro in mounts:
69
+ cmd += ["--mount", f"type=bind,src={src},dst={dst}" + (",readonly" if ro else "")]
70
+ for k, v in {"TT_METAL_VISIBLE_DEVICES": "0", "MESH_DEVICE": "P100", "PYTHONUNBUFFERED": "1",
71
+ "TT_METAL_OPERATION_TIMEOUT_SECONDS": "90", "MUSIC_TT_AUTHKEY": self.authkey}.items():
72
+ cmd += ["-e", f"{k}={v}"]
73
+ cmd += [self.image, "python", "/work/tt_diffrhythm/worker.py", "--socket", "/ipc/worker.sock", "--model", self.model]
74
+ # own session: a supervisor may stop the host process by signalling its process group, and a signal reaching
75
+ # `docker run` directly would stop the container while this process still has to wait for it
76
+ self.proc = subprocess.Popen(cmd, stdout=self.log or subprocess.DEVNULL, stderr=subprocess.STDOUT,
77
+ start_new_session=True)
78
+ self._started = time.monotonic()
79
+ except BaseException:
80
+ self.close()
81
+ raise
82
+ return self
83
+
84
+ def _model_mounts(self):
85
+ if self.model == "diffrhythm": # files, mounted where worker.py expects them
86
+ return [(self.checkpoint.resolve(), "/checkpoint/cfm_model.pt", True),
87
+ (self.vae.resolve(), "/vae/vae_model.pt", True)]
88
+ raise ValueError(f"unknown model {self.model!r}")
89
+
90
+ def died(self) -> bool:
91
+ """The worker container exited on its own (not through close())."""
92
+ return self.proc is not None and self.proc.poll() is not None
93
+
94
+ def _fail(self, message):
95
+ self.failed = True
96
+ raise WorkerError(message)
97
+
98
+ def wait_ready(self, timeout_s: float = 300.0):
99
+ sock = self.ipc / "worker.sock"
100
+ deadline = time.monotonic() + timeout_s
101
+ while not sock.exists():
102
+ self._check_alive()
103
+ if time.monotonic() > deadline:
104
+ self._fail("TT worker did not open its socket")
105
+ time.sleep(0.2)
106
+ try:
107
+ self.conn = Client(str(sock), family="AF_UNIX", authkey=self.authkey.encode())
108
+ except (OSError, EOFError) as exc: # the worker died while loading
109
+ self._fail(f"TT worker connection failed: {exc}")
110
+ try:
111
+ ready = self._recv(timeout_s)
112
+ except (OSError, EOFError) as exc:
113
+ self._fail(f"TT worker connection lost: {exc}")
114
+ self.stats.update(worker_load_s=round(ready["load_s"], 2), start_to_ready_s=round(time.monotonic() - self._started, 2))
115
+ return ready
116
+
117
+ def close(self):
118
+ """Stop the worker and wait for the container to exit; only then give the card back."""
119
+ try:
120
+ if self.conn is not None:
121
+ try:
122
+ self.conn.send({"op": "close"})
123
+ except OSError:
124
+ pass
125
+ self.conn.close()
126
+ if self.proc is not None:
127
+ try:
128
+ self.proc.wait(60)
129
+ except subprocess.TimeoutExpired:
130
+ subprocess.run(["docker", "stop", "-t", "20", self.name], capture_output=True, timeout=60)
131
+ self.proc.wait(60)
132
+ finally:
133
+ self.conn = None
134
+ if self.ipc is not None:
135
+ shutil.rmtree(self.ipc, ignore_errors=True)
136
+
137
+ def __enter__(self):
138
+ return self.start()
139
+
140
+ def __exit__(self, *exc):
141
+ self.close()
142
+
143
+ # ------------------------------------------------------------------ calls
144
+ def _check_alive(self):
145
+ if self.proc is not None and self.proc.poll() is not None:
146
+ self._fail(f"TT worker exited ({self.proc.returncode}); see the worker log")
147
+
148
+ def _recv(self, timeout_s):
149
+ deadline = time.monotonic() + timeout_s
150
+ while not self.conn.poll(1.0):
151
+ self._check_alive()
152
+ if time.monotonic() > deadline:
153
+ self._fail("TT worker did not answer in time")
154
+ msg = self.conn.recv()
155
+ if msg.get("op") == "error":
156
+ self._fail(msg["error"])
157
+ return msg
158
+
159
+ def call(self, msg: dict) -> dict:
160
+ try:
161
+ self.conn.send(msg)
162
+ return self._recv(self.reply_timeout_s)
163
+ except (OSError, EOFError) as exc: # the socket broke: the worker is gone
164
+ self._fail(f"TT worker connection lost: {exc}")
165
+
166
+
167
+ class RemoteDiffRhythmDiT(torch.nn.Module):
168
+ """Drop-in for DiffRhythm's DiT (cfm.transformer) in CFM.sample (batch 1): same call, same output.
169
+
170
+ Keeps upstream's small modules on the host, exactly as upstream runs them: the text embedding (computed once per
171
+ song and CFG branch; upstream recomputes the same thing every call) and the time / start / duration embeddings.
172
+ Everything weight-heavy (input projection, position convs, 16 Llama layers, output) runs on the card."""
173
+
174
+ def __init__(self, worker: TTWorker, dit):
175
+ super().__init__()
176
+ self.worker = worker
177
+ self.text_embed, self.time_embed = dit.text_embed, dit.time_embed
178
+ self.start_time_embed, self.duration_time_embed = dit.start_time_embed, dit.duration_time_embed
179
+ self._song = None
180
+ self.seconds = []
181
+
182
+ def reset(self) -> None:
183
+ """Forget which song is prepared on the card.
184
+
185
+ The worker releases the song's tensors when the VAE runs, so that every song length meets
186
+ the same device allocation state and the VAE's compiled kernels are reused. After that the
187
+ card holds no song, and this cache -- which exists to skip re-sending an unchanged text and
188
+ conditioning -- would otherwise send a `forward` for a song that is no longer there.
189
+
190
+ A caller that builds a shim per song never notices. One that keeps it across songs, as a
191
+ server does, has to call this between them.
192
+ """
193
+ self._song = None
194
+
195
+ def forward(self, x, cond, text, time, drop_audio_cond, drop_text, drop_prompt=False, style_prompt=None,
196
+ start_time=None, duration=None):
197
+ if x.shape[0] != 1:
198
+ raise ValueError("The P100a DiT runs batch 1")
199
+ if bool(drop_audio_cond) != bool(drop_text):
200
+ raise ValueError("The P100a DiT runs CFM.sample's two branches (both conditions kept or both dropped)")
201
+ seq_len = x.shape[1]
202
+ if self._song is None or not (torch.equal(text, self._song[0]) and torch.equal(cond, self._song[1])):
203
+ embeds = {null: wire.encode(self.text_embed(text, seq_len, drop_text=null)[0]) for null in (False, True)}
204
+ self.worker.call({"op": "prepare", "text_embed": embeds, "cond": wire.encode(cond[0])})
205
+ self._song = (text.clone(), cond.clone())
206
+ if time.ndim == 0:
207
+ time = time.repeat(1)
208
+ dtype = next(self.time_embed.parameters()).dtype # upstream hands over fp16 style/start/duration tensors
209
+ c = (self.time_embed(time.to(dtype)) + self.start_time_embed(start_time.to(dtype))
210
+ + self.duration_time_embed(duration.to(dtype)))
211
+ style = torch.zeros_like(style_prompt) if drop_prompt else style_prompt
212
+ reply = self.worker.call({"op": "forward", "x": wire.encode(x[0]), "drop_text": bool(drop_text),
213
+ "style": wire.encode(style[0]), "c": wire.encode(c[0])})
214
+ self.seconds.append(reply["seconds"])
215
+ return wire.decode(reply["v"]).to(x.dtype)[None]
216
+
217
+
218
+ class RemoteVae:
219
+ """Stands in for handler.tiled_decode: overlap-discard tiling like upstream's, but every window of a song has the
220
+ same length (the last one is moved back to end at the song's end), so the P100a compiles only a few shapes.
221
+
222
+ The window is the largest of `chunks` that fits the song: 1024 frames (41 s) decode at 0.61 ms per frame with
223
+ 12.5 % overlap, 512 frames at the same rate with 25 % overlap (vae_profile.py); longer windows get slower per
224
+ frame. Songs of 30-41 s use 512, so no song compiles a shape of its own.
225
+
226
+ latents [B, 64, T] -> audio [B, 2, T * upsample] float32 on the CPU (1920 for ACE-Step, 2048 for DiffRhythm)."""
227
+
228
+ def __init__(self, worker: TTWorker, chunks=VAE_WINDOWS, overlap: int = 64, upsample: int = 1920):
229
+ self.worker, self.chunks, self.overlap, self.upsample = worker, tuple(sorted(chunks, reverse=True)), overlap, upsample
230
+ self.seconds = []
231
+
232
+ def chunk_for(self, frames: int) -> int:
233
+ return next((c for c in self.chunks if c <= frames), self.chunks[-1])
234
+
235
+ @staticmethod
236
+ def windows(frames: int, chunk: int, overlap: int):
237
+ """(window_start, core_start, core_end) covering [0, frames); windows are exactly `chunk` frames long
238
+ (or the whole song when it is shorter) and keep >= overlap frames of context around each core."""
239
+ if frames <= chunk:
240
+ return [(0, 0, frames)]
241
+ stride = chunk - 2 * overlap
242
+ out = []
243
+ for core_start in range(0, frames, stride):
244
+ core_end = min(core_start + stride, frames)
245
+ out.append((min(max(core_start - overlap, 0), frames - chunk), core_start, core_end))
246
+ return out
247
+
248
+ def _decode(self, z: torch.Tensor) -> torch.Tensor:
249
+ reply = self.worker.call({"op": "vae", "z": wire.encode(z)})
250
+ self.seconds.append(reply["seconds"])
251
+ return wire.decode(reply["audio"])
252
+
253
+ def __call__(self, latents, chunk_size=None, overlap=None, offload_wav_to_cpu=None):
254
+ out = []
255
+ for z in latents.detach().float().cpu():
256
+ frames = z.shape[-1]
257
+ chunk = self.chunk_for(frames)
258
+ pieces = []
259
+ for win_start, core_start, core_end in self.windows(frames, chunk, self.overlap):
260
+ win_end = min(win_start + chunk, frames)
261
+ audio = self._decode(z[:, win_start:win_end].contiguous())
262
+ pieces.append(audio[:, (core_start - win_start) * self.upsample:(core_end - win_start) * self.upsample])
263
+ out.append(torch.cat(pieces, dim=-1))
264
+ return torch.stack(out)
code/tt_diffrhythm/server.py ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """HTTP server for DiffRhythm 1.2 Full on one Blackhole p100a, everything in one process.
3
+
4
+ uvicorn tt_diffrhythm.server:app --host 0.0.0.0 --port 20000
5
+
6
+ DiffRhythm's own sampling loop runs here on the CPU and the CFM transformer and the stable-audio
7
+ VAE decoder run on the card, joined by `LocalWorker` instead of the Unix socket `remote.py` uses.
8
+ That split existed because DiffRhythm and tt-metal could not share an interpreter in the
9
+ deployment this port came from; an image built for this model alone resolves one environment that
10
+ satisfies both, so the socket is unnecessary there. See `local.py`.
11
+
12
+ Requires DiffRhythm itself (its `model`, `infer` and `g2p` trees) importable beside this one -- it
13
+ is the model, and this package only moves two of its stages onto the card.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import base64
18
+ import io
19
+ import os
20
+ import random
21
+ import re
22
+ import sys
23
+ import time
24
+ from contextlib import asynccontextmanager
25
+ from pathlib import Path
26
+ from threading import Lock
27
+ from typing import Optional
28
+
29
+ import torch
30
+ from fastapi import FastAPI, HTTPException
31
+ from pydantic import BaseModel, ConfigDict, Field
32
+
33
+ from .local import LocalWorker
34
+ from .remote import RemoteDiffRhythmDiT, RemoteVae
35
+
36
+ LICENSE = "apache-2.0"
37
+ WEIGHTS_REPO = "ASLP-lab/DiffRhythm-1_2-full"
38
+ VAE_REPO = "ASLP-lab/DiffRhythm-vae"
39
+ STYLE_REPO = "OpenMuQ/MuQ-MuLan-large"
40
+ STEPS = 32 # the schedule this port's shapes are compiled for
41
+ CFG_STRENGTH = 4.0
42
+ MAX_FRAMES = 6144 # the "full" checkpoint: 285 seconds at 21.5 frames per second
43
+ MIN_DURATION, MAX_DURATION = 96, 285
44
+ SAMPLE_RATE = 44100
45
+ UPSAMPLE = 2048 # VAE frames -> audio samples
46
+ TURN_WAIT_S = float(os.environ.get("MUSIC_TURN_WAIT_S", "1800"))
47
+
48
+ # DiffRhythm reads `./config/diffrhythm-1b.json` and `./g2p/g2p/vocab.json` relative to the working
49
+ # directory, so the server runs from the checkout rather than rewriting upstream's path handling.
50
+ DIFFRHYTHM_ROOT = os.environ.get("DIFFRHYTHM_ROOT", "")
51
+
52
+ TIMESTAMP = re.compile(r"^\[(\d{1,2}):(\d{2}(?:\.\d{1,3})?)\]\s*(.*)$")
53
+ SECTION = re.compile(r"^\[[A-Za-z][^\]]*\]$")
54
+
55
+ STATE: dict = {"status": "loading", "error": None, "generating": False, "load_s": None, "dram": None}
56
+ WORKER: Optional[LocalWorker] = None
57
+ CFM = TOKENIZER = STYLE = None
58
+ LOCK = Lock()
59
+
60
+
61
+ def _root() -> Path:
62
+ """The DiffRhythm checkout: DIFFRHYTHM_ROOT, or the one shipped beside this package."""
63
+ if DIFFRHYTHM_ROOT:
64
+ return Path(DIFFRHYTHM_ROOT).resolve()
65
+ here = Path(__file__).resolve().parent.parent
66
+ if (here / "infer" / "infer_utils.py").is_file():
67
+ return here
68
+ raise RuntimeError("set DIFFRHYTHM_ROOT to the DiffRhythm checkout (the directory holding infer/)")
69
+
70
+
71
+ def _point_phonemizer_at_a_bundled_espeak() -> None:
72
+ """Let phonemizer find a libespeak-ng that came from a wheel instead of a system package.
73
+
74
+ `g2p.utils.g2p` builds its `EspeakBackend`s at import, so espeak has to be resolvable before
75
+ upstream is imported at all. Everywhere but a container that means `apt install espeak-ng`;
76
+ the `espeakng-loader` wheel carries the library and its data instead. Both have to be set --
77
+ the library alone leaves espeak looking for `phontab` under the path it was built at, which
78
+ does not exist here.
79
+
80
+ Does nothing when the wheel is absent, or when PHONEMIZER_ESPEAK_LIBRARY already names a
81
+ library: a system espeak-ng is the better answer where there is one, and an explicit choice
82
+ is not ours to override. (Asking `EspeakWrapper` whether one is set is not the way to decide
83
+ -- `library_path` is a property on the class, so reading it off the class hands back the
84
+ property object, which is never None.)
85
+ """
86
+ if os.environ.get("PHONEMIZER_ESPEAK_LIBRARY"):
87
+ return
88
+ try:
89
+ import espeakng_loader
90
+ from phonemizer.backend.espeak.wrapper import EspeakWrapper
91
+ except ImportError:
92
+ return
93
+ EspeakWrapper.set_library(str(espeakng_loader.get_library_path()))
94
+ if hasattr(EspeakWrapper, "set_data_path"):
95
+ EspeakWrapper.set_data_path(str(espeakng_loader.get_data_path()))
96
+ else: # older phonemizer: espeak reads the data path from its own environment variable
97
+ os.environ.setdefault("ESPEAK_DATA_PATH", str(espeakng_loader.get_data_path()))
98
+
99
+
100
+ def _enter_checkout() -> Path:
101
+ """Make upstream importable and its relative paths resolve, the way its own scripts do."""
102
+ root = _root()
103
+ _point_phonemizer_at_a_bundled_espeak()
104
+ for path in (root, root / "infer"):
105
+ if str(path) not in sys.path:
106
+ sys.path.insert(0, str(path))
107
+ os.chdir(root)
108
+ return root
109
+
110
+
111
+ def _load_cfm(root: Path, device: str = "cpu"):
112
+ """`infer_utils.prepare_model` without the VAE (the card decodes) and without its
113
+ CWD-relative download cache, so the weights land in the ordinary HF cache."""
114
+ import json
115
+
116
+ from huggingface_hub import hf_hub_download
117
+ from infer_utils import CNENTokenizer, load_checkpoint
118
+ from model import CFM as CFMModel
119
+ from model import DiT
120
+
121
+ with open(root / "config" / "diffrhythm-1b.json") as f:
122
+ config = json.load(f)
123
+ cfm = CFMModel(transformer=DiT(**config["model"], max_frames=MAX_FRAMES),
124
+ num_channels=config["model"]["mel_dim"], max_frames=MAX_FRAMES).to(device)
125
+ cfm = load_checkpoint(cfm, hf_hub_download(WEIGHTS_REPO, "cfm_model.pt"), device=device, use_ema=False)
126
+ return cfm, CNENTokenizer()
127
+
128
+
129
+ def _style_model(device: str = "cpu"):
130
+ from muq import MuQMuLan
131
+
132
+ return MuQMuLan.from_pretrained(STYLE_REPO).to(device).eval().float()
133
+
134
+
135
+ @asynccontextmanager
136
+ async def lifespan(_app: FastAPI):
137
+ global WORKER, CFM, TOKENIZER, STYLE
138
+ started = time.monotonic()
139
+ try:
140
+ import gc
141
+
142
+ from huggingface_hub import hf_hub_download
143
+
144
+ os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
145
+ root = _enter_checkout()
146
+ WORKER = LocalWorker(hf_hub_download(WEIGHTS_REPO, "cfm_model.pt"),
147
+ vae=hf_hub_download(VAE_REPO, "vae_model.pt")).start()
148
+ cfm, TOKENIZER = _load_cfm(root)
149
+ # Replacing the transformer drops the host copy of the big weights; the card holds them.
150
+ cfm.transformer = RemoteDiffRhythmDiT(WORKER, cfm.transformer)
151
+ CFM = cfm.float().eval()
152
+ STYLE = _style_model()
153
+ gc.collect()
154
+ STATE.update(status="ok", load_s=round(time.monotonic() - started, 2),
155
+ dram=WORKER.stats.get("dram"), vae_warm_s=WORKER.stats.get("vae_warm_s"))
156
+ except Exception as exc:
157
+ STATE.update(status="error", error=f"{type(exc).__name__}: {exc}")
158
+ try:
159
+ yield
160
+ finally:
161
+ CFM = TOKENIZER = STYLE = None
162
+ if WORKER is not None:
163
+ WORKER.close()
164
+ WORKER = None
165
+
166
+
167
+ app = FastAPI(title="DiffRhythm 1.2 Full on p100a", lifespan=lifespan, docs_url=None, redoc_url=None)
168
+
169
+
170
+ class Request(BaseModel):
171
+ model_config = ConfigDict(extra="ignore")
172
+ prompt: str = Field(min_length=1, max_length=4000, description="the style description")
173
+ lyrics: str = Field(default="", max_length=20000, description="LRC, or plain lines to be timed")
174
+ instrumental: bool = False
175
+ duration: int = 180
176
+ seed: Optional[int] = Field(default=None, ge=0, le=4294967295)
177
+ inference_steps: Optional[int] = None
178
+
179
+
180
+ def _stamp(seconds: float) -> str:
181
+ centiseconds = round(seconds * 100)
182
+ minutes, remainder = divmod(centiseconds, 6000)
183
+ return f"[{minutes:02d}:{remainder / 100:05.2f}]"
184
+
185
+
186
+ def _lrc(lyrics: str, duration: int, instrumental: bool) -> str:
187
+ """Real LRC as given, or approximate starts derived for untimed lines.
188
+
189
+ The model needs a timestamp on every line. Deriving them by word count is a convenience,
190
+ not learned lyric alignment -- pass LRC when the timing matters."""
191
+ if instrumental:
192
+ return ""
193
+ lines = [line.strip() for line in lyrics.splitlines() if line.strip()]
194
+ lines = [line for line in lines if not SECTION.fullmatch(line)]
195
+ if not lines:
196
+ raise ValueError("vocals need lyrics; set instrumental=true for music without them")
197
+ if len(lines) > 400:
198
+ raise ValueError("at most 400 lyric lines")
199
+ matches = [TIMESTAMP.match(line) for line in lines]
200
+ if any(matches):
201
+ if not all(matches):
202
+ raise ValueError("use either plain lyrics or a timestamp on every line (LRC)")
203
+ out, previous = [], -1.0
204
+ for match in matches:
205
+ minutes, seconds, line = match.groups()
206
+ start = int(minutes) * 60 + float(seconds)
207
+ if float(seconds) >= 60 or not previous <= start < duration:
208
+ raise ValueError("LRC timestamps must be ordered and shorter than the song")
209
+ previous = start
210
+ if line.strip(): # an empty timed line marks a break; the tokenizer cannot encode it
211
+ out.append(f"{_stamp(start)}{line.strip()}")
212
+ if not out:
213
+ raise ValueError("the LRC contains no singable lyrics")
214
+ return "\n".join(out)
215
+ if any(re.match(r"^\[\d", line) for line in lines):
216
+ raise ValueError("invalid LRC timestamp; use [mm:ss.xx] before each line")
217
+ intro, outro = min(12.0, duration * 0.06), min(10.0, duration * 0.05)
218
+ weights = [max(3, len(line.split())) for line in lines]
219
+ total, elapsed, out = sum(weights), 0, []
220
+ for line, weight in zip(lines, weights):
221
+ out.append(f"{_stamp(intro + (duration - intro - outro) * elapsed / total)}{line}")
222
+ elapsed += weight
223
+ return "\n".join(out)
224
+
225
+
226
+ def _wav(waveform: torch.Tensor) -> bytes:
227
+ """[channels, samples] float -> peak-normalized 16-bit 44.1 kHz WAV."""
228
+ import soundfile as sf
229
+
230
+ waveform = waveform.float().cpu()
231
+ if not torch.isfinite(waveform).all():
232
+ raise RuntimeError("non-finite audio; retry with a different seed")
233
+ peak = waveform.abs().max().item()
234
+ if peak < 1e-7:
235
+ raise RuntimeError("silent audio; retry with a different seed")
236
+ buf = io.BytesIO()
237
+ sf.write(buf, (waveform / peak).clamp(-1, 1).T.numpy(), SAMPLE_RATE, subtype="PCM_16", format="WAV")
238
+ return buf.getvalue()
239
+
240
+
241
+ @app.get("/health")
242
+ def health():
243
+ return dict(STATE)
244
+
245
+
246
+ @app.get("/info")
247
+ def info():
248
+ return dict(STATE) | {
249
+ "model": WEIGHTS_REPO,
250
+ "license": LICENSE,
251
+ "device": "Tenstorrent Blackhole p100a",
252
+ "on_card": ["CFM transformer", "stable-audio VAE decoder"],
253
+ "on_cpu": ["MuQ-MuLan style encoder", "lyric tokenizer", "sampler", "post-processing"],
254
+ "inference_steps": STEPS,
255
+ "cfg_strength": CFG_STRENGTH,
256
+ "task_modes": ["text-to-music"],
257
+ "duration_limits": {"min": MIN_DURATION, "max": MAX_DURATION},
258
+ }
259
+
260
+
261
+ @app.post("/predict")
262
+ def predict(request: Request):
263
+ if not request.prompt.strip():
264
+ raise HTTPException(400, "Empty prompt")
265
+ try:
266
+ if request.inference_steps is not None and request.inference_steps != STEPS:
267
+ raise ValueError(f"this port runs {STEPS} steps (the schedule its shapes are compiled for)")
268
+ if not MIN_DURATION <= request.duration <= MAX_DURATION:
269
+ raise ValueError(f"duration must be between {MIN_DURATION} and {MAX_DURATION} seconds "
270
+ "(use ACE-Step for longer songs)")
271
+ lrc = _lrc(request.lyrics, request.duration, request.instrumental)
272
+ except ValueError as exc:
273
+ raise HTTPException(422, str(exc)) from exc
274
+ if STATE["status"] != "ok" or CFM is None:
275
+ raise HTTPException(503, STATE["error"] or "Model is not ready")
276
+ if not LOCK.acquire(timeout=TURN_WAIT_S):
277
+ raise HTTPException(409, "Another song is in progress")
278
+ try:
279
+ import numpy as np
280
+ from infer_utils import (get_lrc_token, get_negative_style_prompt, get_reference_latent,
281
+ get_style_prompt)
282
+
283
+ STATE["generating"] = True
284
+ t0 = time.perf_counter()
285
+ seed = request.seed if request.seed is not None else random.SystemRandom().randrange(2**32)
286
+ random.seed(seed)
287
+ np.random.seed(seed)
288
+ torch.manual_seed(seed)
289
+ with torch.inference_mode():
290
+ style = get_style_prompt(STYLE, prompt=request.prompt).float()
291
+ text, start_time, end_frame, song_duration = get_lrc_token(
292
+ MAX_FRAMES, lrc, TOKENIZER, request.duration, "cpu")
293
+ negative = get_negative_style_prompt("cpu").float()
294
+ cond, segments = get_reference_latent("cpu", MAX_FRAMES, False, None, None, None)
295
+ latents, _ = CFM.sample(
296
+ cond=cond, text=text, duration=end_frame, max_duration=end_frame,
297
+ song_duration=song_duration, style_prompt=style, negative_style_prompt=negative,
298
+ steps=STEPS, cfg_strength=CFG_STRENGTH, start_time=start_time,
299
+ latent_pred_segments=segments, batch_infer_num=1, seed=seed)
300
+ latent = latents[0].float().transpose(1, 2).contiguous()
301
+ vae = RemoteVae(WORKER, upsample=UPSAMPLE)
302
+ waveform = vae(latent)[0]
303
+ wav = _wav(waveform)
304
+ timing = {
305
+ "dit_s": round(sum(CFM.transformer.seconds), 2),
306
+ "dit_calls": len(CFM.transformer.seconds),
307
+ "vae_s": round(sum(vae.seconds), 2),
308
+ "total_s": round(time.perf_counter() - t0, 2),
309
+ }
310
+ CFM.transformer.seconds.clear()
311
+ # The card holds no song once the VAE has run, so the shim's "already prepared" cache has
312
+ # to go with it -- the next request's text and conditioning differ and would otherwise be
313
+ # skipped. A per-song shim would not need this; a server keeps one across songs.
314
+ CFM.transformer.reset()
315
+ return {
316
+ "audio": base64.b64encode(wav).decode(),
317
+ "format": "wav",
318
+ "model": WEIGHTS_REPO,
319
+ "license": LICENSE,
320
+ "seed": seed,
321
+ "duration": request.duration,
322
+ "lrc": lrc,
323
+ "inference_steps": STEPS,
324
+ "timing_s": timing,
325
+ }
326
+ except Exception as exc:
327
+ if CFM is not None:
328
+ CFM.transformer.reset()
329
+ if WORKER is not None and WORKER.failed:
330
+ STATE.update(status="error", error=str(exc))
331
+ raise HTTPException(503, str(exc)) from exc
332
+ finally:
333
+ STATE["generating"] = False
334
+ LOCK.release()
code/tt_diffrhythm/shapes.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Shapes shared by the host pipeline and the TT worker (no torch or ttnn imports)."""
2
+
3
+ # VAE windows in latent frames (remote.py RemoteVae picks the largest that fits the song); the worker builds
4
+ # these programs at start and warm_cache.py compiles them ahead of time
5
+ VAE_WINDOWS = (1024, 512)
code/tt_diffrhythm/warm_cache.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Compile every P100a kernel shape this port can hit, ahead of the first songs.
3
+
4
+ The card compiles kernels per shape and keeps them in TT_METAL_CACHE (data/tt-cache). Without this, the first song
5
+ of each new length bucket pays that compilation (tens of seconds). Shapes, per model (--model):
6
+ ENC_BUCKET), and both VAE windows (remote.py RemoteVae)
7
+ diffrhythm: every DiT sequence bucket of a 96-285 s song (diffrhythm_host.seq_buckets), both CFG branches, and
8
+ both VAE windows
9
+ Random inputs: only the shapes matter. Run it again after changing this package or the TT image.
10
+ """
11
+ import argparse
12
+ import json
13
+ import time
14
+
15
+ import torch
16
+
17
+ from . import diffrhythm_dit, diffrhythm_host as dhost, oobleck_vae
18
+ from .shapes import VAE_WINDOWS
19
+
20
+ def warm_vae(vae, report):
21
+ for window in VAE_WINDOWS: # exactly as the worker does right after loading (worker.py)
22
+ t0 = time.monotonic()
23
+ audio = vae.decode(torch.zeros(64, window))
24
+ rec = {"vae_window": window, "s": round(time.monotonic() - t0, 2), "finite": bool(torch.isfinite(audio).all())}
25
+ report["vae"].append(rec)
26
+ print(json.dumps(rec), flush=True)
27
+
28
+
29
+ def warm_diffrhythm(args):
30
+ g = torch.Generator().manual_seed(0)
31
+ started = time.monotonic()
32
+ dev = diffrhythm_dit.open_device()
33
+ report = {"seq_buckets": dhost.seq_buckets(), "dit": [], "vae": []}
34
+ try:
35
+ dit = diffrhythm_dit.DiffRhythmDiT(dev, dhost.Checkpoint(args.checkpoint or "/checkpoint/cfm_model.pt"))
36
+ vae = oobleck_vae.OobleckTT(dev, args.vae or "/vae/vae_model.pt")
37
+ report["load_s"] = round(time.monotonic() - started, 1)
38
+ warm_vae(vae, report)
39
+ for seq_pad in report["seq_buckets"]:
40
+ frames = min(seq_pad, dhost.MAX_FRAMES) # any length in the bucket gives the same shapes
41
+ t0 = time.monotonic()
42
+ song = dit.prepare({null: torch.randn(frames, 512, generator=g) for null in (False, True)},
43
+ torch.randn(frames, 64, generator=g))
44
+ assert song.geo.seq_pad == seq_pad
45
+ finite = True
46
+ for null in (False, True):
47
+ v = dit.forward(torch.randn(frames, 64, generator=g), null, torch.randn(512, generator=g),
48
+ torch.randn(512, generator=g), song)
49
+ finite &= bool(torch.isfinite(v).all())
50
+ dit.release(song)
51
+ rec = {"seq_pad": seq_pad, "s": round(time.monotonic() - t0, 2), "finite": finite}
52
+ report["dit"].append(rec)
53
+ print(json.dumps(rec), flush=True)
54
+ finally:
55
+ diffrhythm_dit.close_device(dev)
56
+ report["total_s"] = round(time.monotonic() - started, 1)
57
+ print(json.dumps({k: report[k] for k in ("seq_buckets", "load_s", "total_s")}), flush=True)
58
+
59
+
60
+ def main():
61
+ parser = argparse.ArgumentParser()
62
+ parser.add_argument("--checkpoint")
63
+ parser.add_argument("--vae")
64
+ args = parser.parse_args()
65
+ return warm_diffrhythm(args)
66
+
67
+
68
+ if __name__ == "__main__":
69
+ main()
code/tt_diffrhythm/wire.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tensors between the host ACE-Step process and the TT DiT worker.
2
+
3
+ The two sides run different numpy/torch builds (ACE-Step venv vs the TT image), so tensors cross the socket as raw
4
+ little-endian bytes with dtype and shape, inside plain dicts that pickle identically on both sides."""
5
+ from __future__ import annotations
6
+
7
+ import torch
8
+
9
+ _DTYPES = {"float32": torch.float32}
10
+
11
+
12
+ def encode(t: torch.Tensor) -> dict:
13
+ t = t.detach().to("cpu", torch.float32).contiguous()
14
+ return {"dtype": "float32", "shape": list(t.shape), "data": t.numpy().tobytes()}
15
+
16
+
17
+ def decode(d: dict) -> torch.Tensor:
18
+ return torch.frombuffer(bytearray(d["data"]), dtype=_DTYPES[d["dtype"]]).reshape(d["shape"])
code/tt_diffrhythm/worker.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """TT worker: loads a music model's DiT and its audio VAE decoder onto the P100a and answers one host pipeline.
3
+
4
+ Runs inside the TT image; the host (remote.py) owns the container for its whole life and talks to
5
+ this process over a Unix socket in a directory bind-mounted into both:
6
+
7
+ python /work/tt_diffrhythm/worker.py --socket /ipc/worker.sock [--model diffrhythm] --checkpoint ... --vae ...
8
+
9
+ Messages are plain dicts (tensors via wire.py):
10
+ -> {"op": "ready", ...} once the weights are on the device
11
+ <- {"op": "prepare", ...} -> {"op": "prepared", ...}
12
+ DiffRhythm: "text_embed": {drop_text: [T, 512]}, "cond": [T, 64]
13
+ <- {"op": "forward", ...} -> {"op": "velocity", "v": [T, 64], ...}
14
+ DiffRhythm: "x": [T, 64], "drop_text": bool, "style": [512], "c": [512]
15
+ <- {"op": "vae", "z": [64, L]} -> {"op": "audio", "audio": [2, L * upsample], ...}
16
+ <- {"op": "close"}
17
+ Any failure is sent back as {"op": "error", "error": "..."} and ends the worker.
18
+ """
19
+ import argparse
20
+ from multiprocessing.connection import Listener
21
+ import os
22
+ import signal
23
+ import sys
24
+ import time
25
+ import traceback
26
+
27
+ import torch
28
+
29
+ from . import diffrhythm_dit, oobleck_vae, wire
30
+ from .shapes import VAE_WINDOWS
31
+
32
+
33
+ class DiffRhythm:
34
+ def __init__(self, dev, checkpoint):
35
+ from . import diffrhythm_host
36
+
37
+ self.dit = diffrhythm_dit.DiffRhythmDiT(dev, diffrhythm_host.Checkpoint(checkpoint))
38
+
39
+ def prepare(self, msg):
40
+ text_embed = {bool(k): wire.decode(v) for k, v in msg["text_embed"].items()}
41
+ song = self.dit.prepare(text_embed, wire.decode(msg["cond"]))
42
+ return song, {"frames": song.geo.frames, "seq_pad": song.geo.seq_pad}
43
+
44
+ def forward(self, msg, song):
45
+ return self.dit.forward(wire.decode(msg["x"]), bool(msg["drop_text"]), wire.decode(msg["style"]),
46
+ wire.decode(msg["c"]), song)
47
+
48
+
49
+ MODELS = {"diffrhythm": (DiffRhythm, "/checkpoint/cfm_model.pt", "/vae/vae_model.pt")}
50
+
51
+
52
+ def serve(conn, model, vae):
53
+ dit = model.dit
54
+ song = None
55
+ while True:
56
+ msg = conn.recv()
57
+ op = msg.get("op")
58
+ started = time.monotonic()
59
+ if op == "prepare":
60
+ if song is not None:
61
+ dit.release(song)
62
+ song = None
63
+ song, geometry = model.prepare(msg)
64
+ conn.send({"op": "prepared", "seconds": time.monotonic() - started, "geometry": geometry})
65
+ elif op == "forward":
66
+ if song is None:
67
+ raise RuntimeError("forward before prepare")
68
+ v = model.forward(msg, song)
69
+ conn.send({"op": "velocity", "v": wire.encode(v), "seconds": time.monotonic() - started})
70
+ elif op == "vae":
71
+ if song is not None: # sampling is over; freeing the song's tensors first gives the VAE the same device
72
+ dit.release(song) # allocation state for every song length, so its compiled conv kernels are reused
73
+ song = None
74
+ audio = vae.decode(wire.decode(msg["z"]))
75
+ conn.send({"op": "audio", "audio": wire.encode(audio), "seconds": time.monotonic() - started})
76
+ elif op == "close":
77
+ if song is not None:
78
+ dit.release(song)
79
+ return
80
+ else:
81
+ raise RuntimeError(f"unknown op {op!r}")
82
+
83
+
84
+ def main():
85
+ # docker stop / a signal: leave through the finally below so the device is closed, not dropped
86
+ signal.signal(signal.SIGTERM, lambda *_: sys.exit(143))
87
+ parser = argparse.ArgumentParser()
88
+ parser.add_argument("--socket", required=True)
89
+ parser.add_argument("--model", choices=sorted(MODELS), default="diffrhythm")
90
+ parser.add_argument("--checkpoint")
91
+ parser.add_argument("--vae")
92
+ args = parser.parse_args()
93
+ model_class, checkpoint, vae_path = MODELS[args.model]
94
+ authkey = os.environ.get("MUSIC_TT_AUTHKEY", "").encode() or None
95
+ listener = Listener(args.socket, family="AF_UNIX", authkey=authkey)
96
+ started = time.monotonic()
97
+ dev = diffrhythm_dit.open_device()
98
+ conn = None
99
+ try:
100
+ model = model_class(dev, args.checkpoint or checkpoint)
101
+ vae = oobleck_vae.OobleckTT(dev, args.vae or vae_path)
102
+ # Build the VAE programs now, in the same device state for every song: their conv kernels then compile
103
+ # once (TT_METAL_CACHE) and later decodes reuse the in-process programs. Built after a song's DiT steps
104
+ # instead, the device state (and so the kernels) depends on the song length and recompiles for 20-40 s.
105
+ warm = time.monotonic()
106
+ for window in VAE_WINDOWS:
107
+ vae.decode(torch.zeros(64, window))
108
+ ready = {"op": "ready", "model": args.model, "load_s": time.monotonic() - started,
109
+ "vae_warm_s": time.monotonic() - warm, "upsample": vae.upsample, "dram": diffrhythm_dit.dram_stats(dev)}
110
+ conn = listener.accept() # one pipeline per container
111
+ conn.send(ready)
112
+ serve(conn, model, vae)
113
+ except BaseException as exc:
114
+ traceback.print_exc()
115
+ if conn is not None:
116
+ try:
117
+ conn.send({"op": "error", "error": f"{type(exc).__name__}: {exc}"})
118
+ except OSError:
119
+ pass
120
+ raise
121
+ finally:
122
+ diffrhythm_dit.close_device(dev)
123
+ listener.close()
124
+
125
+
126
+ if __name__ == "__main__":
127
+ main()
image/blobs/sha256/0cc125e439ef6882a01bae57bece67f14ec2ad123aec1fb12e86bc50175a4b44 ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schemaVersion": 2,
3
+ "mediaType": "application/vnd.oci.image.index.v1+json",
4
+ "manifests": [
5
+ {
6
+ "mediaType": "application/vnd.oci.image.manifest.v1+json",
7
+ "digest": "sha256:81aae6dd123609902a2f56719e50ef0bcf3aa00aff71fc66b02648b3f4aea2f2",
8
+ "size": 4499,
9
+ "platform": {
10
+ "architecture": "amd64",
11
+ "os": "linux"
12
+ }
13
+ },
14
+ {
15
+ "mediaType": "application/vnd.oci.image.manifest.v1+json",
16
+ "digest": "sha256:8f94a918ad7d7da27e2b025adda011abaa34d892741d6b28221015c75a4ac5ba",
17
+ "size": 837,
18
+ "annotations": {
19
+ "vnd.docker.reference.digest": "sha256:81aae6dd123609902a2f56719e50ef0bcf3aa00aff71fc66b02648b3f4aea2f2",
20
+ "vnd.docker.reference.type": "attestation-manifest"
21
+ },
22
+ "platform": {
23
+ "architecture": "unknown",
24
+ "os": "unknown"
25
+ }
26
+ }
27
+ ]
28
+ }
image/blobs/sha256/340cf5e2c047e0fc940f2973810428f58250242238231682812dec6f6d54f3ac ADDED
Binary file (4.4 kB). View file
 
image/blobs/sha256/44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a ADDED
@@ -0,0 +1 @@
 
 
1
+ {}
image/blobs/sha256/4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1 ADDED
Binary file (32 Bytes). View file
 
image/blobs/sha256/6acee8cd93aa0889cc3a8b09f1b2564234ee82aa7c31e87ab61591552d15e894 ADDED
Binary file (5.61 kB). View file
 
image/blobs/sha256/81aae6dd123609902a2f56719e50ef0bcf3aa00aff71fc66b02648b3f4aea2f2 ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schemaVersion": 2,
3
+ "mediaType": "application/vnd.oci.image.manifest.v1+json",
4
+ "config": {
5
+ "mediaType": "application/vnd.oci.image.config.v1+json",
6
+ "digest": "sha256:eae56eb3c9610901618ab9c30e2ba8cdcf6981cdd3af7ed5755aba2cc46db6e1",
7
+ "size": 14328
8
+ },
9
+ "layers": [
10
+ {
11
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
12
+ "digest": "sha256:98c4455a98982b35380ec62f90d5cf0edff02eeb4b3bb18595d871fdeb0c0c67",
13
+ "size": 29751627
14
+ },
15
+ {
16
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
17
+ "digest": "sha256:3375e6602aa7264d6d67ae69ccb99690c8f40af7ab48d0af060dac1a2e9c24c9",
18
+ "size": 69483284
19
+ },
20
+ {
21
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
22
+ "digest": "sha256:340cf5e2c047e0fc940f2973810428f58250242238231682812dec6f6d54f3ac",
23
+ "size": 4399
24
+ },
25
+ {
26
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
27
+ "digest": "sha256:1f47921d53a3b8de72172e2fd5f0a0d27f1e8f4b77016c6b8a1a4b591bb83ae5",
28
+ "size": 9565639
29
+ },
30
+ {
31
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
32
+ "digest": "sha256:4ec2b03c373212fbe7fda97dbf9eaadb1bd42c2b0afc8b8a49806fe1dbc507f7",
33
+ "size": 138968721
34
+ },
35
+ {
36
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
37
+ "digest": "sha256:b7c2e1ec67bfd9d4578a81ca6987f095b74869db5f518bf2fc9d3ffeb30821e5",
38
+ "size": 66025096
39
+ },
40
+ {
41
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
42
+ "digest": "sha256:428f99cfe94d9b1e42af2f3d8104598f8ca6e6105925c7687595a18c78d21011",
43
+ "size": 503666016
44
+ },
45
+ {
46
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
47
+ "digest": "sha256:ee5adbd6eba4dfd9ba7e59692c46379b3d60a88f8445ec6a045d4aeb336f5bb9",
48
+ "size": 116
49
+ },
50
+ {
51
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
52
+ "digest": "sha256:672d584c094e51e29aa5e46a6dd22cbfb993f4f839609e954d2c299912ea5d77",
53
+ "size": 139080178
54
+ },
55
+ {
56
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
57
+ "digest": "sha256:2d16f4104fc6e34c556d42d6a35adedd5ffa396e71d59ef6dd7d5e88c01225e0",
58
+ "size": 39231465
59
+ },
60
+ {
61
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
62
+ "digest": "sha256:556ec3c4c0e12c97f4c9b8dd0cd65ae321238c55438fbf498055f0ad7afc05bb",
63
+ "size": 39231323
64
+ },
65
+ {
66
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
67
+ "digest": "sha256:b9ddf00cad9d9a3bc91d09c8ee0f4fef42056861bb5ca1209fcc204b68950abf",
68
+ "size": 66525731
69
+ },
70
+ {
71
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
72
+ "digest": "sha256:0b4ec6da503185d389dcfeefc2c5d03ab13e0d76620b5fb6ce022f79bb64140f",
73
+ "size": 12761466
74
+ },
75
+ {
76
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
77
+ "digest": "sha256:49353ead98e6a241dd015dda937bf8dcf335e4eed27fe732df8163dd955d042f",
78
+ "size": 1898379
79
+ },
80
+ {
81
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
82
+ "digest": "sha256:c2a425aad5e9144add8f2a619c6e3d452d30ddbb5d4f4d3e45caab4f7bb3d233",
83
+ "size": 6878
84
+ },
85
+ {
86
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
87
+ "digest": "sha256:e211ac567cceb6b5be62db5e135a1a5f3e77bcacebdac6a8cf9e0cdc24158c09",
88
+ "size": 108850224
89
+ },
90
+ {
91
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
92
+ "digest": "sha256:c6bd350dd2ed57d0ea34248e5b27d985a6786330d2d3ec29f5563e03ca9f00da",
93
+ "size": 1080
94
+ },
95
+ {
96
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
97
+ "digest": "sha256:8f09ce5e10995b3fa6362dae9945d9402a5ccabce3d5bf20df0a53c75d9f94ab",
98
+ "size": 982
99
+ },
100
+ {
101
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
102
+ "digest": "sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1",
103
+ "size": 32
104
+ },
105
+ {
106
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
107
+ "digest": "sha256:e600ba821186d2d540ea813a478dda1e629c539eef283f062f31309cb8c3af47",
108
+ "size": 674
109
+ },
110
+ {
111
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
112
+ "digest": "sha256:3c7495db974af283bccc2dee306a75da6491b2ca664d55597da926c4fd104dd5",
113
+ "size": 216662
114
+ },
115
+ {
116
+ "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
117
+ "digest": "sha256:6acee8cd93aa0889cc3a8b09f1b2564234ee82aa7c31e87ab61591552d15e894",
118
+ "size": 5608
119
+ }
120
+ ]
121
+ }
image/blobs/sha256/8f09ce5e10995b3fa6362dae9945d9402a5ccabce3d5bf20df0a53c75d9f94ab ADDED
Binary file (982 Bytes). View file
 
image/blobs/sha256/8f94a918ad7d7da27e2b025adda011abaa34d892741d6b28221015c75a4ac5ba ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schemaVersion": 2,
3
+ "mediaType": "application/vnd.oci.image.manifest.v1+json",
4
+ "artifactType": "application/vnd.docker.attestation.manifest.v1+json",
5
+ "config": {
6
+ "mediaType": "application/vnd.oci.empty.v1+json",
7
+ "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
8
+ "size": 2,
9
+ "data": "e30="
10
+ },
11
+ "layers": [
12
+ {
13
+ "mediaType": "application/vnd.in-toto+json",
14
+ "digest": "sha256:9b3433e39d3d40a2f64d1f1e8cb155387651684c5f5c9473bd733c72056dee58",
15
+ "size": 1923,
16
+ "annotations": {
17
+ "in-toto.io/predicate-type": "https://slsa.dev/provenance/v1"
18
+ }
19
+ }
20
+ ],
21
+ "subject": {
22
+ "mediaType": "application/vnd.oci.image.manifest.v1+json",
23
+ "digest": "sha256:81aae6dd123609902a2f56719e50ef0bcf3aa00aff71fc66b02648b3f4aea2f2",
24
+ "size": 4499
25
+ }
26
+ }
image/blobs/sha256/9b3433e39d3d40a2f64d1f1e8cb155387651684c5f5c9473bd733c72056dee58 ADDED
@@ -0,0 +1 @@
 
 
1
+ {"_type":"https://in-toto.io/Statement/v1","predicateType":"https://slsa.dev/provenance/v1","subject":[{"name":"pkg:docker/tt-model/diffrhythm-1.2-p100a@build-58648d444?platform=linux%2Famd64","digest":{"sha256":"81aae6dd123609902a2f56719e50ef0bcf3aa00aff71fc66b02648b3f4aea2f2"}}],"predicate":{"buildDefinition":{"buildType":"https://github.com/moby/buildkit/blob/master/docs/attestations/slsa-definitions.md","resolvedDependencies":[{"uri":"pkg:docker/docker/dockerfile@1.7-labs","digest":{"sha256":"b99fecfe00268a8b556fad7d9c37ee25d716ae08a5d7320e6d51c4dd83246894"}},{"uri":"pkg:docker/ubuntu@22.04?platform=linux%2Famd64","digest":{"sha256":"5ec03bb3441e8b0bf3b4f9cd4629a1ae763010dc3035bb8da3ae6cf026486401"}},{"uri":"pkg:docker/ghcr.io/tenstorrent/tt-metal/tt-metalium/ubuntu-22.04-dev-amd64@latest?platform=linux%2Famd64","digest":{"sha256":"df9d279c7f85c17c6fad982d196802682d669cca1b7ced9cbaad8181339cd5fc"}}],"externalParameters":{"configSource":{"path":"Dockerfile"},"request":{"frontend":"gateway.v0","args":{"cmdline":"docker/dockerfile:1.7-labs","context:metalsrc":"local:metalsrc","frontend.caps":"moby.buildkit.frontend.contexts+forward","sharedkey:localdir:metalsrc":"empty-metalsrc:f5e176f12fb9c045","source":"docker/dockerfile:1.7-labs"},"locals":[{"name":"context"},{"name":"dockerfile"},{"name":"metalsrc"}],"root":{"configSource":{"path":"Dockerfile"},"request":{"args":{"context:metalsrc":"local:metalsrc","frontend.caps":"moby.buildkit.frontend.contexts+forward","sharedkey:localdir:metalsrc":"empty-metalsrc:f5e176f12fb9c045"}}},"compatibilityVersion":30}},"internalParameters":{"builderPlatform":"linux/amd64"}},"runDetails":{"builder":{"id":""},"metadata":{"invocationId":"lt0mpnhkaz55xrvi2wqvfduge","startedOn":"2026-10-09T23:02:08.374050954-04:00","finishedOn":"2026-10-09T23:07:18.826107934-04:00","buildkit_metadata":{},"buildkit_completeness":{"request":false,"resolvedDependencies":false}}}}}
image/blobs/sha256/c2a425aad5e9144add8f2a619c6e3d452d30ddbb5d4f4d3e45caab4f7bb3d233 ADDED
Binary file (6.88 kB). View file
 
image/blobs/sha256/c6bd350dd2ed57d0ea34248e5b27d985a6786330d2d3ec29f5563e03ca9f00da ADDED
Binary file (1.08 kB). View file
 
image/blobs/sha256/e600ba821186d2d540ea813a478dda1e629c539eef283f062f31309cb8c3af47 ADDED
Binary file (674 Bytes). View file
 
image/blobs/sha256/eae56eb3c9610901618ab9c30e2ba8cdcf6981cdd3af7ed5755aba2cc46db6e1 ADDED
@@ -0,0 +1 @@
 
 
1
+ {"architecture":"amd64","config":{"User":"tt","Env":["PATH=/opt/tt-venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","VENV=/opt/tt-venv","VIRTUAL_ENV=/opt/tt-venv","TT_METAL_RUNTIME_ROOT=/opt/tt-metal","TT_METAL_HOME=/opt/tt-metal","PYTHONPATH=/opt/tt-metal","LD_LIBRARY_PATH=/opt/tt-metal/build/lib:/opt/openmpi-v5.0.7-ulfm/lib","EXTRA_MODELS_DIR=","TT_VLLM_BUILTIN_MODELS=","TT_MODEL_KIND=tt-dit-server","HF_HOME=/hf","TT_METAL_CACHE=/cache","HOME=/home/tt","USER=tt","LOGNAME=tt"],"Entrypoint":["/usr/local/bin/entrypoint.sh"],"Cmd":["/usr/local/bin/serve-default.sh"],"WorkingDir":"/home/tt/work","Labels":{"org.opencontainers.image.revision":"58648d44443223041148125843ca7c7d4780463e","org.opencontainers.image.version":"22.04","org.tenstorrent.tt-model":"diffrhythm-1.2-p100a","org.tenstorrent.tt-model.arch":"blackhole","org.tenstorrent.tt-model.kind":"tt-dit-server","org.tenstorrent.tt-model.plugin":"","org.tenstorrent.tt-model.profiles":"default","org.tenstorrent.tt-model.repo":"sunandbean/diffrhythm-1.2-p100a","org.tenstorrent.tt-model.tt-metal":"","org.tenstorrent.tt-model.weights":"ASLP-lab/DiffRhythm-1_2-full"},"ArgsEscaped":true},"created":"2026-10-09T23:06:45.672498279-04:00","history":[{"created":"2026-09-24T22:24:04.688908979Z","created_by":"/bin/sh -c #(nop) ARG RELEASE","empty_layer":true},{"created":"2026-09-24T22:24:04.723527376Z","created_by":"/bin/sh -c #(nop) ARG LAUNCHPAD_BUILD_ARCH","empty_layer":true},{"created":"2026-09-24T22:24:04.751455651Z","created_by":"/bin/sh -c #(nop) LABEL org.opencontainers.image.version=22.04","empty_layer":true},{"created":"2026-09-24T22:24:06.837737736Z","created_by":"/bin/sh -c #(nop) ADD file:b0bf3f64519bf10a51e00d4f8ab9c8693620659a0c4a4a5090a9765e754e1d1c in / "},{"created":"2026-09-24T22:24:07.239071539Z","created_by":"/bin/sh -c #(nop) CMD [\"/bin/bash\"]","empty_layer":true},{"created":"2026-10-09T09:22:08.388097186-04:00","created_by":"ARG OMPI_DIR=/opt/openmpi-v5.0.7-ulfm","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T09:22:08.388097186-04:00","created_by":"ARG EXTRA_MODELS_DIR=","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T09:22:08.388097186-04:00","created_by":"ARG TT_MODEL_KIND","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T09:22:08.388097186-04:00","created_by":"ARG MODEL_NAME","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T09:22:08.388097186-04:00","created_by":"ARG MODEL_REPO","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T09:22:08.388097186-04:00","created_by":"ARG MODEL_WEIGHTS","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T09:22:08.388097186-04:00","created_by":"ARG MODEL_ARCH","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T09:22:08.388097186-04:00","created_by":"ARG MODEL_PROFILES","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T09:22:08.388097186-04:00","created_by":"ARG MODEL_TT_METAL_SHA","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T09:22:08.388097186-04:00","created_by":"ARG MODEL_TT_METAL_DESCRIBE","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T09:22:08.388097186-04:00","created_by":"ARG MODEL_PLUGIN_SHA","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T09:22:08.388097186-04:00","created_by":"RUN |11 OMPI_DIR=/opt/openmpi-v5.0.7-ulfm EXTRA_MODELS_DIR= TT_MODEL_KIND=tt-dit-server MODEL_NAME=diffrhythm-1.2-p100a MODEL_REPO=sunandbean/diffrhythm-1.2-p100a MODEL_WEIGHTS=ASLP-lab/DiffRhythm-1_2-full MODEL_ARCH=blackhole MODEL_PROFILES=default MODEL_TT_METAL_SHA=58648d44443223041148125843ca7c7d4780463e MODEL_TT_METAL_DESCRIBE= MODEL_PLUGIN_SHA= /bin/sh -c apt-get update \u0026\u0026 apt-get install -y --no-install-recommends libhwloc15 libnuma1 libatomic1 libudev1 libcap2 zlib1g libmpc3 libmpfr6 libgmp10 libzstd1 libevent-core-2.1-7 libevent-pthreads-2.1-7 libgl1 libsndfile1 ca-certificates \u0026\u0026 apt-get clean \u0026\u0026 rm -rf /var/lib/apt/lists/* # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T09:22:08.570265459-04:00","created_by":"RUN |11 OMPI_DIR=/opt/openmpi-v5.0.7-ulfm EXTRA_MODELS_DIR= TT_MODEL_KIND=tt-dit-server MODEL_NAME=diffrhythm-1.2-p100a MODEL_REPO=sunandbean/diffrhythm-1.2-p100a MODEL_WEIGHTS=ASLP-lab/DiffRhythm-1_2-full MODEL_ARCH=blackhole MODEL_PROFILES=default MODEL_TT_METAL_SHA=58648d44443223041148125843ca7c7d4780463e MODEL_TT_METAL_DESCRIBE= MODEL_PLUGIN_SHA= /bin/sh -c existing=\"$(getent passwd 1000 | cut -d: -f1)\" \u0026\u0026 if [ -n \"$existing\" ]; then userdel -r \"$existing\" 2\u003e/dev/null || userdel \"$existing\"; fi \u0026\u0026 useradd --uid 1000 --create-home --home-dir /home/tt --shell /bin/bash tt \u0026\u0026 mkdir -p /home/tt/work/logs /cache /opt/tt-metal \u0026\u0026 chown -R tt:tt /home/tt /cache /opt/tt-metal \u0026\u0026 chmod 1777 /home/tt /home/tt/work /home/tt/work/logs /cache # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T09:26:50.393285226-04:00","created_by":"COPY /opt/openmpi-v5.0.7-ulfm /opt/openmpi-v5.0.7-ulfm # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T09:26:50.967022517-04:00","created_by":"COPY /opt/tenstorrent /opt/tenstorrent # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:05:49.28099242-04:00","created_by":"COPY /usr/local/share/uv /usr/local/share/uv # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:05:52.554077344-04:00","created_by":"COPY /opt/tt-venv /opt/tt-venv # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:05:52.741256841-04:00","created_by":"COPY /opt/vllm /opt/vllm # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:05:53.245850738-04:00","created_by":"COPY --chown=tt:tt /opt/tt-metal/runtime /opt/tt-metal/runtime # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:05:53.42384338-04:00","created_by":"COPY --chown=tt:tt /opt/tt-metal/build_Release /opt/tt-metal/build_Release # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:05:53.584846845-04:00","created_by":"COPY --chown=tt:tt /opt/tt-metal/build /opt/tt-metal/build # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:05:54.25191112-04:00","created_by":"COPY --chown=tt:tt /opt/tt-metal/tt_metal /opt/tt-metal/tt_metal # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:05:54.825250753-04:00","created_by":"COPY --chown=tt:tt /opt/tt-metal/ttnn /opt/tt-metal/ttnn # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:05:54.915803026-04:00","created_by":"COPY --chown=tt:tt /opt/tt-metal/tools /opt/tt-metal/tools # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:05:54.941505166-04:00","created_by":"COPY --chown=tt:tt /opt/tt-metal/setup.py /opt/tt-metal/pyproject.toml /opt/tt-metal/ # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:05:55.213574197-04:00","created_by":"COPY --chown=tt:tt code/ /opt/tt-metal/ # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:05:55.238685903-04:00","created_by":"COPY entrypoint.sh /usr/local/bin/entrypoint.sh # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"COPY --chmod=0755 serve-default.sh /usr/local/bin/serve-default.sh # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"ENV VENV=/opt/tt-venv","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"ENV VIRTUAL_ENV=/opt/tt-venv","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"ENV PATH=/opt/tt-venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"ENV TT_METAL_RUNTIME_ROOT=/opt/tt-metal","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"ENV TT_METAL_HOME=/opt/tt-metal","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"ENV PYTHONPATH=/opt/tt-metal","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"ENV LD_LIBRARY_PATH=/opt/tt-metal/build/lib:/opt/openmpi-v5.0.7-ulfm/lib","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"ENV EXTRA_MODELS_DIR=","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"ARG TT_VLLM_BUILTIN_MODELS=","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"ENV TT_VLLM_BUILTIN_MODELS=","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"ENV TT_MODEL_KIND=tt-dit-server","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"ENV HF_HOME=/hf","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"ENV TT_METAL_CACHE=/cache","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"ENV HOME=/home/tt","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"ENV USER=tt","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"ENV LOGNAME=tt","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.256601142-04:00","created_by":"USER tt","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:05:55.273892093-04:00","created_by":"WORKDIR /home/tt/work","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:05:55.28789741-04:00","created_by":"COPY verify.sh /ctx/verify.sh # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:06:45.494946817-04:00","created_by":"RUN |12 OMPI_DIR=/opt/openmpi-v5.0.7-ulfm EXTRA_MODELS_DIR= TT_MODEL_KIND=tt-dit-server MODEL_NAME=diffrhythm-1.2-p100a MODEL_REPO=sunandbean/diffrhythm-1.2-p100a MODEL_WEIGHTS=ASLP-lab/DiffRhythm-1_2-full MODEL_ARCH=blackhole MODEL_PROFILES=default MODEL_TT_METAL_SHA=58648d44443223041148125843ca7c7d4780463e MODEL_TT_METAL_DESCRIBE= MODEL_PLUGIN_SHA= TT_VLLM_BUILTIN_MODELS= /bin/sh -c bash /ctx/verify.sh # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:06:45.494946817-04:00","created_by":"USER root","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:06:45.672498279-04:00","created_by":"RUN |12 OMPI_DIR=/opt/openmpi-v5.0.7-ulfm EXTRA_MODELS_DIR= TT_MODEL_KIND=tt-dit-server MODEL_NAME=diffrhythm-1.2-p100a MODEL_REPO=sunandbean/diffrhythm-1.2-p100a MODEL_WEIGHTS=ASLP-lab/DiffRhythm-1_2-full MODEL_ARCH=blackhole MODEL_PROFILES=default MODEL_TT_METAL_SHA=58648d44443223041148125843ca7c7d4780463e MODEL_TT_METAL_DESCRIBE= MODEL_PLUGIN_SHA= TT_VLLM_BUILTIN_MODELS= /bin/sh -c chmod -R a+rwX /home/tt # buildkit","comment":"buildkit.dockerfile.v0"},{"created":"2026-10-09T23:06:45.672498279-04:00","created_by":"USER tt","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:06:45.672498279-04:00","created_by":"LABEL org.tenstorrent.tt-model=diffrhythm-1.2-p100a org.tenstorrent.tt-model.repo=sunandbean/diffrhythm-1.2-p100a org.tenstorrent.tt-model.weights=ASLP-lab/DiffRhythm-1_2-full org.tenstorrent.tt-model.arch=blackhole org.tenstorrent.tt-model.kind=tt-dit-server org.tenstorrent.tt-model.profiles=default org.opencontainers.image.revision=58648d44443223041148125843ca7c7d4780463e org.tenstorrent.tt-model.tt-metal= org.tenstorrent.tt-model.plugin=","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:06:45.672498279-04:00","created_by":"ENTRYPOINT [\"/usr/local/bin/entrypoint.sh\"]","comment":"buildkit.dockerfile.v0","empty_layer":true},{"created":"2026-10-09T23:06:45.672498279-04:00","created_by":"CMD [\"/usr/local/bin/serve-default.sh\"]","comment":"buildkit.dockerfile.v0","empty_layer":true}],"os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:cbaf391e670933f026c05c7dedec3faf8f194b6c2d83b0d5694ad68323c8077c","sha256:09daac6f134b2dba27b8d4a0ef3121a37537cad3f15daa032b9431f80ff87448","sha256:5287d0203a2b8dcd23409b5e9013a5c575f805d95599f294f57401c395c68522","sha256:542109b0dfa66e7d5c45e42504d6acaf67a10027553a1855ed50d97cf40b5fd4","sha256:85747f2ebe464ef004163a156c985fd7654ef8463f0e1f673d5fc3ebb8ca5036","sha256:0a843c5fa2e20d2fb2d102bd7c52914df44527da4a07f6a90d1b7cbdc99d4139","sha256:3c8124f890f49acc6c287be3d6967d2c7aea4b0613442eecb2d12649e346dd19","sha256:84150c24aea3517574d06f1ccb0fd8cbb705ef7fe1360b737bd73f41fe5fe802","sha256:7c7537d0f65122e0a4fb6e585e744529be140d9f30775d4ac3e7d71678841766","sha256:c55f647fbe1619ed1658092a93e0de5543b510df376925b0077fbf87c2b6a606","sha256:0f111d51bc656a83c00484d1ecaf7c053336f5d87914e3e169fdfb1b8730c0b1","sha256:b2b6c01d259d2ba562d3ee67e8481bb3c12c484fa4817411822773177d83122e","sha256:558e4e46be0c2d5366bf9e13cb1076dffcfa9b386b7a702a2be672df0abbfe26","sha256:2aa75a7513dd7fd2e2df7fb28da3f3de2bddfe115eb51b562b3a9cf7faea0719","sha256:ac5907e681ddb595c56a03bfe14231bfdd907438015951cbeaa6313e4b5072af","sha256:5fc6fd4ae4f4966d156d66670a5f1c1ec3087a656ecb06b8af21f27be8e94870","sha256:89c2380aa2116794479b618d5e7a373ba519c24b3a5e67756a1c09e6ca089fa5","sha256:568be329db8740036ac4055096cd89a8b909b0375fefe0f2885a287c7ffb9855","sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef","sha256:7b7966ac1b44afb1004b16928394cc98583c8854c533405e3e68d01de8600c5b","sha256:228b2f895919d9b9f799eb573f98d3e01f7e6e45b01a406366b69b1aa333fa91","sha256:369854526b98a8f50c544e3aca82a2cc696396fbe0f96ca020dca91a87502fd5"]}}
image/blobs/sha256/ee5adbd6eba4dfd9ba7e59692c46379b3d60a88f8445ec6a045d4aeb336f5bb9 ADDED
Binary file (116 Bytes). View file
 
image/index.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"schemaVersion":2,"mediaType":"application/vnd.oci.image.index.v1+json","manifests":[{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:0cc125e439ef6882a01bae57bece67f14ec2ad123aec1fb12e86bc50175a4b44","size":856,"annotations":{"io.containerd.image.name":"docker.io/tt-model/diffrhythm-1.2-p100a:0cc125e439ef","org.opencontainers.image.ref.name":"0cc125e439ef"}}]}
image/manifest.json ADDED
@@ -0,0 +1 @@
 
 
1
+ [{"Config":"blobs/sha256/eae56eb3c9610901618ab9c30e2ba8cdcf6981cdd3af7ed5755aba2cc46db6e1","RepoTags":["tt-model/diffrhythm-1.2-p100a:0cc125e439ef"],"Layers":["blobs/sha256/98c4455a98982b35380ec62f90d5cf0edff02eeb4b3bb18595d871fdeb0c0c67","blobs/sha256/3375e6602aa7264d6d67ae69ccb99690c8f40af7ab48d0af060dac1a2e9c24c9","blobs/sha256/340cf5e2c047e0fc940f2973810428f58250242238231682812dec6f6d54f3ac","blobs/sha256/1f47921d53a3b8de72172e2fd5f0a0d27f1e8f4b77016c6b8a1a4b591bb83ae5","blobs/sha256/4ec2b03c373212fbe7fda97dbf9eaadb1bd42c2b0afc8b8a49806fe1dbc507f7","blobs/sha256/b7c2e1ec67bfd9d4578a81ca6987f095b74869db5f518bf2fc9d3ffeb30821e5","blobs/sha256/428f99cfe94d9b1e42af2f3d8104598f8ca6e6105925c7687595a18c78d21011","blobs/sha256/ee5adbd6eba4dfd9ba7e59692c46379b3d60a88f8445ec6a045d4aeb336f5bb9","blobs/sha256/672d584c094e51e29aa5e46a6dd22cbfb993f4f839609e954d2c299912ea5d77","blobs/sha256/2d16f4104fc6e34c556d42d6a35adedd5ffa396e71d59ef6dd7d5e88c01225e0","blobs/sha256/556ec3c4c0e12c97f4c9b8dd0cd65ae321238c55438fbf498055f0ad7afc05bb","blobs/sha256/b9ddf00cad9d9a3bc91d09c8ee0f4fef42056861bb5ca1209fcc204b68950abf","blobs/sha256/0b4ec6da503185d389dcfeefc2c5d03ab13e0d76620b5fb6ce022f79bb64140f","blobs/sha256/49353ead98e6a241dd015dda937bf8dcf335e4eed27fe732df8163dd955d042f","blobs/sha256/c2a425aad5e9144add8f2a619c6e3d452d30ddbb5d4f4d3e45caab4f7bb3d233","blobs/sha256/e211ac567cceb6b5be62db5e135a1a5f3e77bcacebdac6a8cf9e0cdc24158c09","blobs/sha256/c6bd350dd2ed57d0ea34248e5b27d985a6786330d2d3ec29f5563e03ca9f00da","blobs/sha256/8f09ce5e10995b3fa6362dae9945d9402a5ccabce3d5bf20df0a53c75d9f94ab","blobs/sha256/4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1","blobs/sha256/e600ba821186d2d540ea813a478dda1e629c539eef283f062f31309cb8c3af47","blobs/sha256/3c7495db974af283bccc2dee306a75da6491b2ca664d55597da926c4fd104dd5","blobs/sha256/6acee8cd93aa0889cc3a8b09f1b2564234ee82aa7c31e87ab61591552d15e894"]}]
image/oci-layout ADDED
@@ -0,0 +1 @@
 
 
1
+ {"imageLayoutVersion":"1.0.0"}
requirements.lock ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ accelerate==1.15.0
2
+ annotated-doc==0.0.5
3
+ annotated-types==0.8.0
4
+ anyio==4.15.1
5
+ attrs==26.1.0
6
+ beartype==0.22.9
7
+ certifi==2026.7.22
8
+ cffi==2.1.1
9
+ cfgv==3.5.0
10
+ charset-normalizer==3.5.2
11
+ click==8.5.0
12
+ cloudpickle==3.1.2
13
+ cn2an==0.5.24
14
+ contourpy==1.3.3
15
+ cycler==0.12.1
16
+ decorator==5.3.1
17
+ Deprecated==3.0.0
18
+ distlib==0.4.3
19
+ distro==1.9.0
20
+ dlinfo==2.0.0
21
+ easydict==1.13
22
+ einops==0.8.2
23
+ einx==0.4.3
24
+ elastic-transport==9.4.2
25
+ elasticsearch==9.5.1
26
+ ema-pytorch==0.8.3
27
+ espeakng-loader==0.2.4
28
+ fastapi==0.143.0
29
+ filelock==4.1.0
30
+ flatbuffers==25.12.19
31
+ fonttools==4.66.1
32
+ frozendict==2.4.7
33
+ fsspec==2026.9.0
34
+ ftfy==6.3.1
35
+ googleapis-common-protos==1.75.5
36
+ graphviz==0.21
37
+ h11==0.16.0
38
+ hf-xet==1.7.0
39
+ httpcore2==2.13.1
40
+ httpx2==2.13.1
41
+ huggingface_hub==2.2.0
42
+ identify==2.6.20
43
+ idna==3.20
44
+ inflect==7.5.0
45
+ jaconv==0.5.0
46
+ jieba==0.42.1
47
+ Jinja2==3.1.6
48
+ joblib==1.6.0
49
+ kiwisolver==1.5.1
50
+ lazy-loader==0.6
51
+ librosa==1.0.0
52
+ linkify-it-py==2.2.0
53
+ llvmlite==0.50.0
54
+ loguru==0.7.3
55
+ markdown-it-py==4.2.0
56
+ MarkupSafe==3.0.4
57
+ matplotlib==3.11.2
58
+ mdit-py-plugins==0.6.1
59
+ mdurl==0.1.2
60
+ ml_dtypes==0.5.4
61
+ more-itertools==11.1.0
62
+ mpmath==1.3.0
63
+ msgpack==1.2.3
64
+ muq==0.1.0
65
+ mutagen==1.48.1
66
+ narwhals==2.26.0
67
+ networkx==3.7
68
+ nnaudio==0.3.4
69
+ nodeenv==1.11.0
70
+ numba==0.68.0
71
+ numpy==2.5.3
72
+ onnxruntime==1.31.0
73
+ opentelemetry-api==1.45.1
74
+ opentelemetry-exporter-http-transport==0.66b1
75
+ opentelemetry-exporter-otlp-common==0.66b1
76
+ opentelemetry-exporter-otlp-proto-common==1.45.1
77
+ opentelemetry-exporter-otlp-proto-http==1.45.1
78
+ opentelemetry-proto==1.45.1
79
+ opentelemetry-sdk==1.45.1
80
+ opentelemetry-semantic-conventions==0.66b1
81
+ packaging==26.3
82
+ pandas==3.0.6
83
+ phonemizer==3.4.0
84
+ pillow==12.3.0
85
+ platformdirs==4.12.4
86
+ pooch==1.9.0
87
+ pre_commit==4.6.2
88
+ proces==0.1.7
89
+ protobuf==7.36.2
90
+ psutil==7.2.2
91
+ py3langid==0.2.2
92
+ pycparser==3.11
93
+ pydantic==2.14.0
94
+ pydantic_core==2.50.0
95
+ Pygments==2.21.0
96
+ pykakasi==2.3.0
97
+ pyluwen==0.10.0
98
+ pyparsing==3.3.3
99
+ pypinyin==0.55.0
100
+ python-dateutil==2.9.0.post0
101
+ python-discovery==1.6.2
102
+ PyYAML==6.0.3
103
+ regex==2026.9.29
104
+ requests==2.34.2
105
+ rich==15.0.0
106
+ safetensors==0.8.0
107
+ scikit-learn==1.9.1
108
+ scipy==1.18.1
109
+ seaborn==0.13.2
110
+ setuptools==80.10.2
111
+ setuptools-scm==8.1.0
112
+ shellingham==1.5.4
113
+ six==1.17.0
114
+ sniffio==1.3.1
115
+ soundfile==0.14.0
116
+ soxr==1.1.0
117
+ starlette==1.7.0
118
+ sympy==1.14.0
119
+ textual==8.2.8
120
+ threadpoolctl==3.7.0
121
+ tokenizers==0.23.3
122
+ tomli==2.5.0
123
+ torch==2.14.1+cpu
124
+ torch-einops-utils==0.1.36
125
+ torchaudio==2.11.0+cpu
126
+ torchdiffeq==0.2.5
127
+ torchvision==0.29.1+cpu
128
+ tqdm==4.70.1
129
+ transformers==5.19.0
130
+ truststore==0.10.4
131
+ tt-smi==6.7.0
132
+ tt-tools-common==1.6.0
133
+ tt-umd==0.9.12
134
+ typeguard==4.6.0
135
+ typer==0.27.3
136
+ typing-inspection==0.4.4
137
+ typing_extensions==4.16.0
138
+ Unidecode==1.4.0
139
+ urllib3==2.8.0
140
+ uvicorn==0.54.0
141
+ virtualenv==21.14.6
142
+ wandb==0.30.0
143
+ wcwidth==0.9.2
144
+ wheel==0.48.0
145
+ wrapt==2.5.0
146
+ x-clip==0.14.4
147
+ x-transformers==2.31.7
148
+ xxhash==4.0.1
tt_kernel_manifest.json ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "schema_version": "5.1",
3
+ "name": "diffrhythm-1.2-p100a",
4
+ "tt_metal_version": "0.0.0.dev0",
5
+ "arch": "blackhole",
6
+ "device_count": 1,
7
+ "producer": {
8
+ "tt_kernel_version": "0.1.0",
9
+ "created_at": "2026-10-10T03:07:37.410607+00:00",
10
+ "hostname": ""
11
+ },
12
+ "weights": {
13
+ "repo_id": "ASLP-lab/DiffRhythm-1_2-full",
14
+ "revision": null,
15
+ "allow_patterns": null,
16
+ "ignore_patterns": null,
17
+ "repo_type": "model"
18
+ },
19
+ "mesh": null,
20
+ "entrypoint": null,
21
+ "resources": null,
22
+ "capabilities": null,
23
+ "env": {},
24
+ "bundled": null,
25
+ "deps": null,
26
+ "container": {
27
+ "image": {
28
+ "registry": "hf",
29
+ "repository": "diffrhythm-1.2-p100a",
30
+ "tag": "tt-model/diffrhythm-1.2-p100a:0cc125e439ef",
31
+ "digest": "sha256:0cc125e439ef6882a01bae57bece67f14ec2ad123aec1fb12e86bc50175a4b44"
32
+ },
33
+ "kind": "tt-dit-server",
34
+ "runtime": {
35
+ "app": "tt_diffrhythm.server:app",
36
+ "mesh_shape_env": "DIFFRHYTHM_MESH_SHAPE",
37
+ "packages": [
38
+ "torchaudio==2.11.0+cpu",
39
+ "torchdiffeq",
40
+ "x-transformers",
41
+ "transformers",
42
+ "librosa",
43
+ "soundfile",
44
+ "muq",
45
+ "mutagen",
46
+ "ema-pytorch",
47
+ "accelerate",
48
+ "einops",
49
+ "tqdm",
50
+ "wandb",
51
+ "scipy",
52
+ "numpy",
53
+ "huggingface_hub",
54
+ "phonemizer",
55
+ "espeakng-loader",
56
+ "pykakasi",
57
+ "jieba",
58
+ "onnxruntime",
59
+ "cn2an",
60
+ "pypinyin",
61
+ "Unidecode",
62
+ "inflect",
63
+ "py3langid==0.2.2"
64
+ ],
65
+ "lock": "requirements.lock"
66
+ },
67
+ "serve": {
68
+ "hardware": "p100a",
69
+ "mesh_device": "P100",
70
+ "port": 8000,
71
+ "max_model_len": null,
72
+ "max_num_seqs": null,
73
+ "block_size": null,
74
+ "server_timeout": null,
75
+ "capabilities": null,
76
+ "additional_config": {},
77
+ "args": [],
78
+ "env": {
79
+ "ARCH_NAME": "blackhole",
80
+ "TT_METAL_OPERATION_TIMEOUT_SECONDS": "600.0"
81
+ }
82
+ },
83
+ "serve_profiles": [
84
+ {
85
+ "hardware": null,
86
+ "mesh_device": null,
87
+ "port": null,
88
+ "max_model_len": null,
89
+ "max_num_seqs": null,
90
+ "block_size": null,
91
+ "server_timeout": null,
92
+ "capabilities": null,
93
+ "additional_config": {},
94
+ "args": [],
95
+ "env": {},
96
+ "name": "default",
97
+ "description": null
98
+ }
99
+ ],
100
+ "default_profile": null,
101
+ "code_dir": "code",
102
+ "verify": [
103
+ "import tt_diffrhythm.server as s; assert s.app",
104
+ "from tt_diffrhythm.local import LocalWorker; assert LocalWorker",
105
+ "import tt_diffrhythm.server as s; s._enter_checkout(); import infer_utils, model; assert model.CFM and infer_utils.load_checkpoint",
106
+ "from tt_diffrhythm.server import _enter_checkout; _enter_checkout(); import os; assert os.path.isfile('infer/example/vocal.npy'), 'the negative style prompt did not ship'",
107
+ "from tt_diffrhythm.server import _enter_checkout; _enter_checkout(); import json; assert json.load(open('config/diffrhythm-1b.json'))['model']",
108
+ "from tt_diffrhythm.server import _enter_checkout; _enter_checkout(); from infer_utils import CNENTokenizer; assert CNENTokenizer().encode('a quiet morning')",
109
+ "from tt_diffrhythm.server import _point_phonemizer_at_a_bundled_espeak as fix; fix(); from phonemizer.backend import EspeakBackend; assert EspeakBackend('en-us').phonemize(['hello'])[0].strip()"
110
+ ],
111
+ "built": {
112
+ "image": "tt-model/diffrhythm-1.2-p100a:0cc125e439ef",
113
+ "repo": "sunandbean/diffrhythm-1.2-p100a",
114
+ "tt_model_version": "0.1.0",
115
+ "created_at": "2026-10-10T03:02:08+00:00",
116
+ "tt_metal": {
117
+ "sha": "58648d44443223041148125843ca7c7d4780463e",
118
+ "describe": null,
119
+ "dirty": false,
120
+ "scm_version": "0.0.0.dev0",
121
+ "mode": "git",
122
+ "remote": "https://github.com/tenstorrent/tt-metal",
123
+ "branch": null,
124
+ "pushed": null
125
+ },
126
+ "code_sha256": "322cdea4755e9a8117adc934aed2edfd1e3a7631fbbe9949bdbdc46c077cf184",
127
+ "image_digest": "sha256:0cc125e439ef6882a01bae57bece67f14ec2ad123aec1fb12e86bc50175a4b44"
128
+ }
129
+ }
130
+ }