Download code/tt_diffrhythm/local.py from sunandbean/diffrhythm-1.2-p100a: direct link, hf CLI and curl.
- Browser
- Download file 6.59 kB
-
https://huggingface.co/sunandbean/diffrhythm-1.2-p100a/resolve/main/code/tt_diffrhythm/local.py
- Command line
-
hf download hf://sunandbean/diffrhythm-1.2-p100a/code/tt_diffrhythm/local.py
-
curl -L -o local.py https://huggingface.co/sunandbean/diffrhythm-1.2-p100a/resolve/main/code/tt_diffrhythm/local.py
6.59 kB
| # SPDX-License-Identifier: Apache-2.0 | |
| """The card in THIS process, behind the same `.call(msg)` surface `TTWorker` gives. | |
| `RemoteDiffRhythmDiT` and `RemoteVae` reach the card through exactly one method -- `worker.call(msg)` | |
| -- so swapping the transport is enough to run everything in one process: the shims, their window | |
| arithmetic and their drop-in signatures are reused unchanged rather than reimplemented here. | |
| Why there are two transports at all: DiffRhythm resolves `torch 2.10.0+cu128` and `numpy>=2`, while | |
| ttnn's extension modules are built against tt-metal's own `torch 2.11.0+cpu` and `numpy 1.x`. Two | |
| environments cannot be one process, so the deployment this port came from ran DiffRhythm on the host | |
| and the card in a container, joined by a Unix socket (`remote.py` / `worker.py`). A container built | |
| for this model alone has no such split -- one environment satisfies both -- and then the socket is | |
| pure cost. Use `LocalWorker` there, `TTWorker` when the two environments must stay apart. | |
| from tt_diffrhythm import LocalWorker, RemoteDiffRhythmDiT, RemoteVae | |
| worker = LocalWorker(cfm_model_pt, vae=vae_model_pt).start() | |
| cfm.transformer = RemoteDiffRhythmDiT(worker, cfm.transformer) | |
| waveform = RemoteVae(worker, upsample=worker.upsample)(latent) | |
| ... | |
| worker.close() | |
| """ | |
| from __future__ import annotations | |
| import time | |
| from pathlib import Path | |
| from typing import Optional | |
| import torch | |
| from . import diffrhythm_dit, oobleck_vae, wire | |
| from .shapes import VAE_WINDOWS | |
| from .worker import DiffRhythm | |
| MODELS = {"diffrhythm": DiffRhythm} | |
| class LocalWorker: | |
| """`TTWorker`'s surface, answered in-process. `checkpoint` is cfm_model.pt and `vae` is | |
| vae_model.pt; both are the files DiffRhythm's own Hugging Face cache holds.""" | |
| def __init__(self, checkpoint, vae, model: str = "diffrhythm", warm: bool = True): | |
| self.checkpoint = Path(checkpoint) | |
| self.vae_path = Path(vae) | |
| self.model_name = model | |
| self.warm = warm | |
| self.dev = None | |
| self.model = self.vae = None | |
| self.song = None | |
| self.stats: dict = {} | |
| self.failed = False | |
| self.upsample: Optional[int] = None | |
| # ------------------------------------------------------------------ lifecycle | |
| def start(self) -> "LocalWorker": | |
| started = time.monotonic() | |
| self.dev = diffrhythm_dit.open_device() | |
| try: | |
| self.model = MODELS[self.model_name](self.dev, str(self.checkpoint)) | |
| self.vae = oobleck_vae.OobleckTT(self.dev, str(self.vae_path)) | |
| self.upsample = self.vae.upsample | |
| if self.warm: | |
| # Same reason worker.main() warms here: built now, the VAE's conv kernels compile | |
| # in one device state and every later decode reuses the in-process programs. Built | |
| # after a song's DiT steps instead, the state -- and so the kernels -- depends on | |
| # the song length and recompiles for 20-40 s. | |
| t0 = time.monotonic() | |
| for window in VAE_WINDOWS: | |
| self.vae.decode(torch.zeros(64, window)) | |
| self.stats["vae_warm_s"] = round(time.monotonic() - t0, 2) | |
| self.stats["load_s"] = round(time.monotonic() - started, 2) | |
| self.stats["dram"] = diffrhythm_dit.dram_stats(self.dev) | |
| except BaseException: | |
| self.close() | |
| raise | |
| return self | |
| def wait_ready(self, timeout_s: float = 0.0) -> dict: | |
| """`TTWorker.wait_ready`'s shape. Loading already finished in `start`, so this only reports.""" | |
| if self.model is None: | |
| raise RuntimeError("LocalWorker.start() has not run") | |
| return {"op": "ready", "model": self.model_name, "upsample": self.upsample, **self.stats} | |
| def died(self) -> bool: | |
| """There is no child process to lose; a failure here raised in the caller's own stack.""" | |
| return False | |
| def close(self) -> None: | |
| if self.song is not None and self.model is not None: | |
| self.model.dit.release(self.song) | |
| self.song = None | |
| self.model = self.vae = None | |
| if self.dev is not None: | |
| diffrhythm_dit.close_device(self.dev) | |
| self.dev = None | |
| def __enter__(self): | |
| return self.start() | |
| def __exit__(self, *exc): | |
| self.close() | |
| # ------------------------------------------------------------------ calls | |
| def call(self, msg: dict) -> dict: | |
| """`worker.py:serve`'s dispatch with the socket taken out and nothing else changed. | |
| Messages still carry `wire`-encoded tensors even though both ends are now one process. | |
| That is deliberate: it is what lets `RemoteDiffRhythmDiT` and `RemoteVae` run against this | |
| worker unmodified, and the encoding is a float32 copy of a few MB against card work | |
| measured in seconds.""" | |
| if self.model is None: | |
| raise RuntimeError("LocalWorker.start() has not run") | |
| op = msg.get("op") | |
| started = time.monotonic() | |
| try: | |
| if op == "prepare": | |
| if self.song is not None: | |
| self.model.dit.release(self.song) | |
| self.song = None | |
| self.song, geometry = self.model.prepare(msg) | |
| return {"op": "prepared", "seconds": time.monotonic() - started, "geometry": geometry} | |
| if op == "forward": | |
| if self.song is None: | |
| raise RuntimeError("forward before prepare") | |
| v = self.model.forward(msg, self.song) | |
| return {"op": "velocity", "v": wire.encode(v), "seconds": time.monotonic() - started} | |
| if op == "vae": | |
| if self.song is not None: | |
| # Sampling is over; freeing the song's tensors first gives the VAE the same | |
| # device allocation state for every song length, so its compiled conv kernels | |
| # are reused instead of recompiled. | |
| self.model.dit.release(self.song) | |
| self.song = None | |
| audio = self.vae.decode(wire.decode(msg["z"])) | |
| return {"op": "audio", "audio": wire.encode(audio), "seconds": time.monotonic() - started} | |
| if op == "close": | |
| self.close() | |
| return {"op": "closed", "seconds": time.monotonic() - started} | |
| except BaseException: | |
| self.failed = True # the card may be unhealthy, same contract as TTWorker | |
| raise | |
| raise RuntimeError(f"unknown op {op!r}") | |