WaveCut's picture
Runtime update: released native 1.0.5, memory flags (VAE tile 256, lazy KV handoff), verified-digest cache, background VAE load, chunked INT8 head quantization, compile paths only with --warm-compile; measurements refreshed
cb66b66 verified
Raw History Blame Contribute Delete
10.7 kB
#!/usr/bin/env python3
"""Generate Russian music with the measured YuE2 packed inference profiles."""
import argparse
import dataclasses
import json
import math
import hashlib
import os
import sys
import time
PROCESS_T0 = time.time() # module import; the interpreter launch itself is not included
from pathlib import Path
SRC = Path(__file__).absolute().parent
ROOT = SRC.parent
sys.path.insert(0, str(SRC / "runtime/kernels/base"))
sys.path.insert(0, str(SRC / "runtime"))
PROFILES = ("reference", "fused", "fast", "memory")
def load_pipeline(*, vae=None, low_memory=False, offload_ar=False, progress=True,
profile=None, ode_steps=None, vae_precision=None, fuse_rms_quant=False,
capture_sampling=None, kv_cache_dtype="bf16", int8_attention=None):
import torch
profile = profile or ("reference" if low_memory else "fast")
if profile not in PROFILES:
raise ValueError(f"Unknown profile {profile!r}")
if low_memory and profile != "reference":
raise ValueError("Unfused --low-memory requires profile=reference; use profile=memory for staged experts")
if fuse_rms_quant and profile not in {"fast", "memory"}:
raise ValueError("RMS/activation fusion requires profile=fast or profile=memory")
if kv_cache_dtype not in {"bf16", "int8"}:
raise ValueError("kv_cache_dtype must be bf16 or int8")
if kv_cache_dtype == "int8" and (profile not in {"fast", "memory"} or not fuse_rms_quant):
raise ValueError("INT8 KV requires profile=fast or memory and fuse_rms_quant=True")
if int8_attention is not None and int8_attention not in {"reference", "ordered", "subwarp"}:
raise ValueError("int8_attention must be reference, ordered, subwarp, or None")
if int8_attention is not None and kv_cache_dtype != "int8":
raise ValueError("int8_attention requires kv_cache_dtype=int8")
int8_attention = (int8_attention or "ordered") if kv_cache_dtype == "int8" else None
if capture_sampling is not None and type(capture_sampling) is not bool:
raise ValueError("capture_sampling must be a boolean or None")
if capture_sampling and profile not in {"fast", "memory"}:
raise ValueError("Captured sampling requires profile=fast or profile=memory")
if not torch.cuda.is_available():
raise RuntimeError("This release requires an NVIDIA CUDA GPU.")
if torch.cuda.get_device_capability() not in {(8, 9), (12, 0)}:
raise RuntimeError("This runtime ships kernels for SM89 (Ada/RTX 40) and SM120 (Blackwell consumer); other GPUs need a rebuild.")
if ode_steps is not None and (isinstance(ode_steps, bool) or not isinstance(ode_steps, int) or ode_steps < 1):
raise ValueError("ode_steps must be a positive integer")
if vae_precision is not None and vae_precision not in {"fp32", "fp16", "bf16"}:
raise ValueError("vae_precision must be fp32, fp16, or bf16")
if vae is None:
from huggingface_hub import snapshot_download
lock = json.loads((ROOT / "source-lock.json").read_text())
vae = snapshot_download("m-a-p/YuE2-Vae", revision=lock["m-a-p/YuE2-Vae"],
allow_patterns=["*.json", "*.safetensors", "*.py", "LICENSE", "licenses/*", "THIRD_PARTY_NOTICES.md"])
from yue2_orbit import OrbitPipeline
pipe = OrbitPipeline.from_pretrained(str(ROOT), vae=vae, device="cuda",
memory_budget_gib=18, vae_core_frames=512,
offload_ar=offload_ar or profile == "memory", progress=progress)
pipe.fused = not low_memory
pipe.trim_cache = True
pipe.profile = profile
pipe.fuse_rms_quant = bool(fuse_rms_quant)
pipe.kv_cache_dtype = kv_cache_dtype
pipe.int8_attention = int8_attention
fast = profile in {"fast", "memory"}
pipe.capture_sampling = fast if capture_sampling is None else capture_sampling
pipe.generation_config = dataclasses.replace(pipe.generation_config,
ode_steps=ode_steps if ode_steps is not None else 8 if fast else 32)
precision = vae_precision or ("fp16" if fast else "fp32")
pipe.vae_compute_dtype = {"fp32": None, "fp16": torch.float16, "bf16": torch.bfloat16}[precision]
# Include packed adapters and binaries, not only the upstream yue2 package.
files = sorted(p for p in SRC.rglob("*") if p.is_file() and
"__pycache__" not in p.parts and "kernel-source" not in p.parts and
(p.suffix in {".py", ".so", ".whl"} or p.name == "requirements.txt"))
hashes = {str(p.relative_to(SRC)): hashlib.sha256(p.read_bytes()).hexdigest() for p in files}
pipe.runtime_bundle_sha256 = hashlib.sha256(json.dumps(hashes, sort_keys=True).encode()).hexdigest()
return pipe
def _guidance_scale(value):
value = float(value)
if not math.isfinite(value) or not 0 <= value <= 20:
raise argparse.ArgumentTypeError("cfg scale must be finite and in [0,20]")
return value
class _NvmlPeak:
"""Whole-process NVML peak of this GPU's used memory (20 ms sampling), for the report."""
def __init__(self):
import threading
self.peak = 0; self._stop = threading.Event(); self._thread = None
try:
import pynvml
pynvml.nvmlInit(); self._h = pynvml.nvmlDeviceGetHandleByIndex(0); self._nv = pynvml
except Exception:
self._nv = None
return
self._thread = threading.Thread(target=self._run, daemon=True); self._thread.start()
def _run(self):
while not self._stop.wait(0.02):
try:
self.peak = max(self.peak, self._nv.nvmlDeviceGetMemoryInfo(self._h).used)
except Exception:
return
def stop(self):
self._stop.set()
if self._thread is not None:
self._thread.join(timeout=1)
return self.peak / 2**30 if self._nv is not None else None
def main():
nvml_peak = _NvmlPeak()
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--prompt", default=str(ROOT / "examples/prompts/default-ru.json"))
parser.add_argument("--output", default="song")
parser.add_argument("--vae")
parser.add_argument("--profile", choices=PROFILES)
parser.add_argument("--ode-steps", type=int)
parser.add_argument("--cfg-scale", type=_guidance_scale,
help="Semantic guidance scale; 1 uses one decode branch")
parser.add_argument("--vae-precision", choices=("fp32", "fp16", "bf16"))
parser.add_argument("--allocator", choices=("native", "expandable"))
parser.add_argument("--low-memory", action="store_true")
parser.add_argument("--offload-ar", action="store_true")
parser.add_argument("--fuse-rms-quant", action="store_true",
help="Fuse RMSNorm into activation quantization; faster experimental arithmetic")
parser.add_argument("--kv-cache-dtype", choices=["bf16", "int8"], default="bf16",
help="AR KV cache precision; int8 requires --fuse-rms-quant and fast/memory")
parser.add_argument("--int8-attention", choices=["reference", "ordered", "subwarp"],
help="INT8 decode: ordered preserves previous arithmetic (default); subwarp changes rounding; reference uses the previous kernel")
parser.add_argument("--no-captured-sampling", action="store_true",
help="Use the earlier eager sampler for comparison")
parser.add_argument("--warm-compile", action="store_true",
help="keep the torch.compile paths (VAE decoder, NAR pointwise chains); pays off only when one process serves several songs")
parser.add_argument("--mode", choices=["off", "exact", "fast", "lowmem", "turbo"], default="exact",
help="off: reference decode path; exact: same sampled tokens, faster pipeline; fast: + INT8 output heads and NAR KV handoff; lowmem: + INT8 embeddings; turbo: W4A4 output heads")
args = parser.parse_args()
allocator = args.allocator or ("expandable" if (args.profile or "fast") in {"fast", "memory"} and not args.low_memory else None)
if allocator is not None:
os.environ["PYTORCH_ALLOC_CONF"] = "expandable_segments:True" if allocator == "expandable" else "backend:native"
request = json.loads(Path(args.prompt).read_text())
out = Path(args.output)
out.mkdir(parents=True, exist_ok=True)
with load_pipeline(vae=args.vae, low_memory=args.low_memory, offload_ar=args.offload_ar,
profile=args.profile, ode_steps=args.ode_steps, vae_precision=args.vae_precision,
fuse_rms_quant=args.fuse_rms_quant, kv_cache_dtype=args.kv_cache_dtype,
int8_attention=args.int8_attention,
capture_sampling=False if args.no_captured_sampling else None) as pipe:
mode_flags = []
if args.mode != "off":
from fast_ops import apply_mode
model = pipe._load_model()
mode_flags = apply_mode(pipe, model, args.mode, warm=args.warm_compile)
gen_start = time.perf_counter()
generation = {k: request[k] for k in ("style", "lyrics", "seed", "cot") if k in request}
if args.cfg_scale is not None:
generation["cfg_scale"] = args.cfg_scale
song = pipe(**generation)
t_save = time.perf_counter()
song.save_artifacts(out)
t_artifacts = time.perf_counter() - t_save
song.save(out / "audio.wav")
t_wav = time.perf_counter() - t_save - t_artifacts
stage_peaks = [v.get("peak_gib", 0) for v in song.timing.get("stages", {}).get("memory", {}).values()]
report = dict(seconds_including_load=time.time() - PROCESS_T0, generation_seconds=time.perf_counter() - gen_start,
mode=args.mode, mode_flags=mode_flags,
nvml_process_peak_gib=nvml_peak.stop(), torch_process_peak_gib=max(stage_peaks) if stage_peaks else None,
save_artifacts_seconds=round(t_artifacts, 3), save_wav_seconds=round(t_wav, 3),
timing=song.timing,
audio_seconds=len(song.audio) / song.sample_rate,
packed_runtime=pipe.runtime_report(), truncated=song.truncated,
allocator_config=os.environ.get("PYTORCH_ALLOC_CONF", os.environ.get("PYTORCH_CUDA_ALLOC_CONF")))
(out / "release-run.json").write_text(json.dumps(report, indent=2))
print(json.dumps(report), flush=True)
if __name__ == "__main__":
main()