dumont-talker / scripts /orpheus_continuous_batching.py
marcosremar2's picture
feat: add Orpheus TTS continuous batching benchmark
62e8495
Raw
History Blame Contribute Delete
11.9 kB
"""
Orpheus TTS - Continuous Batching Test v3
==========================================
Uses AsyncLLMEngine with correct token decoding based on Axolotl's preprocessing.
Token format per 7-token frame:
[layer0, layer1_a, layer2_a, layer2_b, layer1_b, layer2_c, layer2_d]
Where:
- layer0: 128266 + value
- layer1_a: 128266 + 4096 + value
- layer2_a: 128266 + 2*4096 + value
- layer2_b: 128266 + 3*4096 + value
- layer1_b: 128266 + 4*4096 + value
- layer2_c: 128266 + 5*4096 + value
- layer2_d: 128266 + 6*4096 + value
"""
import os
import sys
import time
import wave
import asyncio
import numpy as np
os.environ["VLLM_ATTENTION_BACKEND"] = "FLASH_ATTN"
import torch
from transformers import AutoTokenizer
from vllm import AsyncLLMEngine, AsyncEngineArgs, SamplingParams
from snac import SNAC
# Orpheus special tokens
START_TOKEN = 128259
END_TOKENS = [128009, 128260, 128261, 128257]
STOP_TOKEN = 128258
AUDIO_TOKEN_BASE = 128266
def decode_tokens_to_audio(token_ids, snac_model):
"""Decode Orpheus tokens to audio using SNAC with correct layer offsets."""
# Filter audio tokens and decode by layer
audio_frames = []
for t in token_ids:
if isinstance(t, str):
continue
if t >= AUDIO_TOKEN_BASE:
# Determine which layer this token belongs to
offset = t - AUDIO_TOKEN_BASE
layer = offset // 4096
value = offset % 4096
audio_frames.append((layer, value))
if len(audio_frames) < 7:
return None
# Group into 7-token frames and extract codes for each layer
num_complete_frames = len(audio_frames) // 7
if num_complete_frames == 0:
return None
codes_0 = [] # layer 0
codes_1 = [] # layer 1
codes_2 = [] # layer 2
for i in range(num_complete_frames):
base = i * 7
# Frame format: [l0, l1_a, l2_a, l2_b, l1_b, l2_c, l2_d]
codes_0.append(audio_frames[base][1]) # layer 0 value
codes_1.append(audio_frames[base + 1][1]) # layer 1 first
codes_1.append(audio_frames[base + 4][1]) # layer 1 second
codes_2.append(audio_frames[base + 2][1]) # layer 2 first
codes_2.append(audio_frames[base + 3][1]) # layer 2 second
codes_2.append(audio_frames[base + 5][1]) # layer 2 third
codes_2.append(audio_frames[base + 6][1]) # layer 2 fourth
try:
# Convert to tensors with correct shape
with torch.no_grad():
codes = [
torch.tensor(codes_0, dtype=torch.int64).unsqueeze(0).to("cuda"),
torch.tensor(codes_1, dtype=torch.int64).unsqueeze(0).to("cuda"),
torch.tensor(codes_2, dtype=torch.int64).unsqueeze(0).to("cuda"),
]
audio = snac_model.decode(codes)
return audio.squeeze().cpu().numpy()
except Exception as e:
print(f" Decode error: {e}")
return None
async def run_tests():
"""Run continuous batching tests."""
print("=" * 60)
print("ORPHEUS TTS - CONTINUOUS BATCHING TEST v3")
print("=" * 60)
# Load tokenizer
print("\n[1] Loading tokenizer...")
tokenizer = AutoTokenizer.from_pretrained("canopylabs/orpheus-3b-0.1-ft")
# Create AsyncLLMEngine
print("\n[2] Loading vLLM AsyncLLMEngine...")
start_load = time.time()
engine_args = AsyncEngineArgs(
model="canopylabs/orpheus-3b-0.1-ft",
dtype="bfloat16",
max_model_len=4096,
gpu_memory_utilization=0.9,
max_num_seqs=8, # Continuous batching
enable_chunked_prefill=True,
enable_prefix_caching=True,
enforce_eager=False,
)
engine = AsyncLLMEngine.from_engine_args(engine_args)
load_time = time.time() - start_load
print(f" vLLM loaded in {load_time:.2f}s")
# Load SNAC decoder
print("\n[3] Loading SNAC decoder...")
snac = SNAC.from_pretrained("hubertsiuzdak/snac_24khz").eval().to("cuda")
print(" SNAC loaded!")
# Sampling params
sampling_params = SamplingParams(
temperature=0.2,
top_p=0.9,
max_tokens=4096,
stop_token_ids=[STOP_TOKEN],
repetition_penalty=1.1,
)
def format_prompt(text: str, voice: str = "tara") -> str:
"""Format prompt with Orpheus special tokens."""
adapted_prompt = f"{voice}: {text}"
prompt_tokens = tokenizer(adapted_prompt, return_tensors="pt")
start_token = torch.tensor([[START_TOKEN]], dtype=torch.int64)
end_tokens = torch.tensor([END_TOKENS], dtype=torch.int64)
all_input_ids = torch.cat([start_token, prompt_tokens.input_ids, end_tokens], dim=1)
prompt_string = tokenizer.decode(all_input_ids[0])
return prompt_string
async def generate_speech(text: str, voice: str = "tara", request_id: str = None):
"""Generate speech for a single request."""
prompt_string = format_prompt(text, voice)
request_id = request_id or f"req_{time.time()}"
start = time.time()
token_ids = []
async for output in engine.generate(prompt_string, sampling_params, request_id):
token_ids = list(output.outputs[0].token_ids)
gen_time = time.time() - start
# Count audio tokens
audio_token_count = sum(1 for t in token_ids if isinstance(t, int) and t >= AUDIO_TOKEN_BASE)
# Decode to audio
audio = decode_tokens_to_audio(token_ids, snac)
if audio is None:
return {
'success': False,
'text': text[:30],
'gen_time': gen_time,
'tokens': len(token_ids),
'audio_tokens': audio_token_count,
}
audio_duration = len(audio) / 24000
rtf = gen_time / audio_duration if audio_duration > 0 else float('inf')
return {
'success': True,
'text': text[:30],
'gen_time': gen_time,
'tokens': len(token_ids),
'audio_tokens': audio_token_count,
'audio_duration': audio_duration,
'rtf': rtf,
'audio': audio,
}
# =========================================================================
# TEST 1: SEQUENTIAL (baseline)
# =========================================================================
print("\n" + "=" * 60)
print("[4] Test SEQUENTIAL (baseline)")
print("=" * 60)
test_texts = [
"Hello! This is the first test.",
"Second test to measure performance.",
"Third test for consistent results.",
]
sequential_results = []
total_seq_time = 0
for i, text in enumerate(test_texts, 1):
print(f"\n Test {i}: \"{text}\"")
result = await generate_speech(text, request_id=f"seq_{i}")
if result['success']:
print(f" -> Time: {result['gen_time']:.2f}s | Audio: {result['audio_duration']:.2f}s | RTF: {result['rtf']:.3f}")
sequential_results.append(result)
total_seq_time += result['gen_time']
else:
print(f" -> ERROR: {result['tokens']} tokens ({result['audio_tokens']} audio), no audio")
# =========================================================================
# TEST 2: PARALLEL (Continuous Batching)
# =========================================================================
print("\n" + "=" * 60)
print("[5] Test PARALLEL (Continuous Batching)")
print("=" * 60)
parallel_texts = [
"Hello, how are you today?",
"The weather is beautiful outside.",
"I love programming with Python.",
"Machine learning is fascinating.",
]
batch_results_summary = []
for num_concurrent in [2, 4]:
print(f"\n === {num_concurrent} CONCURRENT REQUESTS ===")
texts = parallel_texts[:num_concurrent]
start_batch = time.time()
tasks = [
generate_speech(text, request_id=f"par_{num_concurrent}_{i}")
for i, text in enumerate(texts)
]
results = await asyncio.gather(*tasks)
batch_time = time.time() - start_batch
# Calculate metrics
successful = [r for r in results if r['success']]
total_audio = sum(r['audio_duration'] for r in successful)
print(f"\n Results:")
for r in results:
if r['success']:
print(f" - \"{r['text']}...\" -> {r['gen_time']:.2f}s | {r['audio_duration']:.2f}s | RTF: {r['rtf']:.3f}")
else:
print(f" - \"{r['text']}...\" -> FAILED ({r['audio_tokens']} audio tokens)")
if total_audio > 0:
batch_rtf = batch_time / total_audio
throughput = len(successful) / batch_time
print(f"\n Aggregate Metrics:")
print(f" - Total wall-clock time: {batch_time:.2f}s")
print(f" - Success rate: {len(successful)}/{len(texts)}")
print(f" - Total audio generated: {total_audio:.2f}s")
print(f" - Batch RTF: {batch_rtf:.3f}")
print(f" - Throughput: {throughput:.2f} req/s")
print(f" - Effective speed: {1/batch_rtf:.1f}x real-time")
batch_results_summary.append({
'concurrent': num_concurrent,
'batch_time': batch_time,
'total_audio': total_audio,
'batch_rtf': batch_rtf,
'throughput': throughput,
})
# =========================================================================
# FINAL SUMMARY
# =========================================================================
print("\n" + "=" * 60)
print("[6] === FINAL SUMMARY ===")
print("=" * 60)
if sequential_results:
seq_rtfs = [r['rtf'] for r in sequential_results]
avg_seq_rtf = sum(seq_rtfs) / len(seq_rtfs)
total_seq_audio = sum(r['audio_duration'] for r in sequential_results)
print(f"\n SEQUENTIAL (baseline):")
print(f" - Average RTF: {avg_seq_rtf:.3f}")
print(f" - Speed: {1/avg_seq_rtf:.1f}x real-time")
if batch_results_summary:
print(f"\n CONTINUOUS BATCHING:")
for bs in batch_results_summary:
speedup = avg_seq_rtf / bs['batch_rtf'] if bs['batch_rtf'] > 0 else 0
print(f" - {bs['concurrent']} concurrent: RTF={bs['batch_rtf']:.3f}, {bs['throughput']:.2f} req/s, {speedup:.1f}x speedup")
# Capacity estimate
users_seq = 1 / avg_seq_rtf if avg_seq_rtf > 0 else 0
print(f"\n RTX 4090 CAPACITY ESTIMATE:")
print(f" - Sequential: ~{users_seq:.0f} real-time users")
if batch_results_summary:
best_batch = min(batch_results_summary, key=lambda x: x['batch_rtf'])
users_batch = best_batch['concurrent'] / best_batch['batch_rtf'] if best_batch['batch_rtf'] > 0 else 0
print(f" - With batching: ~{users_batch:.0f} real-time users")
print("=" * 60)
# Save audio
if sequential_results and sequential_results[-1]['success']:
audio = sequential_results[-1]['audio']
output_path = "/root/test_batching_v3_output.wav"
audio_int16 = (audio * 32767).astype(np.int16)
with wave.open(output_path, "wb") as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(24000)
wf.writeframes(audio_int16.tobytes())
print(f"\n Audio saved to: {output_path}")
if __name__ == "__main__":
asyncio.run(run_tests())