sunandbean's picture
Add files using upload-large-folder tool
cfbcb75 verified
Raw History Blame Contribute Delete
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}")