Commit ·
18ee4b6
1
Parent(s): 2c41a3f
feat: add max users and realistic streaming tests for Orpheus TTS
Browse filesNew tests on RTX 4090:
- Max users test: up to 256 concurrent users without errors
- Realistic streaming test with variable phrase lengths
Key findings:
- Without buffer: ~12-16 users with RTF < 1.0
- With 500ms client buffer: up to 256 users without stuttering
- Max gap between frames: 375ms at 256 users (covered by buffer)
Recommendation: Use ~500ms audio buffer on client for language learning apps
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- docs/orpheus-tts-benchmark.md +45 -2
- scripts/orpheus_max_users_test.py +162 -0
- scripts/orpheus_streaming_test.py +244 -0
docs/orpheus-tts-benchmark.md
CHANGED
|
@@ -107,15 +107,58 @@ 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
|
|
|
|
|
|
|
|
|
|
| 119 |
- **12x melhoria** sobre o metodo sequencial
|
| 120 |
-
- **<100ms latencia** para primeiro audio
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
- Ideal para aplicacoes de avatar interativo e aulas de idiomas
|
|
|
|
|
|
|
|
|
| 107 |
enable_prefix_caching=True
|
| 108 |
```
|
| 109 |
|
| 110 |
+
## Teste de Limite Maximo (max_num_seqs=256)
|
| 111 |
+
|
| 112 |
+
Testamos ate onde a RTX 4090 aguenta com frases curtas identicas:
|
| 113 |
+
|
| 114 |
+
| Users | Sucesso | TTFF Max | RTF |
|
| 115 |
+
|-------|---------|----------|-----|
|
| 116 |
+
| 32 | 100% | 116ms | 0.154 |
|
| 117 |
+
| 64 | 100% | 136ms | 0.125 |
|
| 118 |
+
| 128 | 100% | 228ms | 0.091 |
|
| 119 |
+
| 200 | 100% | 335ms | 0.284 |
|
| 120 |
+
| 256 | 100% | 413ms | 0.244 |
|
| 121 |
+
|
| 122 |
+
**Resultado:** 256 usuarios simultaneos sem erros, TTFF < 500ms.
|
| 123 |
+
|
| 124 |
+
## Teste Realista de Streaming (frases variadas)
|
| 125 |
+
|
| 126 |
+
Testamos com frases de tamanhos variados (curtas, medias, longas) para simular uso real:
|
| 127 |
+
|
| 128 |
+
| Users | Max Gap | RTF Max | Streaming com buffer 500ms |
|
| 129 |
+
|-------|---------|---------|---------------------------|
|
| 130 |
+
| 32 | 86ms | 3.67 | OK |
|
| 131 |
+
| 64 | 105ms | 4.51 | OK |
|
| 132 |
+
| 100 | 139ms | 5.98 | OK |
|
| 133 |
+
| 128 | 160ms | 6.88 | OK |
|
| 134 |
+
| 200 | 232ms | 9.80 | OK |
|
| 135 |
+
| 256 | 375ms | 11.78 | OK |
|
| 136 |
+
|
| 137 |
+
**Conclusao do teste realista:**
|
| 138 |
+
- Com buffer de ~500ms no cliente: ate 256 usuarios funcionam sem travamento
|
| 139 |
+
- Sem buffer (streaming puro): ~12-16 usuarios com RTF < 1.0
|
| 140 |
+
- Para aulas de idiomas com avatar: recomendado usar buffer no cliente
|
| 141 |
+
|
| 142 |
## Arquivos
|
| 143 |
|
| 144 |
- `scripts/orpheus_continuous_batching.py` - Script de teste de continuous batching
|
| 145 |
- `scripts/orpheus_stress_test.py` - Teste de stress com 4/8/12/16 usuarios
|
| 146 |
- `scripts/orpheus_ttft_test.py` - Teste de latencia TTFT
|
| 147 |
+
- `scripts/orpheus_max_users_test.py` - Teste de limite maximo de usuarios
|
| 148 |
+
- `scripts/orpheus_streaming_test.py` - Teste realista de streaming com frases variadas
|
| 149 |
|
| 150 |
## Conclusao
|
| 151 |
|
| 152 |
+
Com Continuous Batching + Chunked Prefill + Prefix Caching, uma unica RTX 4090 pode suportar:
|
| 153 |
+
|
| 154 |
+
**Streaming puro (sem buffer):**
|
| 155 |
+
- **~12-16 usuarios** com RTF < 1.0
|
| 156 |
- **12x melhoria** sobre o metodo sequencial
|
| 157 |
+
- **<100ms latencia** para primeiro audio
|
| 158 |
+
|
| 159 |
+
**Com buffer de 500ms no cliente:**
|
| 160 |
+
- **Ate 256 usuarios simultaneos** sem travamento
|
| 161 |
+
- Max gap entre frames: 375ms (coberto pelo buffer)
|
| 162 |
- Ideal para aplicacoes de avatar interativo e aulas de idiomas
|
| 163 |
+
|
| 164 |
+
**Recomendacao:** Para aulas de idiomas, usar buffer de ~500ms no cliente para suportar mais usuarios.
|
scripts/orpheus_max_users_test.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Teste de Limite MÁXIMO - Até onde vai antes de travar?
|
| 3 |
+
"""
|
| 4 |
+
import os
|
| 5 |
+
os.environ["VLLM_ATTENTION_BACKEND"] = "FLASH_ATTN"
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
import time
|
| 9 |
+
import asyncio
|
| 10 |
+
from transformers import AutoTokenizer
|
| 11 |
+
from vllm import AsyncLLMEngine, AsyncEngineArgs, SamplingParams
|
| 12 |
+
|
| 13 |
+
START_TOKEN = 128259
|
| 14 |
+
END_TOKENS = [128009, 128260, 128261, 128257]
|
| 15 |
+
STOP_TOKEN = 128258
|
| 16 |
+
AUDIO_TOKEN_BASE = 128266
|
| 17 |
+
|
| 18 |
+
async def main():
|
| 19 |
+
print("=" * 70)
|
| 20 |
+
print("TESTE DE LIMITE MÁXIMO - ATÉ ONDE VAI?")
|
| 21 |
+
print("=" * 70)
|
| 22 |
+
|
| 23 |
+
print("\n[1] Carregando modelo com max_num_seqs=256...")
|
| 24 |
+
tokenizer = AutoTokenizer.from_pretrained("canopylabs/orpheus-3b-0.1-ft")
|
| 25 |
+
|
| 26 |
+
# Máximo possível
|
| 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.95,
|
| 32 |
+
max_num_seqs=256, # Tentando o máximo
|
| 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_request(text, request_id):
|
| 56 |
+
prompt_string = format_prompt(text)
|
| 57 |
+
start = time.time()
|
| 58 |
+
ttff = None
|
| 59 |
+
audio_token_count = 0
|
| 60 |
+
|
| 61 |
+
async for output in engine.generate(prompt_string, sampling_params, request_id):
|
| 62 |
+
current_audio = sum(1 for t in output.outputs[0].token_ids if t >= AUDIO_TOKEN_BASE)
|
| 63 |
+
if ttff is None and current_audio >= 7:
|
| 64 |
+
ttff = time.time() - start
|
| 65 |
+
audio_token_count = current_audio
|
| 66 |
+
|
| 67 |
+
total_time = time.time() - start
|
| 68 |
+
audio_duration = (audio_token_count // 7) * 0.023
|
| 69 |
+
|
| 70 |
+
return {
|
| 71 |
+
'ttff': ttff or total_time,
|
| 72 |
+
'total_time': total_time,
|
| 73 |
+
'audio_duration': audio_duration,
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
# Gerar 256 frases de teste
|
| 77 |
+
base_texts = [
|
| 78 |
+
"Hello, how are you?", "Good morning!", "Nice to meet you.",
|
| 79 |
+
"How is the weather?", "I love learning.", "This is great.",
|
| 80 |
+
"Thank you so much.", "Have a nice day.", "See you later.",
|
| 81 |
+
"What time is it?", "Where are you from?", "I am happy.",
|
| 82 |
+
"Let's practice.", "Very interesting.", "Good job today.",
|
| 83 |
+
"Keep it up.", "Well done!", "Excellent work.",
|
| 84 |
+
]
|
| 85 |
+
test_texts = (base_texts * 15)[:256] # 256 frases
|
| 86 |
+
|
| 87 |
+
print("\n[2] Warmup...")
|
| 88 |
+
await measure_request("Warmup.", "warmup")
|
| 89 |
+
|
| 90 |
+
print("\n[3] Testando limites...")
|
| 91 |
+
print("=" * 70)
|
| 92 |
+
|
| 93 |
+
results_summary = []
|
| 94 |
+
|
| 95 |
+
for num_users in [32, 48, 64, 96, 128, 160, 200, 256]:
|
| 96 |
+
print(f"\n>>> TESTANDO {num_users} USUÁRIOS <<<")
|
| 97 |
+
|
| 98 |
+
texts = test_texts[:num_users]
|
| 99 |
+
|
| 100 |
+
try:
|
| 101 |
+
start_batch = time.time()
|
| 102 |
+
tasks = [measure_request(text, f"u{num_users}_{i}") for i, text in enumerate(texts)]
|
| 103 |
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
| 104 |
+
batch_time = time.time() - start_batch
|
| 105 |
+
|
| 106 |
+
errors = [r for r in results if isinstance(r, Exception)]
|
| 107 |
+
successful = [r for r in results if not isinstance(r, Exception)]
|
| 108 |
+
|
| 109 |
+
if errors:
|
| 110 |
+
print(f" ERROS: {len(errors)}/{num_users}")
|
| 111 |
+
print(f" Tipo: {type(errors[0]).__name__}")
|
| 112 |
+
if len(successful) == 0:
|
| 113 |
+
print(f" >>> FALHA TOTAL - LIMITE ATINGIDO <<<")
|
| 114 |
+
break
|
| 115 |
+
|
| 116 |
+
if successful:
|
| 117 |
+
ttffs = [r['ttff'] for r in successful]
|
| 118 |
+
total_audio = sum(r['audio_duration'] for r in successful)
|
| 119 |
+
|
| 120 |
+
rtf = batch_time / total_audio if total_audio > 0 else 999
|
| 121 |
+
realtime = total_audio / batch_time if batch_time > 0 else 0
|
| 122 |
+
|
| 123 |
+
print(f" OK: {len(successful)}/{num_users} | Time: {batch_time:.1f}s | TTFF max: {max(ttffs)*1000:.0f}ms | RTF: {rtf:.3f}")
|
| 124 |
+
|
| 125 |
+
results_summary.append({
|
| 126 |
+
'users': num_users,
|
| 127 |
+
'success': len(successful),
|
| 128 |
+
'errors': len(errors),
|
| 129 |
+
'batch_time': batch_time,
|
| 130 |
+
'ttff_max': max(ttffs),
|
| 131 |
+
'rtf': rtf,
|
| 132 |
+
'realtime': realtime
|
| 133 |
+
})
|
| 134 |
+
|
| 135 |
+
# Se TTFF > 2 segundos, parar
|
| 136 |
+
if max(ttffs) > 2.0:
|
| 137 |
+
print(f" >>> TTFF muito alto ({max(ttffs)*1000:.0f}ms) - PARANDO <<<")
|
| 138 |
+
break
|
| 139 |
+
|
| 140 |
+
except Exception as e:
|
| 141 |
+
print(f" CRASH: {type(e).__name__}: {str(e)[:80]}")
|
| 142 |
+
break
|
| 143 |
+
|
| 144 |
+
print("\n" + "=" * 70)
|
| 145 |
+
print("RESUMO FINAL")
|
| 146 |
+
print("=" * 70)
|
| 147 |
+
|
| 148 |
+
print("\n| Users | OK | Erros | Time | TTFF Max | RTF |")
|
| 149 |
+
print("|-------|-----|-------|--------|-----------|-------|")
|
| 150 |
+
for r in results_summary:
|
| 151 |
+
print(f"| {r['users']:5} | {r['success']:3} | {r['errors']:5} | {r['batch_time']:6.1f}s | {r['ttff_max']*1000:7.0f}ms | {r['rtf']:.3f} |")
|
| 152 |
+
|
| 153 |
+
if results_summary:
|
| 154 |
+
last_good = [r for r in results_summary if r['errors'] == 0 and r['ttff_max'] < 1.0]
|
| 155 |
+
if last_good:
|
| 156 |
+
best = max(last_good, key=lambda x: x['users'])
|
| 157 |
+
print(f"\n>>> MÁXIMO ESTÁVEL: {best['users']} usuários (TTFF < 1s, 0 erros) <<<")
|
| 158 |
+
|
| 159 |
+
print("=" * 70)
|
| 160 |
+
|
| 161 |
+
if __name__ == "__main__":
|
| 162 |
+
asyncio.run(main())
|
scripts/orpheus_streaming_test.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Teste REALISTA de Streaming - Simula usuários com frases variadas
|
| 3 |
+
Verifica se o áudio trava para algum usuário durante streaming contínuo
|
| 4 |
+
"""
|
| 5 |
+
import os
|
| 6 |
+
os.environ["VLLM_ATTENTION_BACKEND"] = "FLASH_ATTN"
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
import time
|
| 10 |
+
import asyncio
|
| 11 |
+
import random
|
| 12 |
+
from transformers import AutoTokenizer
|
| 13 |
+
from vllm import AsyncLLMEngine, AsyncEngineArgs, SamplingParams
|
| 14 |
+
|
| 15 |
+
START_TOKEN = 128259
|
| 16 |
+
END_TOKENS = [128009, 128260, 128261, 128257]
|
| 17 |
+
STOP_TOKEN = 128258
|
| 18 |
+
AUDIO_TOKEN_BASE = 128266
|
| 19 |
+
|
| 20 |
+
# Frases variadas - curtas, médias e longas (simulando aula de idiomas)
|
| 21 |
+
PHRASES_SHORT = [
|
| 22 |
+
"Hello!", "Yes.", "No.", "Thank you.", "Good morning.",
|
| 23 |
+
"How are you?", "I'm fine.", "See you!", "Bye!", "Nice!",
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
PHRASES_MEDIUM = [
|
| 27 |
+
"I would like to order a coffee, please.",
|
| 28 |
+
"The weather is really nice today, isn't it?",
|
| 29 |
+
"Can you help me find the train station?",
|
| 30 |
+
"I'm learning English and it's very interesting.",
|
| 31 |
+
"What time does the movie start tonight?",
|
| 32 |
+
"My favorite color is blue, what about yours?",
|
| 33 |
+
"I work as a software engineer in the city.",
|
| 34 |
+
"Do you have any recommendations for dinner?",
|
| 35 |
+
]
|
| 36 |
+
|
| 37 |
+
PHRASES_LONG = [
|
| 38 |
+
"I've been studying English for about three years now, and I find it fascinating how much progress I've made since I started.",
|
| 39 |
+
"Yesterday I went to the supermarket and bought some fruits, vegetables, and other groceries for the whole week.",
|
| 40 |
+
"The conference will be held next Monday at the main auditorium, and all employees are expected to attend the presentation.",
|
| 41 |
+
"Learning a new language opens up so many opportunities for travel, career advancement, and making new friends from different cultures.",
|
| 42 |
+
"Could you please explain the process step by step so that I can understand it better and apply it correctly in my work?",
|
| 43 |
+
]
|
| 44 |
+
|
| 45 |
+
def get_random_phrase():
|
| 46 |
+
"""Retorna frase aleatória com distribuição realista"""
|
| 47 |
+
r = random.random()
|
| 48 |
+
if r < 0.4: # 40% frases curtas
|
| 49 |
+
return random.choice(PHRASES_SHORT)
|
| 50 |
+
elif r < 0.8: # 40% frases médias
|
| 51 |
+
return random.choice(PHRASES_MEDIUM)
|
| 52 |
+
else: # 20% frases longas
|
| 53 |
+
return random.choice(PHRASES_LONG)
|
| 54 |
+
|
| 55 |
+
async def main():
|
| 56 |
+
print("=" * 70)
|
| 57 |
+
print("TESTE REALISTA DE STREAMING - FRASES VARIADAS")
|
| 58 |
+
print("=" * 70)
|
| 59 |
+
|
| 60 |
+
print("\n[1] Carregando modelo...")
|
| 61 |
+
tokenizer = AutoTokenizer.from_pretrained("canopylabs/orpheus-3b-0.1-ft")
|
| 62 |
+
|
| 63 |
+
engine_args = AsyncEngineArgs(
|
| 64 |
+
model="canopylabs/orpheus-3b-0.1-ft",
|
| 65 |
+
dtype="bfloat16",
|
| 66 |
+
max_model_len=4096,
|
| 67 |
+
gpu_memory_utilization=0.95,
|
| 68 |
+
max_num_seqs=256,
|
| 69 |
+
enable_chunked_prefill=True,
|
| 70 |
+
enable_prefix_caching=True,
|
| 71 |
+
enforce_eager=False,
|
| 72 |
+
)
|
| 73 |
+
engine = AsyncLLMEngine.from_engine_args(engine_args)
|
| 74 |
+
|
| 75 |
+
sampling_params = SamplingParams(
|
| 76 |
+
temperature=0.2,
|
| 77 |
+
top_p=0.9,
|
| 78 |
+
max_tokens=4096,
|
| 79 |
+
stop_token_ids=[STOP_TOKEN],
|
| 80 |
+
repetition_penalty=1.1,
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
def format_prompt(text, voice="tara"):
|
| 84 |
+
adapted_prompt = f"{voice}: {text}"
|
| 85 |
+
prompt_tokens = tokenizer(adapted_prompt, return_tensors="pt")
|
| 86 |
+
start_token = torch.tensor([[START_TOKEN]], dtype=torch.int64)
|
| 87 |
+
end_tokens = torch.tensor([END_TOKENS], dtype=torch.int64)
|
| 88 |
+
all_input_ids = torch.cat([start_token, prompt_tokens.input_ids, end_tokens], dim=1)
|
| 89 |
+
return tokenizer.decode(all_input_ids[0])
|
| 90 |
+
|
| 91 |
+
async def simulate_streaming(text, user_id):
|
| 92 |
+
"""
|
| 93 |
+
Simula streaming real - verifica se cada chunk chega a tempo
|
| 94 |
+
Retorna True se streaming foi fluido, False se travou
|
| 95 |
+
"""
|
| 96 |
+
prompt_string = format_prompt(text)
|
| 97 |
+
start = time.time()
|
| 98 |
+
|
| 99 |
+
ttff = None # Time to first frame
|
| 100 |
+
audio_tokens = 0
|
| 101 |
+
last_frame_time = None
|
| 102 |
+
frame_gaps = [] # Tempo entre frames
|
| 103 |
+
max_gap = 0
|
| 104 |
+
frames_generated = 0
|
| 105 |
+
|
| 106 |
+
# Cada frame de 7 tokens = ~23ms de áudio
|
| 107 |
+
# Se o gap entre frames > 23ms, o áudio vai travar
|
| 108 |
+
FRAME_DURATION = 0.023 # 23ms por frame
|
| 109 |
+
|
| 110 |
+
async for output in engine.generate(prompt_string, sampling_params, f"user_{user_id}"):
|
| 111 |
+
current_audio = sum(1 for t in output.outputs[0].token_ids if t >= AUDIO_TOKEN_BASE)
|
| 112 |
+
current_frames = current_audio // 7
|
| 113 |
+
|
| 114 |
+
# Novo frame gerado?
|
| 115 |
+
if current_frames > frames_generated:
|
| 116 |
+
now = time.time()
|
| 117 |
+
|
| 118 |
+
if frames_generated == 0:
|
| 119 |
+
ttff = now - start
|
| 120 |
+
last_frame_time = now
|
| 121 |
+
else:
|
| 122 |
+
gap = now - last_frame_time
|
| 123 |
+
# Normalizar pelo número de frames gerados de uma vez
|
| 124 |
+
new_frames = current_frames - frames_generated
|
| 125 |
+
gap_per_frame = gap / new_frames
|
| 126 |
+
frame_gaps.append(gap_per_frame)
|
| 127 |
+
max_gap = max(max_gap, gap_per_frame)
|
| 128 |
+
last_frame_time = now
|
| 129 |
+
|
| 130 |
+
frames_generated = current_frames
|
| 131 |
+
|
| 132 |
+
total_time = time.time() - start
|
| 133 |
+
audio_duration = frames_generated * FRAME_DURATION
|
| 134 |
+
|
| 135 |
+
# Streaming travou se algum gap foi maior que o dobro da duração do frame
|
| 136 |
+
# (dando margem para buffering)
|
| 137 |
+
BUFFER_MARGIN = 3.0 # 3x a duração do frame como margem
|
| 138 |
+
streaming_ok = max_gap < (FRAME_DURATION * BUFFER_MARGIN) if frame_gaps else True
|
| 139 |
+
|
| 140 |
+
avg_gap = sum(frame_gaps) / len(frame_gaps) if frame_gaps else 0
|
| 141 |
+
|
| 142 |
+
return {
|
| 143 |
+
'user_id': user_id,
|
| 144 |
+
'text_len': len(text),
|
| 145 |
+
'ttff': ttff or 0,
|
| 146 |
+
'total_time': total_time,
|
| 147 |
+
'audio_duration': audio_duration,
|
| 148 |
+
'frames': frames_generated,
|
| 149 |
+
'avg_gap': avg_gap,
|
| 150 |
+
'max_gap': max_gap,
|
| 151 |
+
'streaming_ok': streaming_ok,
|
| 152 |
+
'rtf': total_time / audio_duration if audio_duration > 0 else 999,
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
print("\n[2] Warmup...")
|
| 156 |
+
await simulate_streaming("Hello world.", 0)
|
| 157 |
+
|
| 158 |
+
print("\n[3] Testando streaming realista...")
|
| 159 |
+
print("=" * 70)
|
| 160 |
+
|
| 161 |
+
results_summary = []
|
| 162 |
+
|
| 163 |
+
for num_users in [32, 64, 100, 128, 150, 200, 256]:
|
| 164 |
+
print(f"\n>>> {num_users} USUÁRIOS COM FRASES VARIADAS <<<")
|
| 165 |
+
|
| 166 |
+
# Gerar frases aleatórias para cada usuário
|
| 167 |
+
user_phrases = [get_random_phrase() for _ in range(num_users)]
|
| 168 |
+
|
| 169 |
+
# Mostrar distribuição
|
| 170 |
+
short = sum(1 for p in user_phrases if len(p) < 20)
|
| 171 |
+
medium = sum(1 for p in user_phrases if 20 <= len(p) < 80)
|
| 172 |
+
long = sum(1 for p in user_phrases if len(p) >= 80)
|
| 173 |
+
print(f" Distribuição: {short} curtas, {medium} médias, {long} longas")
|
| 174 |
+
|
| 175 |
+
try:
|
| 176 |
+
start_batch = time.time()
|
| 177 |
+
tasks = [simulate_streaming(phrase, i) for i, phrase in enumerate(user_phrases)]
|
| 178 |
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
| 179 |
+
batch_time = time.time() - start_batch
|
| 180 |
+
|
| 181 |
+
errors = [r for r in results if isinstance(r, Exception)]
|
| 182 |
+
successful = [r for r in results if not isinstance(r, Exception)]
|
| 183 |
+
|
| 184 |
+
if errors:
|
| 185 |
+
print(f" ERROS: {len(errors)}")
|
| 186 |
+
|
| 187 |
+
if successful:
|
| 188 |
+
streaming_ok = [r for r in successful if r['streaming_ok']]
|
| 189 |
+
streaming_bad = [r for r in successful if not r['streaming_ok']]
|
| 190 |
+
|
| 191 |
+
max_gaps = [r['max_gap'] * 1000 for r in successful]
|
| 192 |
+
ttffs = [r['ttff'] * 1000 for r in successful]
|
| 193 |
+
rtfs = [r['rtf'] for r in successful]
|
| 194 |
+
|
| 195 |
+
print(f" Streaming OK: {len(streaming_ok)}/{len(successful)}")
|
| 196 |
+
print(f" TTFF: min={min(ttffs):.0f}ms, max={max(ttffs):.0f}ms, avg={sum(ttffs)/len(ttffs):.0f}ms")
|
| 197 |
+
print(f" Max Gap: min={min(max_gaps):.0f}ms, max={max(max_gaps):.0f}ms, avg={sum(max_gaps)/len(max_gaps):.0f}ms")
|
| 198 |
+
print(f" RTF: min={min(rtfs):.3f}, max={max(rtfs):.3f}, avg={sum(rtfs)/len(rtfs):.3f}")
|
| 199 |
+
|
| 200 |
+
if streaming_bad:
|
| 201 |
+
print(f" ⚠️ STREAMING TRAVOU para {len(streaming_bad)} usuários!")
|
| 202 |
+
worst = max(streaming_bad, key=lambda x: x['max_gap'])
|
| 203 |
+
print(f" Pior caso: user_{worst['user_id']} com gap de {worst['max_gap']*1000:.0f}ms")
|
| 204 |
+
else:
|
| 205 |
+
print(f" ✓ Streaming fluido para TODOS os usuários!")
|
| 206 |
+
|
| 207 |
+
results_summary.append({
|
| 208 |
+
'users': num_users,
|
| 209 |
+
'ok': len(streaming_ok),
|
| 210 |
+
'bad': len(streaming_bad),
|
| 211 |
+
'max_gap_worst': max(max_gaps),
|
| 212 |
+
'ttff_max': max(ttffs),
|
| 213 |
+
'rtf_max': max(rtfs),
|
| 214 |
+
})
|
| 215 |
+
|
| 216 |
+
except Exception as e:
|
| 217 |
+
print(f" CRASH: {type(e).__name__}: {str(e)[:80]}")
|
| 218 |
+
break
|
| 219 |
+
|
| 220 |
+
print("\n" + "=" * 70)
|
| 221 |
+
print("RESUMO - TESTE DE STREAMING REALISTA")
|
| 222 |
+
print("=" * 70)
|
| 223 |
+
|
| 224 |
+
print("\n| Users | Stream OK | Travou | Max Gap | TTFF Max | RTF Max |")
|
| 225 |
+
print("|-------|-----------|--------|---------|----------|---------|")
|
| 226 |
+
for r in results_summary:
|
| 227 |
+
status = "✓" if r['bad'] == 0 else "⚠️"
|
| 228 |
+
print(f"| {r['users']:5} | {r['ok']:9} | {r['bad']:6} | {r['max_gap_worst']:6.0f}ms | {r['ttff_max']:7.0f}ms | {r['rtf_max']:7.3f} |")
|
| 229 |
+
|
| 230 |
+
# Encontrar limite
|
| 231 |
+
all_ok = [r for r in results_summary if r['bad'] == 0]
|
| 232 |
+
if all_ok:
|
| 233 |
+
max_ok = max(all_ok, key=lambda x: x['users'])
|
| 234 |
+
print(f"\n>>> MÁXIMO SEM TRAVAMENTO: {max_ok['users']} usuários <<<")
|
| 235 |
+
|
| 236 |
+
some_bad = [r for r in results_summary if r['bad'] > 0]
|
| 237 |
+
if some_bad:
|
| 238 |
+
first_bad = min(some_bad, key=lambda x: x['users'])
|
| 239 |
+
print(f">>> PRIMEIRO TRAVAMENTO: {first_bad['users']} usuários ({first_bad['bad']} travaram) <<<")
|
| 240 |
+
|
| 241 |
+
print("=" * 70)
|
| 242 |
+
|
| 243 |
+
if __name__ == "__main__":
|
| 244 |
+
asyncio.run(main())
|