File size: 10,656 Bytes
3ea3da7
ac12f15
 
 
 
f0c91ec
ac12f15
 
 
 
cb66b66
 
3ea3da7
 
ac12f15
 
 
 
 
 
 
 
bd260d4
99323da
3ea3da7
ac12f15
 
 
 
 
b35ab1f
 
c7a8520
 
 
 
99323da
 
 
 
 
bd260d4
 
 
 
ac12f15
 
9a2611f
 
ac12f15
 
 
 
3ea3da7
ac12f15
 
 
 
3ea3da7
ac12f15
9a2611f
ac12f15
 
 
 
b35ab1f
c7a8520
99323da
ac12f15
bd260d4
ac12f15
 
 
 
 
 
 
 
 
 
3ea3da7
 
ac12f15
f0c91ec
 
 
 
 
 
 
cb66b66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3ea3da7
cb66b66
ac12f15
 
 
 
 
 
f0c91ec
 
ac12f15
 
 
 
b35ab1f
 
c7a8520
 
99323da
 
bd260d4
 
cb66b66
 
2304838
 
ac12f15
dec8208
ac12f15
 
 
 
 
 
bd260d4
c7a8520
99323da
bd260d4
2304838
 
 
9a2611f
cb66b66
 
f0c91ec
 
 
 
cb66b66
ac12f15
cb66b66
ac12f15
cb66b66
 
 
 
 
 
9a2611f
ac12f15
 
 
 
 
 
3ea3da7
ac12f15
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
#!/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()