marcosremar2 Claude Opus 4.5 commited on
Commit
fddfe2b
·
1 Parent(s): 62e8495

feat: add TTFT latency benchmark for Orpheus TTS

Browse files

Performance results on RTX 4090:
- 12x throughput improvement with Continuous Batching
- <100ms latency to first audio chunk (even with 16 concurrent users)

Optimization techniques used:
1. Continuous Batching (max_num_seqs=16)
2. Chunked Prefill - interleaves new request prefill with token generation
3. Prefix Caching - reuses computation for common prefixes

TTFT Results:
- Sequential: 63ms to first audio frame
- 16 concurrent: 93ms to first audio frame (worst case)

Capacity: ~12-13 real-time users per RTX 4090

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

docs/orpheus-tts-benchmark.md CHANGED
@@ -73,12 +73,49 @@ Os tokens de audio sao organizados em frames de 7 tokens:
73
  - Layer 1: offset 4096 (2 tokens por frame)
74
  - Layer 2: offset 8192, 12288, etc. (4 tokens por frame)
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  ## Arquivos
77
 
78
- - `scripts/orpheus_continuous_batching.py` - Script de teste
79
- - `scripts/orpheus_stress_test.py` - Teste de stress
80
- - `orpheus_demo_3_phrases.wav` - Audio de demonstracao
81
 
82
  ## Conclusao
83
 
84
- Com Continuous Batching, uma unica RTX 4090 pode suportar **~12-13 usuarios simultaneos em tempo real** para TTS com Orpheus, uma melhoria de **12x** sobre o metodo sequencial.
 
 
 
 
73
  - Layer 1: offset 4096 (2 tokens por frame)
74
  - Layer 2: offset 8192, 12288, etc. (4 tokens por frame)
75
 
76
+ ## Latencia TTFT (Time-To-First-Token)
77
+
78
+ Medimos a latencia ate o primeiro chunk de audio ser gerado:
79
+
80
+ | Requisicoes Simultaneas | TTFT (min) | TTFT (max) | TTFF* (avg) |
81
+ |------------------------|------------|------------|-------------|
82
+ | 1 (sequencial) | 12ms | 12ms | **63ms** |
83
+ | 4 | 12ms | 28ms | **81ms** |
84
+ | 8 | 12ms | 29ms | **82ms** |
85
+ | 12 | 14ms | 32ms | **85ms** |
86
+ | 16 | 16ms | 38ms | **93ms** |
87
+
88
+ *TTFF = Time to First Frame (7 tokens = ~23ms de audio playable)
89
+
90
+ **Conclusao:** Mesmo com 16 requisicoes simultaneas, cada usuario recebe o primeiro chunk de audio em menos de **100ms** - imperceptivel para o usuario!
91
+
92
+ ## Tecnicas de Otimizacao Utilizadas
93
+
94
+ ### 1. Continuous Batching
95
+ Permite processar multiplas requisicoes simultaneamente na mesma GPU. Em vez de processar uma requisicao por vez (sequencial), o vLLM agrupa varias requisicoes e as processa em paralelo.
96
+
97
+ ```python
98
+ max_num_seqs=16 # Ate 16 requisicoes simultaneas
99
+ ```
100
+
101
+ ### 2. Chunked Prefill + Prefix Caching
102
+ - **Chunked Prefill**: Divide o processamento do prompt em chunks menores, permitindo intercalar prefill de novas requisicoes com a geracao de tokens de requisicoes existentes
103
+ - **Prefix Caching**: Reutiliza computacoes de prefixos comuns entre requisicoes (como tokens especiais do Orpheus)
104
+
105
+ ```python
106
+ enable_chunked_prefill=True
107
+ enable_prefix_caching=True
108
+ ```
109
+
110
  ## Arquivos
111
 
112
+ - `scripts/orpheus_continuous_batching.py` - Script de teste de continuous batching
113
+ - `scripts/orpheus_stress_test.py` - Teste de stress com 4/8/12/16 usuarios
114
+ - `scripts/orpheus_ttft_test.py` - Teste de latencia TTFT
115
 
116
  ## Conclusao
117
 
