tts-dgx-spark-faster-qwen3-tts/config/benchmark_api.py
mARTin-B78 92e2381d0b Reduce latency: CUDA warmup, chunk_size=4, max-seq-len 2048
- run_server.py: warm up CUDA graphs at server startup via lifespan
  event so the first real request does not pay the 7-8s graph-
  compilation penalty; uses modern lifespan API instead of deprecated
  on_event
- generate_voices.py: set chunk_size=4 per voice so streaming clients
  receive first audio after ~333ms instead of ~1s
- docker-compose.yml: add --max-seq-len 2048 (halves static KV cache,
  reduces VRAM and graph-capture time)
- config/benchmark_api.py: new script to measure TTFA, RTF and speed
  against the live API endpoint

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 20:43:21 +02:00

176 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Benchmark the faster-qwen3-tts API server.
Measures:
- TTFA : time-to-first-audio (ms) — latency before playback can start
- Total : wall-clock time until full audio received (s)
- RTF : real-time factor = generation_time / audio_duration (lower = faster)
- Speed : audio_duration / generation_time (higher = faster, e.g. 3× real-time)
Usage:
python benchmark_api.py [--host localhost] [--port 8020] [--voice alloy]
[--runs 3] [--format wav]
"""
import argparse
import struct
import sys
import time
import requests
# ── Test sentences of increasing length ─────────────────────────────────────
SENTENCES = [
("short", "Hello, how are you today?"),
("medium", "The quick brown fox jumps over the lazy dog near the river bank."),
("long", "Artificial intelligence is transforming the way we interact with "
"technology. From voice assistants to autonomous vehicles, machine "
"learning models are becoming an integral part of everyday life."),
]
SAMPLE_RATE = 24000
BYTES_PER_SAMPLE = 2 # 16-bit PCM
def parse_wav_header(data: bytes) -> int:
"""Return the data-chunk offset so we can skip the header bytes."""
# WAV: RIFF(4) + size(4) + WAVE(4) + fmt (8+16) + data(4) + size(4) = 44 bytes
if data[:4] != b"RIFF" or data[8:12] != b"WAVE":
return 0
# Walk chunks after WAVE marker
pos = 12
while pos + 8 <= len(data):
chunk_id = data[pos:pos+4]
chunk_sz = struct.unpack_from("<I", data, pos+4)[0]
if chunk_id == b"data":
return pos + 8
pos += 8 + chunk_sz
return 44 # fallback
def benchmark_request(url: str, payload: dict, label: str, run: int) -> dict:
t0 = time.perf_counter()
first_byte_t = None
raw = bytearray()
with requests.post(url, json=payload, stream=True, timeout=120) as resp:
resp.raise_for_status()
for chunk in resp.iter_content(chunk_size=512):
if chunk:
if first_byte_t is None:
first_byte_t = time.perf_counter()
raw.extend(chunk)
t1 = time.perf_counter()
ttfa_ms = (first_byte_t - t0) * 1000 if first_byte_t else float("nan")
total_s = t1 - t0
# Strip WAV header to count actual PCM bytes
if payload.get("response_format", "wav") == "wav":
data_offset = parse_wav_header(bytes(raw))
pcm_bytes = len(raw) - data_offset
else:
pcm_bytes = len(raw)
audio_s = pcm_bytes / (SAMPLE_RATE * BYTES_PER_SAMPLE)
rtf = total_s / audio_s if audio_s > 0 else float("nan")
speedup = audio_s / total_s if total_s > 0 else float("nan")
return {
"label": label,
"run": run,
"ttfa_ms": ttfa_ms,
"total_s": total_s,
"audio_s": audio_s,
"rtf": rtf,
"speedup": speedup,
}
def main():
p = argparse.ArgumentParser(description="Benchmark faster-qwen3-tts API")
p.add_argument("--host", default="localhost")
p.add_argument("--port", type=int, default=8020)
p.add_argument("--voice", default=None,
help="Voice name (default: first available voice)")
p.add_argument("--runs", type=int, default=3,
help="Repetitions per sentence (default: 3)")
p.add_argument("--format", default="wav", choices=["wav", "pcm"],
help="Response format (default: wav)")
args = p.parse_args()
base = f"http://{args.host}:{args.port}"
url = f"{base}/v1/audio/speech"
# Auto-detect first voice if not specified
voice = args.voice
if not voice:
try:
models = requests.get(f"{base}/v1/models", timeout=5).json()
voice = models["data"][0]["id"]
print(f"Auto-selected voice: {voice!r}")
except Exception:
voice = "alloy"
print(f"Could not auto-detect voice, using {voice!r}")
print(f"\n{''*65}")
print(f" faster-qwen3-tts API Benchmark")
print(f" URL: {url} voice={voice} runs={args.runs} fmt={args.format}")
print(f"{''*65}\n")
header = f"{'Label':<10} {'Run':>3} {'TTFA*':>9} {'Total':>7} {'Audio':>7} {'RTF':>6} {'Speed':>8}"
print(header)
print("" * len(header))
print(" * TTFA = time to first PCM byte (WAV header excluded)")
print()
all_results = []
for label, text in SENTENCES:
payload = {
"model": "tts-1",
"input": text,
"voice": voice,
"response_format": args.format,
}
for run in range(1, args.runs + 1):
try:
r = benchmark_request(url, payload, label, run)
all_results.append(r)
print(
f"{r['label']:<10} {r['run']:>3} "
f"{r['ttfa_ms']:>7.0f}ms "
f"{r['total_s']:>6.2f}s "
f"{r['audio_s']:>6.2f}s "
f"{r['rtf']:>5.2f}x "
f"{r['speedup']:>6.1f}x rt"
)
sys.stdout.flush()
except Exception as e:
print(f"{label:<10} run {run} ERROR: {e}")
# ── Summary ──────────────────────────────────────────────────────────────
if all_results:
print(f"\n{''*65}")
print(" Summary (averages across all runs)")
print(f"{''*65}")
for label, _ in SENTENCES:
rows = [r for r in all_results if r["label"] == label]
if not rows:
continue
avg_ttfa = sum(r["ttfa_ms"] for r in rows) / len(rows)
avg_total = sum(r["total_s"] for r in rows) / len(rows)
avg_rtf = sum(r["rtf"] for r in rows) / len(rows)
avg_speed = sum(r["speedup"] for r in rows) / len(rows)
print(
f" {label:<10} TTFA={avg_ttfa:.0f}ms "
f"total={avg_total:.2f}s RTF={avg_rtf:.2f}x "
f"speed={avg_speed:.1f}× real-time"
)
print()
if __name__ == "__main__":
main()