maya2-native / maya3 /engine.py
multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
6784f71 verified
Raw History Blame Contribute Delete
35.1 kB
"""Maya3 streaming engine: one GPU-owner thread, continuous batching over seats, everything hot CUDA-graphed.
Per tick (one 80 ms Mimi frame for every active seat):
1. admit : new requests get a slot; the voice's cached prefix KV (reference span, constant per voice) is copied into
the slot and only the target text is prefilled -- INSIDE the same graphed backbone step as the decode
rows (inline prefill, serve/fast_backbone.StaticBackbone.step_mixed). Plain requests (no prefix) are
prefilled entirely inline. So a new request's first frame is produced in its admission tick.
2. cb0 : controlled stop (tau 0.35 / hazard 0.6 / argmax AEOS, min frames) + cb0 sampling on the GPU, with the
per-seat AEOS bias of the EOS-boost loop guard.
3. depth : 31 levels in one CUDA graph (serve/fast_depth.FastDepth), fp16 by default (closer to fp32 greedy than bf16).
4. mimi : one-frame static-state Mimi decode graph (serve/fast_codec.StaticMimi) -> int16 PCM on the GPU.
5. ONE device->host copy (stop codes, cb0, PCM) and ONE sync; then guards + delivery on the host.
Differences from the offline reference decoder used for the quality evals (FastBatch): prefix-KV reuse + inline
prefill instead of a full eager HF prefill per request, vectorised loop guard, one sync per tick, PCM as int16.
"""
from __future__ import annotations
import collections
import dataclasses
import gc
import itertools
import os
import sys
import threading
import time
from pathlib import Path
from typing import Callable, Optional
import numpy as np
import torch
ROOT = Path(__file__).resolve().parents[1]
for p in (ROOT, ROOT / "scripts"):
if str(p) not in sys.path:
sys.path.insert(0, str(p))
os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
from maya3 import guards as G # noqa: E402
from maya3.prompt import DEFAULT_BUILDER, MODES, Voice, VoiceRegistry # noqa: E402
SR = 24000
FRAME = 1920
BUCKETS = (1, 2, 4, 8, 16, 24, 32, 48, 64, 80, 96, 112, 128, 160, 192, 224, 256)
ROW_BUCKETS = (1, 2, 4, 8, 16, 24, 32, 48, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 448, 512)
REASONS = {1: "aeos", 2: "aeos_argmax", 3: "aeos_tau", 4: "aeos_hazard"}
@dataclasses.dataclass
class EngineConfig:
max_seats: int = 16
capacity: int = 2048 # backbone KV tokens per seat (prompt + generated frames)
depth_dtype: str = "fp16" # fp16 | bf16 | fp32
temp: float = 0.7 # depth sampler
top_k: int = 64
# cb0 sampler defaults = best setting of a sampler sweep (presence penalty 0.8 over the last 12 frames +
# temperature 0.9; best stability objective, raw-text no-stop rate 0.79 -> 0.07 %)
cb0_temp: float = 0.9
cb0_top_k: int = 64
cb0_top_p: float = 1.0
cb0_rep: float = 0.8 # presence penalty: logit -= rep for every code seen in the last cb0_rep_win frames
cb0_rep_win: int = 12
hard_cap: int = 1500 # frames (120 s): absolute per-chunk ceiling
max_inline_rows: int = 384 # prefill rows folded into one tick (protects running seats' tick time)
max_admit_per_tick: int = 8
busy_frac: float = 0.75 # above this seat occupancy, admit at most busy_admit per tick (smooths tick time)
busy_admit: int = 4
seed: int = 20261024
guards: G.GuardConfig = dataclasses.field(default_factory=G.GuardConfig)
gap_s: float = 0.15 # silence between the text pieces of one request
chunk_s: float = 20.0 # long text is split into pieces of about this many seconds
use_graphs: bool = True
fuse_proj: bool = False # backbone: one GEMM for q|k|v and one for gate|up per layer
fuse_rope: bool = True # backbone: fused q/k RMSNorm + RoPE + KV-cache write (Triton; implies fuse_proj)
fused_norm: bool = False # backbone: F.rms_norm instead of the HF RMSNorm module
fused_depth: bool = True # depth: Triton add+LayerNorm and one-kernel top-k sampler
compile_depth: bool = False # Inductor-fuse the depth frame body before capture (measured: no gain; off)
free_reference_depth: bool = True # drop the fp32 reference depth stack once FastDepth holds its own copy (-2.2 GB)
verify_prefix: bool = False # debug: assert the cached prefix embedding equals the recomputed one
class Request:
"""One synthesis request. on_event(kind, payload) is called from the engine thread:
('audio', np.int16 [n]) PCM 24 kHz mono, one or more frames
('end', dict) reason, frames, audio_s, ttfa_s, pieces, stops
('error', dict) code, error
It must be cheap and non-blocking (put into a queue)."""
_ids = itertools.count()
def __init__(self, pieces, lang, voice: Voice, on_event, max_frames=None, rid=None, sampling=None):
self.id = rid or f"r{next(self._ids)}"
self.sampling = dict(sampling or {})
self.pieces = list(pieces)
self.lang = lang
self.voice = voice
self.on_event = on_event
self.max_frames = max_frames
self.cancelled = False
self.t_submit = time.perf_counter()
self.t_first = None
self.k = 0 # current piece
self.frames = 0
self.stops = []
self.done = False
def cancel(self):
self.cancelled = True
def _check_sampling(sp):
if not sp:
return {}
allowed = {"seed", "temperature", "top_k", "top_p", "repetition_penalty"}
bad = set(sp) - allowed
if bad:
raise ValueError(f"unknown sampling keys {sorted(bad)}; allowed {sorted(allowed)}")
out = {k: v for k, v in sp.items() if v is not None}
if "temperature" in out and not 0.05 <= float(out["temperature"]) <= 2.0:
raise ValueError("temperature must be in [0.05, 2.0]")
if "top_k" in out and not 1 <= int(out["top_k"]) <= 2049:
raise ValueError("top_k must be in [1, 2049]")
if "top_p" in out and not 0.0 < float(out["top_p"]) <= 1.0:
raise ValueError("top_p must be in (0, 1]")
if "repetition_penalty" in out and not 0.0 <= float(out["repetition_penalty"]) <= 5.0:
raise ValueError("repetition_penalty must be in [0, 5]")
if "seed" in out:
out["seed"] = int(out["seed"])
return out
class _Seat:
__slots__ = ("req", "slot", "frames", "cap", "loose_cap", "minimum", "suspend", "boost_at", "n_boost", "hist", "new")
def __init__(self, req, slot, cap, loose_cap, minimum, suspend, hist):
self.req, self.slot, self.cap, self.loose_cap, self.minimum, self.suspend = req, slot, cap, loose_cap, minimum, suspend
self.frames = 0
self.boost_at = None
self.n_boost = 0
self.hist = np.full(hist, -1, np.int64)
self.new = True
class Engine:
def __init__(self, ckpt, backbone_dir, mimi_dir, config: EngineConfig = None, device="cuda", registry=None,
builder=DEFAULT_BUILDER, model_config=None, log=print):
from transformers import MimiModel
from tts.data.packing_v2 import PackerV2
from tts.model.load_s3 import load_model_for_eval
from scripts import s2_serve_controls as S
self.S = S
self.cfg = cfg = config or EngineConfig()
self.device = device
self.log = log
torch.backends.cuda.enable_cudnn_sdp(False)
torch.backends.cuda.matmul.allow_tf32 = False
t0 = time.perf_counter()
mcfg = {"model": {"backbone": str(backbone_dir), "attn": "sdpa"}, "data": {"max_tokens": 8192}}
self.model, self.provenance = load_model_for_eval(str(ckpt), device=device, dtype=torch.bfloat16, cfg=mcfg,
config=str(model_config or ROOT / "configs/s3_fresh_v1.yaml"),
amortize_frac=1.0, require_anchor=True, verbose=False)
self.model.depth.float()
self.packer = PackerV2(self.model.backbone.tokenizer, self.model.backbone.token_ids, max_tokens=8192)
self.prior = S.load_prior(str(ROOT / "configs/text_frames_prior.json"))
self.dur = G.DurationPrior()
self.mimi = MimiModel.from_pretrained(str(mimi_dir)).to(device).eval()
self.load_s = time.perf_counter() - t0
self.registry = registry or VoiceRegistry()
self.builder = builder
self._build_fast()
self._pending = collections.deque()
self._calls = collections.deque() # callables run on the engine thread between ticks (voice prep, encodes)
self._cv = threading.Condition()
self._stop = False
self._thread = None
self.stats = {"ticks": 0, "admitted": 0, "completed": 0, "prefix_hits": 0, "prefix_misses": 0, "fallback_prefill": 0}
self.tick_ms = collections.deque(maxlen=20000)
self.phase_ms = {}
self.failed = None
# ------------------------------------------------------------------ fast components
def _build_fast(self):
from serve.fast_backbone import StaticBackbone
from serve.fast_depth import FastDepth
from serve.fast_codec import StaticMimi
cfg, dev = self.cfg, self.device
n = cfg.max_seats
buckets = tuple(b for b in BUCKETS if b < n) + (next(b for b in BUCKETS if b >= n),)
rows = tuple(b for b in ROW_BUCKETS if b <= buckets[-1] + cfg.max_inline_rows)
rows = rows + tuple(b for b in ROW_BUCKETS if b > rows[-1])[:1]
self.buckets, self.row_buckets, self.max_rows = buckets, rows, rows[-1]
ctx = tuple(c for c in (256, 512, 768, 1024, 1536, 2048, 3072) if c < cfg.capacity) + (cfg.capacity,)
dt = {"bf16": torch.bfloat16, "fp16": torch.float16, "fp32": torch.float32}[cfg.depth_dtype]
self.backbone = StaticBackbone(self.model.backbone, n, capacity=cfg.capacity, batch_buckets=rows, ctx_buckets=ctx,
attention="triton", use_graphs=cfg.use_graphs,
fuse_proj=cfg.fuse_proj, fused_norm=cfg.fused_norm, fuse_rope=cfg.fuse_rope)
if cfg.compile_depth:
import torch._dynamo as _dyn
_dyn.config.cache_size_limit = max(64, _dyn.config.cache_size_limit)
self.depth = FastDepth(self.model, n, dtype=dt, batch_buckets=buckets, seed=cfg.seed + 1, temp=cfg.temp, top_k=cfg.top_k,
compile=cfg.compile_depth, fused=cfg.fused_depth)
self.codec = StaticMimi(self.mimi, n, batch_buckets=buckets)
if cfg.free_reference_depth and getattr(self.model.depth, "groups", None) is not None:
d = self.model.depth # only spk_proj_* / null_mem are used after this (speaker memory); FastDepth has its own copy
for name in ("groups", "cross", "heads", "code_lo", "code_up", "in_proj", "level_emb", "norm"):
if hasattr(d, name):
setattr(d, name, None)
torch.cuda.empty_cache()
m = n + 1
D = self.model.backbone.d_model
self.h = torch.zeros(m, D, device=dev, dtype=torch.bfloat16)
self.feedback = torch.zeros_like(self.h)
self.frames_t = torch.zeros(m, device=dev, dtype=torch.long)
self.min_t = torch.zeros(m, device=dev, dtype=torch.long)
self.hazard_t = torch.zeros(m, device=dev, dtype=torch.float32)
self.aeos_bias = torch.zeros(m, device=dev, dtype=torch.float32)
# per-slot sampler settings and RNG stream (per-request seed => reproducible output at a fixed batch composition)
self.temp_t = torch.full((m,), cfg.cb0_temp, device=dev, dtype=torch.float32)
self.topk_t = torch.full((m,), cfg.cb0_top_k, device=dev, dtype=torch.long)
self.topp_t = torch.full((m,), cfg.cb0_top_p, device=dev, dtype=torch.float32)
self.rep_t = torch.full((m,), cfg.cb0_rep, device=dev, dtype=torch.float32)
self.seed_t = torch.zeros(m, device=dev, dtype=torch.int32)
self.ctr_t = torch.zeros(m, device=dev, dtype=torch.int32)
self.hist_t = torch.full((m, max(1, cfg.cb0_rep_win)), -1, device=dev, dtype=torch.long)
import random as _random
self._seed_rng = _random.Random(cfg.seed)
self.generator = torch.Generator(device=dev).manual_seed(cfg.seed)
self.free = list(range(n))[::-1]
self.active = []
self._any_rep = False # some active request overrides the presence penalty (host-side flag, no sync)
self._any_top_p = cfg.cb0_top_p < 1.0
# one pinned host buffer per tick: [B, 2 + FRAME/4] int64 view = stop code, cb0, PCM int16 packed
self._host = torch.empty(n, 2 + FRAME // 4, dtype=torch.int64).pin_memory()
self._evt = torch.cuda.Event()
self.warm = False
def warmup(self):
t0 = time.perf_counter()
self.backbone.warmup()
self.depth.warmup()
self.codec.warmup()
torch.cuda.synchronize()
# exercise the admission paths once (prefix KV build, inline prefill, cb0 kernels)
v = self.registry.get("plain")
for _ in range(2):
done = threading.Event()
r = Request(["Hello, this is a warmup."], "en", v, lambda k, p, d=done: d.set() if k != "audio" else None, max_frames=3)
self._pending.append(r)
while self._pending or self.active:
self._tick()
gc.collect()
gc.freeze() # long-lived objects (model, graphs) leave the GC's scan set: no multi-ms pauses
g0, g1, g2 = gc.get_threshold()
gc.set_threshold(max(g0, 50000), g1, g2)
self.warm = True
self.warmup_s = time.perf_counter() - t0
return self.warmup_s
# ------------------------------------------------------------------ voices
@torch.inference_mode()
def prepare_voice(self, voice: Voice):
"""Pack the voice's prefix, compute speaker memory and the prefix backbone KV (once per voice)."""
with voice.lock:
if voice.mem is not None:
return voice
use_icl, use_mem = MODES[voice.mode]
builder = voice.builder or self.builder
voice.prefix = builder.prefix(self.packer, voice)
voice.spk_refs = (np.asarray(voice.ecapa, np.float32).reshape(1, 192) if voice.ecapa is not None
else np.zeros((1, 192), np.float32))
refs_t = torch.as_tensor(voice.spk_refs, device=self.device)[None]
voice.mem = self.model.depth.spk_memory(refs_t, "ecapa", null_mask=torch.tensor([not use_mem], device=self.device))
voice.rmask = torch.ones(1, 1, dtype=torch.bool, device=self.device)
voice.ts = int(voice.prefix["tokens"].shape[0]) if voice.prefix is not None else 0
if voice.ts:
# embed a dummy request to get the prefix embeddings exactly as the full prompt composes them
ex = builder.example(self.packer, voice, "a", voice.ref_lang or "en")
prompt, n = self.S.prompt_from_example(ex, self.model, self.device, torch.bfloat16, 32)
emb = prompt[:, :voice.ts].to(torch.bfloat16)
h, cache = self.model.backbone(emb, use_cache=True)
voice.kv = [(l.keys[:, :, :voice.ts].clone(), l.values[:, :, :voice.ts].clone()) for l in cache.layers]
voice.emb = emb.clone()
del cache
return voice
# ------------------------------------------------------------------ public (thread-safe)
def split(self, text, lang):
return self.S.split_text(text, lang, self.prior, self.cfg.chunk_s) if self.cfg.chunk_s > 0 else [text]
def submit(self, text, lang, voice="plain", on_event=None, max_frames=None, rid=None, split=True, sampling=None):
"""sampling: optional {seed, temperature, top_k, top_p, repetition_penalty} for codebook 0 (the depth decoder keeps the
engine's temperature/top-k but draws from the request's seeded stream). Same seed + same text + same voice => same audio
when the request runs alone (batched bf16 numerics may differ slightly with batch composition)."""
sampling = _check_sampling(sampling)
if isinstance(voice, str):
voice = self.registry.get(voice)
if not isinstance(text, (list, tuple)):
if not isinstance(text, str) or not text.strip():
raise ValueError("text must be a non-empty string")
text = self.split(text.strip(), lang) if split else [text.strip()]
req = Request(text, lang, voice, on_event or (lambda k, p: None), max_frames=max_frames, rid=rid, sampling=sampling)
with self._cv:
if self.failed:
raise RuntimeError(f"engine failed: {self.failed}")
self._pending.append(req)
self._cv.notify()
return req
def call(self, fn, timeout=120):
"""Run fn() on the engine thread (between ticks) and return its result; direct call if the loop is not running."""
if self._thread is None or threading.current_thread() is self._thread:
with torch.inference_mode():
return fn()
box = {}
ev = threading.Event()
with self._cv:
self._calls.append((fn, box, ev))
self._cv.notify()
if not ev.wait(timeout):
raise TimeoutError("engine call timed out")
if "error" in box:
raise box["error"]
return box["result"]
def _run_calls(self):
while self._calls:
with self._cv:
fn, box, ev = self._calls.popleft()
try:
box["result"] = fn()
except Exception as exc:
box["error"] = exc
ev.set()
def start(self):
if self._thread is None:
if not self.warm:
self.warmup()
self._thread = threading.Thread(target=self._loop, name="maya3-engine", daemon=True)
self._thread.start()
return self
def close(self):
with self._cv:
self._stop = True
self._cv.notify()
if self._thread is not None:
self._thread.join(timeout=10)
def health(self):
recent = list(self.tick_ms)[-2000:]
rs = sorted(recent)
return {"ready": self.warm and self.failed is None, "failed": self.failed, "active": len(self.active),
"pending": len(self._pending), "max_seats": self.cfg.max_seats, **self.stats,
"tick_ms_p50": rs[len(rs) // 2] if rs else None, "tick_ms_p99": rs[int(len(rs) * .99)] if rs else None,
"phase_ms": self.phase_ms, **self._mem()}
def _mem(self):
try:
return {"mem_alloc_gb": round(torch.cuda.memory_allocated() / 1e9, 2), "mem_reserved_gb": round(torch.cuda.memory_reserved() / 1e9, 2)}
except Exception:
return {}
# ------------------------------------------------------------------ loop
def _loop(self):
try:
with torch.inference_mode():
while True:
with self._cv:
while not self._stop and not self._pending and not self.active and not self._calls:
self._cv.wait(0.5)
if self._stop:
break
self._run_calls()
self._tick()
except BaseException as exc: # pragma: no cover
import traceback
traceback.print_exc()
self.failed = f"{type(exc).__name__}: {exc}"
finally:
with self._cv:
jobs = list(self._pending) + [s.req for s in self.active]
self._pending.clear()
for r in jobs:
self._emit(r, "error", {"code": "unavailable", "error": self.failed or "engine stopped"})
self.active = []
def _emit(self, req, kind, payload):
try:
req.on_event(kind, payload)
except Exception: # a broken consumer must not kill the engine
req.cancelled = True
# ------------------------------------------------------------------ admission
def _plan(self, req):
"""Build the prompt for req's current piece. Returns (prompt_emb [1,T,D] bf16, ts, cap, loose_cap, minimum, suspend)."""
S, cfg = self.S, self.cfg
voice = req.voice
if voice.mem is None:
self.prepare_voice(voice)
text = req.pieces[req.k]
builder = voice.builder or self.builder
ex = builder.example(self.packer, voice, text, req.lang)
prompt, n = S.prompt_from_example(ex, self.model, self.device, torch.bfloat16, 32)
prompt = prompt.to(torch.bfloat16)
ef, er = S.expected_frames(self.prior, req.lang, len(text))
loose = min(cfg.hard_cap, int(2.0 * (ef + er) + 40))
cap = loose
g = cfg.guards
if g.enabled:
cap = min(cap, max(int(ef * 0.5) + 8, self.dur.cap_frames(text, req.lang, g.dur_k, g.dur_margin_s)))
if req.max_frames: # total over all pieces of the request
cap = min(cap, max(1, int(req.max_frames) - req.frames))
minimum = max(4, int(0.5 * ef))
if g.enabled and g.loop and g.aware_v2:
suspend = G.suspend_windows_v2(text, req.lang, self.dur)
else:
suspend = G.tag_windows(text, req.lang, self.dur, g.tag_hold_s) if (g.enabled and g.loop and g.tag_aware and "[" in text) else ()
return prompt, int(n), cap, loose, minimum, suspend
def _bind(self, req, cap, loose, minimum, suspend):
slot = self.free.pop()
v = req.voice
self.depth.set_speaker(slot, v.mem, v.rmask)
self.codec.reset(slot)
self.frames_t[slot] = 0
self.min_t[slot] = minimum
self.hazard_t[slot] = 0
self.aeos_bias[slot] = 0
sp, cfg = req.sampling, self.cfg
if "seed" in sp and sp["seed"] is not None: # each piece of a multi-piece request gets its own derived stream
seed = (int(sp["seed"]) * 1000003 + req.k * 7919) % (2 ** 31 - 1)
else:
seed = self._seed_rng.randrange(1, 2 ** 31 - 1)
self.seed_t[slot] = seed
self.ctr_t[slot] = 0
self.depth.row_seed[slot] = (seed * 69069 + 1) % (2 ** 31 - 1)
self.depth.row_ctr[slot] = 0
self.temp_t[slot] = float(sp.get("temperature") or cfg.cb0_temp)
self.topk_t[slot] = int(sp.get("top_k") or cfg.cb0_top_k)
self.topp_t[slot] = float(sp.get("top_p") or cfg.cb0_top_p)
self.rep_t[slot] = float(sp["repetition_penalty"] if sp.get("repetition_penalty") is not None else cfg.cb0_rep)
self.hist_t[slot] = -1
if float(sp.get("top_p") or 1.0) < 1.0:
self._any_top_p = True
if sp.get("repetition_penalty"):
self._any_rep = True
return _Seat(req, slot, cap, loose, minimum, suspend, self.cfg.guards.hist)
def _admit(self):
"""Pop pending requests into seats. Returns (new seats, inline prefills [(slot, x)])."""
new, prefills, rows = [], [], len(self.active)
cap_admit = self.cfg.max_admit_per_tick
if len(self.active) > self.cfg.busy_frac * self.cfg.max_seats:
cap_admit = min(cap_admit, self.cfg.busy_admit)
while self._pending and self.free and len(new) < cap_admit:
with self._cv:
req = self._pending[0]
if req.cancelled:
self._pending.popleft()
self._emit(req, "error", {"code": "cancelled", "error": "cancelled"})
continue
try:
prompt, n, cap, loose, minimum, suspend = self._plan(req)
except Exception as exc:
with self._cv:
self._pending.popleft()
self._emit(req, "error", {"code": "invalid_request", "error": f"{type(exc).__name__}: {exc}"})
continue
if n + cap + 2 > self.cfg.capacity:
cap = self.cfg.capacity - n - 2
if cap < 8:
with self._cv:
self._pending.popleft()
self._emit(req, "error", {"code": "invalid_request",
"error": f"prompt of {n} tokens leaves no room in KV capacity {self.cfg.capacity}"})
continue
v = req.voice
ts = v.ts
tokens = n - ts
inline = 0 < tokens and rows + tokens <= self.max_rows
if not inline and new:
break # row budget for this tick is used; admit next tick
with self._cv:
self._pending.popleft()
seat = self._bind(req, cap, loose, minimum, suspend)
if self.cfg.verify_prefix and ts:
assert torch.equal(prompt[:, :ts], v.emb), "cached prefix embedding differs from the recomputed prompt"
if inline:
if ts:
self.backbone.load_prefix(seat.slot, v.kv, ts)
self.stats["prefix_hits"] += 1
else:
self.backbone.lengths[seat.slot] = 0
self.backbone.lengths_cpu[seat.slot] = 0
prefills.append((seat.slot, prompt[0, ts:]))
rows += tokens
else: # prompt longer than one tick's row budget: eager HF prefill (from the cached prefix if any)
from serve.cache import cache_from_owned_tensors
if ts:
h, cache = self.model.backbone(prompt[:, ts:], past_key_values=cache_from_owned_tensors(v.kv), use_cache=True)
else:
h, cache = self.model.backbone(prompt, use_cache=True)
self.backbone.load_slot(seat.slot, cache, n)
self.h[seat.slot] = h[0, -1]
del cache
self.stats["fallback_prefill"] += 1
seat.new = "eager" # already has its hidden state; it decodes this tick without a backbone row
new.append(seat)
self.stats["admitted"] += 1
return new, prefills
# ------------------------------------------------------------------ one tick
@torch.inference_mode()
def _tick(self):
t0 = time.perf_counter()
cfg = self.cfg
# cancelled seats leave before any work
keep = []
for s in self.active:
if s.req.cancelled:
self._release(s)
self._emit(s.req, "error", {"code": "cancelled", "error": "cancelled"})
else:
keep.append(s)
self.active = keep
if self._pending and any(r.cancelled for r in self._pending):
with self._cv:
gone = [r for r in self._pending if r.cancelled]
self._pending = collections.deque(r for r in self._pending if not r.cancelled)
for r in gone:
self._emit(r, "error", {"code": "cancelled", "error": "cancelled"})
new, prefills = self._admit()
t1 = time.perf_counter()
dec = [s.slot for s in self.active]
if dec or prefills:
idx = torch.tensor(dec, device=self.device, dtype=torch.long) if dec else None
if prefills:
h, last = self.backbone.step_mixed(dec, self.feedback[idx] if dec else None, prefills)
for (slot, _), hid in zip(prefills, last):
self.h[slot] = hid
else:
h = self.backbone.step(dec, self.feedback[idx])
if dec:
self.h[idx] = h
self.active = self.active + new
if not self.active:
return
t2 = time.perf_counter()
seats = self.active
slots = [s.slot for s in seats]
B = len(slots)
idx = torch.tensor(slots, device=self.device, dtype=torch.long)
h = self.h[idx]
code, cb0 = self._cb0(h, idx)
codes, fb = self.depth(slots, h, cb0)
self.feedback[idx] = fb.to(self.feedback.dtype)
pcm = self.codec.decode(slots, codes)
pcm16 = (pcm.clamp(-1, 1) * 32767).round().to(torch.int16)
packed = torch.cat([code[:, None], cb0[:, None], pcm16.view(torch.int64).view(B, FRAME // 4)], 1)
host = self._host[:B]
host.copy_(packed, non_blocking=True)
self._evt.record()
self._evt.synchronize()
t3 = time.perf_counter()
hv = host.numpy()
stop_l = hv[:, 0].tolist()
cb0_l = hv[:, 1]
pcm_np = hv[:, 2:].view(np.int16)
now = time.perf_counter()
finished = []
g = cfg.guards
check = []
for i, s in enumerate(seats):
req = s.req
s.new = False
if stop_l[i]:
finished.append((s, REASONS[stop_l[i]]))
continue
s.frames += 1
req.frames += 1
if req.t_first is None:
req.t_first = now
self._emit(req, "audio", pcm_np[i].copy())
s.hist[:-1] = s.hist[1:]
s.hist[-1] = cb0_l[i]
if s.frames >= s.cap:
if s.req.max_frames and s.req.frames >= s.req.max_frames:
reason = "max_frames" # the caller's limit, not a model failure
elif g.enabled and s.cap < s.loose_cap:
reason = "cap_guard"
else:
reason = "cap"
finished.append((s, reason))
elif g.enabled and g.loop and s.frames >= g.start and s.frames % g.every == 0 and \
not any(a <= s.frames <= b for a, b in s.suspend):
check.append(i)
if check:
W = np.stack([seats[i].hist for i in check])
n = np.array([seats[i].frames for i in check])
hits = G.loop_detect_batch(W, n, g)
fin_ids = {id(f[0]) for f in finished}
for i, hit in zip(check, hits):
s = seats[i]
if id(s) in fin_ids:
continue
if hit is not None and g.mode == "boost":
if s.boost_at is None:
s.boost_at = s.frames
s.n_boost += 1
self.aeos_bias[s.slot] = g.boost
if s.frames - s.boost_at >= g.backstop:
finished.append((s, "loop_backstop"))
elif hit is not None:
finished.append((s, "loop_" + hit.split(":")[0]))
elif s.boost_at is not None:
self.aeos_bias[s.slot] = 0.0
s.boost_at = None
if finished:
gone = set()
for s, reason in finished:
gone.add(id(s))
self._release(s)
self._finish_piece(s, reason)
self.active = [s for s in self.active if id(s) not in gone]
t4 = time.perf_counter()
self.stats["ticks"] += 1
self.tick_ms.append((t4 - t0) * 1e3)
self.phase_ms = {"admit": round((t1 - t0) * 1e3, 2), "backbone": round((t2 - t1) * 1e3, 2),
"frame": round((t3 - t2) * 1e3, 2), "host": round((t4 - t3) * 1e3, 2), "batch": B,
"prefill_rows": sum(int(x.shape[0]) for _, x in prefills)}
def _release(self, s):
self.backbone.free(s.slot)
self.aeos_bias[s.slot] = 0
self.free.append(s.slot)
def _finish_piece(self, s, reason):
req = s.req
req.stops.append({"reason": reason, "frames": s.frames, "cap": s.cap, "n_boost": s.n_boost})
req.k += 1
if req.k < len(req.pieces) and not req.cancelled and not (req.max_frames and req.frames >= req.max_frames):
self._emit(req, "audio", np.zeros(int(self.cfg.gap_s * SR), np.int16))
with self._cv:
self._pending.appendleft(req) # next piece goes first: the stream continues without queueing
return
req.done = True
self.stats["completed"] += 1
self._emit(req, "end", {"reason": req.stops[-1]["reason"], "frames": req.frames, "audio_s": req.frames * FRAME / SR,
"ttfa_s": (req.t_first - req.t_submit) if req.t_first else None,
"wall_s": time.perf_counter() - req.t_submit, "pieces": len(req.pieces), "stops": req.stops})
def _cb0(self, h, idx):
"""Controlled stop + cb0 sampling (same rule as the reference decoder and ServeSession.speak(stop='controlled')) for all seats, with
per-seat temperature / top-k / top-p / presence penalty and a per-request Philox stream."""
from tts.tokens import AEOS_CLASS
from serve.fast_depth_kernels import row_exponential
logits = self.model.backbone.cb0_logits(h).float() / self.temp_t[idx, None]
logits[:, AEOS_CLASS] += self.aeos_bias[idx]
rep = self.rep_t[idx]
if self.cfg.cb0_rep_win > 0 and bool((self.cfg.cb0_rep > 0) or self._any_rep):
hist = self.hist_t[idx]
pen = torch.zeros_like(logits)
pen.scatter_add_(1, hist.clamp_min(0), (hist >= 0).float())
pen[:, AEOS_CLASS] = 0
logits = logits - rep[:, None] * (pen > 0).float()
srt = logits.sort(-1, descending=True).values
kth = srt.gather(1, (self.topk_t[idx] - 1).clamp(0, logits.shape[1] - 1)[:, None])
logits = logits.masked_fill(logits < kth, -float("inf"))
if self._any_top_p:
sp_, si = logits.softmax(-1).sort(-1, descending=True)
drop = (sp_.cumsum(-1) - sp_) > self.topp_t[idx, None]
logits = logits.masked_fill(torch.zeros_like(drop).scatter(1, si, drop), -float("inf"))
probs = logits.softmax(-1)
pa = probs[:, AEOS_CLASS]
arg = probs.argmax(-1)
frames = self.frames_t[idx]
can = frames >= self.min_t[idx]
hz = self.hazard_t[idx]
hz = torch.where(can, 1 - (1 - hz) * (1 - pa), hz)
code = torch.zeros_like(arg)
code = torch.where(can & (hz >= .6), torch.full_like(code, 4), code)
code = torch.where(can & (pa >= .35), torch.full_like(code, 3), code)
code = torch.where(can & (arg == AEOS_CLASS), torch.full_like(code, 2), code)
q = probs.clone()
q[:, AEOS_CLASS] = 0
q = q / q.sum(-1, keepdim=True).clamp_min(1e-12)
E = row_exponential(q.shape[0], q.shape[1], self.seed_t[idx], self.ctr_t[idx], level=0)
cb0 = (q / E).argmax(-1).clamp_max(AEOS_CLASS - 1)
self.ctr_t[idx] += 1
if self.cfg.cb0_rep_win > 0:
self.hist_t[idx] = torch.cat([self.hist_t[idx, 1:], cb0[:, None]], 1)
self.hazard_t[idx] = hz
self.frames_t[idx] = frames + 1
return code, cb0