118
+ Com Continuous Batching + Chunked Prefill + Prefix Caching, uma unica RTX 4090 pode suportar **~12-13 usuarios simultaneos em tempo real** para TTS com Orpheus:
119
+ - **12x melhoria** sobre o metodo sequencial
120
+ - **<100ms latencia** para primeiro audio (mesmo com 16 usuarios)
121
+ - Ideal para aplicacoes de avatar interativo e aulas de idiomas
scripts/orpheus_ttft_test.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Teste de Time-To-First-Token (TTFT) para Orpheus TTS
3
+ Mede a latência até o primeiro chunk de áudio ser gerado
4
+ """
5
+ import os
6
+ os.environ["VLLM_ATTENTION_BACKEND"] = "FLASH_ATTN"
7
+
8
+ import torch
9
+ import time
10
+ import asyncio
11
+ from transformers import AutoTokenizer
12
+ from vllm import AsyncLLMEngine, AsyncEngineArgs, SamplingParams
13
+
14
+ START_TOKEN = 128259
15
+ END_TOKENS = [128009, 128260, 128261, 128257]
16
+ STOP_TOKEN = 128258
17
+ AUDIO_TOKEN_BASE = 128266
18
+
19
+ async def main():
20
+ print("=" * 70)
21
+ print("TESTE DE TTFT (Time-To-First-Token) - ORPHEUS TTS")
22
+ print("=" * 70)
23
+
24
+ print("\n[1] Carregando modelo...")
25
+ tokenizer = AutoTokenizer.from_pretrained("canopylabs/orpheus-3b-0.1-ft")
26
+
27
+ engine_args = AsyncEngineArgs(
28
+ model="canopylabs/orpheus-3b-0.1-ft",
29
+ dtype="bfloat16",
30
+ max_model_len=4096,
31
+ gpu_memory_utilization=0.9,
32
+ max_num_seqs=16,
33
+ enable_chunked_prefill=True,
34
+ enable_prefix_caching=True,
35
+ enforce_eager=False,
36
+ )
37
+ engine = AsyncLLMEngine.from_engine_args(engine_args)
38
+
39
+ sampling_params = SamplingParams(
40
+ temperature=0.2,
41
+ top_p=0.9,
42
+ max_tokens=4096,
43
+ stop_token_ids=[STOP_TOKEN],
44
+ repetition_penalty=1.1,
45
+ )
46
+
47
+ def format_prompt(text, voice="tara"):
48
+ adapted_prompt = f"{voice}: {text}"
49
+ prompt_tokens = tokenizer(adapted_prompt, return_tensors="pt")
50
+ start_token = torch.tensor([[START_TOKEN]], dtype=torch.int64)
51
+ end_tokens = torch.tensor([END_TOKENS], dtype=torch.int64)
52
+ all_input_ids = torch.cat([start_token, prompt_tokens.input_ids, end_tokens], dim=1)
53
+ return tokenizer.decode(all_input_ids[0])
54
+
55
+ async def measure_ttft(text, request_id):
56
+ """Mede o tempo até o primeiro token de áudio"""
57
+ prompt_string = format_prompt(text)
58
+ start = time.time()
59
+ ttft = None
60
+ ttfa = None # Time to first audio token
61
+ first_audio_frame = None # Time to first complete 7-token frame
62
+ audio_token_count = 0
63
+ total_tokens = 0
64
+
65
+ async for output in engine.generate(prompt_string, sampling_params, request_id):
66
+ total_tokens = len(output.outputs[0].token_ids)
67
+
68
+ # Medir TTFT (primeiro token qualquer)
69
+ if ttft is None and total_tokens > 0:
70
+ ttft = time.time() - start
71
+
72
+ # Medir TTFA (primeiro token de áudio)
73
+ if ttfa is None:
74
+ for t in output.outputs[0].token_ids:
75
+ if t >= AUDIO_TOKEN_BASE:
76
+ ttfa = time.time() - start
77
+ break
78
+
79
+ # Contar tokens de áudio
80
+ audio_token_count = sum(1 for t in output.outputs[0].token_ids if t >= AUDIO_TOKEN_BASE)
81
+
82
+ # Medir tempo até primeiro frame completo (7 tokens de áudio)
83
+ if first_audio_frame is None and audio_token_count >= 7:
84
+ first_audio_frame = time.time() - start
85
+
86
+ total_time = time.time() - start
87
+
88
+ return {
89
+ 'text': text[:40],
90
+ 'ttft': ttft, # Time to first token
91
+ 'ttfa': ttfa, # Time to first audio token
92
+ 'ttff': first_audio_frame, # Time to first audio frame (7 tokens = ~23ms de áudio)
93
+ 'total_time': total_time,
94
+ 'total_tokens': total_tokens,
95
+ 'audio_tokens': audio_token_count,
96
+ }
97
+
98
+ test_texts = [
99
+ "Hello, how are you doing today?",
100
+ "The weather is beautiful outside.",
101
+ "I love programming with Python.",
102
+ "Machine learning is fascinating.",
103
+ "Can you help me with this task?",
104
+ "Let me explain how this works.",
105
+ "This is a test of the system.",
106
+ "Technology is amazing these days.",
107
+ "Have a wonderful day ahead.",
108
+ "Thank you for your patience.",
109
+ "Let's work together on this.",
110
+ "The future looks very bright.",
111
+ "I appreciate your help today.",
112
+ "This demonstration is working.",
113
+ "Audio generation is fast now.",
114
+ "Real-time speech synthesis.",
115
+ ]
116
+
117
+ print("\n[2] Teste SEQUENCIAL (baseline TTFT)...")
118
+ print("-" * 70)
119
+
120
+ # Warmup
121
+ print(" Warmup...")
122
+ await measure_ttft("Warmup test.", "warmup")
123
+
124
+ seq_results = []
125
+ for i, text in enumerate(test_texts[:4]):
126
+ result = await measure_ttft(text, f"seq_{i}")
127
+ seq_results.append(result)
128
+ print(f" [{i+1}] TTFT: {result['ttft']*1000:.0f}ms | TTFA: {result['ttfa']*1000:.0f}ms | TTFF: {result['ttff']*1000:.0f}ms | Total: {result['total_time']:.2f}s")
129
+
130
+ avg_ttft = sum(r['ttft'] for r in seq_results) / len(seq_results)
131
+ avg_ttfa = sum(r['ttfa'] for r in seq_results) / len(seq_results)
132
+ avg_ttff = sum(r['ttff'] for r in seq_results) / len(seq_results)
133
+
134
+ print(f"\n MÉDIA SEQUENCIAL:")
135
+ print(f" - TTFT (primeiro token): {avg_ttft*1000:.0f}ms")
136
+ print(f" - TTFA (primeiro áudio): {avg_ttfa*1000:.0f}ms")
137
+ print(f" - TTFF (primeiro frame): {avg_ttff*1000:.0f}ms (~23ms de áudio)")
138
+
139
+ # Teste com requisições simultâneas
140
+ print("\n" + "=" * 70)
141
+ print("[3] Teste TTFT com requisições SIMULTÂNEAS")
142
+ print("=" * 70)
143
+
144
+ for num_concurrent in [4, 8, 12, 16]:
145
+ print(f"\n>>> {num_concurrent} REQUISIÇÕES SIMULTÂNEAS <<<")
146
+ print("-" * 50)
147
+
148
+ texts = test_texts[:num_concurrent]
149
+
150
+ tasks = [measure_ttft(text, f"par_{num_concurrent}_{i}") for i, text in enumerate(texts)]
151
+ results = await asyncio.gather(*tasks)
152
+
153
+ ttfts = [r['ttft'] for r in results]
154
+ ttfas = [r['ttfa'] for r in results]
155
+ ttffs = [r['ttff'] for r in results if r['ttff']]
156
+
157
+ print(f" TTFT - Min: {min(ttfts)*1000:.0f}ms | Max: {max(ttfts)*1000:.0f}ms | Avg: {sum(ttfts)/len(ttfts)*1000:.0f}ms")
158
+ print(f" TTFA - Min: {min(ttfas)*1000:.0f}ms | Max: {max(ttfas)*1000:.0f}ms | Avg: {sum(ttfas)/len(ttfas)*1000:.0f}ms")
159
+ if ttffs:
160
+ print(f" TTFF - Min: {min(ttffs)*1000:.0f}ms | Max: {max(ttffs)*1000:.0f}ms | Avg: {sum(ttffs)/len(ttffs)*1000:.0f}ms")
161
+
162
+ # Mostrar cada resultado
163
+ print(f"\n Detalhes por requisição:")
164
+ for i, r in enumerate(results):
165
+ print(f" [{i+1}] TTFT: {r['ttft']*1000:.0f}ms | TTFA: {r['ttfa']*1000:.0f}ms | TTFF: {r['ttff']*1000:.0f}ms")
166
+
167
+ print("\n" + "=" * 70)
168
+ print("RESUMO - LATÊNCIA PARA PRIMEIRO ÁUDIO")
169
+ print("=" * 70)
170
+ print("""
171
+ TTFT = Time to First Token (qualquer token)
172
+ TTFA = Time to First Audio token
173
+ TTFF = Time to First Frame (7 tokens = ~23ms de áudio playable)
174
+
175
+ Para streaming de áudio, o TTFF é o mais importante:
176
+ é quando você tem o primeiro chunk de áudio para tocar.
177
+ """)
178
+ print("=" * 70)
179
+
180
+ if __name__ == "__main__":
181
+ asyncio.run(main())