09a1c98514
- pocket_server.py (Produktions-TTS mit Stimmen-Waechter), text_norm, Bench-/Diag-Skripte - lucy-f5: f5_server/f5_test/bench_dml (DirectML-Experiment, Phase C/D offen) - .gitignore: venvs/Modelle/Audio/Logs der beiden Ordner + box_recon/gemma_swap-Scratch Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
106 lines
5.0 KiB
Python
106 lines
5.0 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""sweep_b2.py — findet den B2-Sweet-Spot auf DIESER Maschine (Ryzen 9700X, 8C/16T).
|
||
Variiert WORKERS × THREADS-pro-Worker und misst je Konfig auf einer langen, mehrsätzigen Antwort:
|
||
- wall : Gesamt-Wall-Clock (Generierung der ganzen Antwort)
|
||
- ttfb : Time-to-first-audio (Reaktionszeit = erster Satz fertig)
|
||
- audio : erzeugte Audiolänge (variiert leicht, da temp>0 stochastisch)
|
||
- RTF : wall/audio (längen-normalisiert -> fairer Durchsatz-Vergleich; <1 = schneller als Echtzeit)
|
||
Nutzt die ECHTEN pocket_server-Funktionen (_worker_init/_warmup/_gen_sentences_ordered).
|
||
Schreibt sweep_b2_result.json + eine Tabelle und nennt am Ende den Sweet Spot.
|
||
"""
|
||
import os, time, json, statistics as st
|
||
import numpy as np, librosa
|
||
from concurrent.futures import ProcessPoolExecutor
|
||
from pocket_tts import TTSModel
|
||
import pocket_server as ps
|
||
|
||
BASE = ps.BASE
|
||
LONG = ("Guten Morgen, Commander. "
|
||
"Das nächtliche Backup ist sauber durchgelaufen und es gab keine Fehler. "
|
||
"Der Dienst läuft stabil und die Engine antwortet zügig. "
|
||
"Ich habe die Modelle vorgewärmt und die Latenz im Blick behalten. "
|
||
"Die Protokolle zeigen keine Auffälligkeiten in den letzten Stunden. "
|
||
"Wenn du möchtest, fasse ich die offenen Punkte für heute zusammen. "
|
||
"Danach kümmere ich mich um die anstehenden Updates und melde mich wieder.")
|
||
|
||
# (workers, threads_pro_worker). Ziel: workers*threads ~ 6–8 (8 physische Kerne).
|
||
CONFIGS = [("seriell", 0, 0), ("pool", 2, 4), ("pool", 2, 3),
|
||
("pool", 3, 2), ("pool", 3, 3), ("pool", 4, 2)]
|
||
REPS = 3
|
||
|
||
|
||
def _measure(sents):
|
||
"""Eine Durchführung: (wall, ttfb, audio_s) über den aktuell in STATE gesetzten Pfad."""
|
||
t0 = time.time(); first = None; total = 0
|
||
for i, a in enumerate(ps._gen_sentences_ordered(sents)):
|
||
if i == 0:
|
||
first = time.time() - t0
|
||
total += a.size
|
||
return time.time() - t0, first, total / 24000.0
|
||
|
||
|
||
def run_serial(reps):
|
||
m = TTSModel.load_model(language=ps.LANG, lsd_decode_steps=ps.LSD, temp=ps.TEMP,
|
||
noise_clamp=ps.NOISE_CLAMP, quantize=ps.QUANTIZE)
|
||
ref = ps._prep_ref()
|
||
try:
|
||
vs = m.get_state_for_audio_prompt(ps.VOICE_ST)
|
||
except Exception:
|
||
vs = m.get_state_for_audio_prompt(ref)
|
||
ra, _ = librosa.load(ref, sr=m.sample_rate, mono=True)
|
||
ps.STATE.clear()
|
||
ps.STATE.update(m=m, vs=vs, sr=m.sample_rate, ref_fp=ps._fingerprint(ra, m.sample_rate))
|
||
sents = ps._split_sentences(LONG)
|
||
rows = [_measure(sents) for _ in range(reps)]
|
||
ps.STATE.clear(); del m
|
||
return rows, len(sents)
|
||
|
||
|
||
def run_pool(workers, threads, reps):
|
||
os.environ["LUCY_WORKER_THREADS"] = str(threads) # vom Worker beim Spawn gelesen
|
||
pool = ProcessPoolExecutor(max_workers=workers, initializer=ps._worker_init)
|
||
list(pool.map(ps._warmup, range(workers)))
|
||
ref = ps._prep_ref(); ra, _ = librosa.load(ref, sr=24000, mono=True)
|
||
ps.STATE.clear()
|
||
ps.STATE.update(sr=24000, ref_fp=ps._fingerprint(ra, 24000), pool=pool)
|
||
sents = ps._split_sentences(LONG)
|
||
rows = [_measure(sents) for _ in range(reps)]
|
||
pool.shutdown(wait=True); ps.STATE.clear()
|
||
return rows, len(sents)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
results = []
|
||
print(f"Text: {len(LONG)} Zeichen, REPS={REPS}\n")
|
||
print(f"{'config':>14} | {'wall':>6} {'ttfb':>6} {'audio':>6} {'RTF':>5}")
|
||
print("-" * 48)
|
||
for kind, w, t in CONFIGS:
|
||
try:
|
||
rows, nsent = (run_serial(REPS) if kind == "seriell" else run_pool(w, t, REPS))
|
||
except Exception as e:
|
||
print(f"{kind} w{w} t{t}: FEHLER {e}")
|
||
continue
|
||
wall = st.median([r[0] for r in rows])
|
||
ttfb = st.median([r[1] for r in rows])
|
||
audio = st.median([r[2] for r in rows])
|
||
rtf = wall / max(audio, 0.01)
|
||
label = "seriell(1×all)" if kind == "seriell" else f"{w}w×{t}t"
|
||
print(f"{label:>14} | {wall:6.2f} {ttfb:6.2f} {audio:6.2f} {rtf:5.2f}")
|
||
results.append(dict(label=label, kind=kind, workers=(1 if kind == "seriell" else w),
|
||
threads=t, wall=wall, ttfb=ttfb, audio=audio, rtf=rtf, sents=nsent))
|
||
|
||
json.dump(results, open(os.path.join(BASE, "sweep_b2_result.json"), "w"), indent=2)
|
||
|
||
pool_rows = [r for r in results if r["kind"] == "pool"]
|
||
if pool_rows:
|
||
best_tput = min(pool_rows, key=lambda r: r["rtf"])
|
||
best_ttfb = min(pool_rows, key=lambda r: r["ttfb"])
|
||
# Sweet Spot: bester Durchsatz, aber TTFB nicht >20% über dem TTFB-Sieger (Reaktion zählt)
|
||
cand = [r for r in pool_rows if r["ttfb"] <= best_ttfb["ttfb"] * 1.20]
|
||
sweet = min(cand, key=lambda r: r["rtf"]) if cand else best_tput
|
||
print("\n>> bester Durchsatz :", best_tput["label"], f"(RTF {best_tput['rtf']:.2f})")
|
||
print(">> beste Reaktion :", best_ttfb["label"], f"(TTFB {best_ttfb['ttfb']:.2f}s)")
|
||
print(">> SWEET SPOT :", sweet["label"],
|
||
f"-> LUCY_WORKERS={sweet['workers']} LUCY_WORKER_THREADS={sweet['threads']}")
|
||
print("SWEEP_DONE")
|