BreezeRT: TTS com latência ultrabaixa

Como o BreezeRT reduz a latência do primeiro áudio do Breeze TTS 2 com escalonamento por etapas, blocos progressivos, CUDA Graphs e prazos de reprodução.

BreezeRT: TTS com latência ultrabaixa

O BreezeRT mantém baixa a latência do primeiro pacote do Breeze TTS 2 sob alta concorrência. Reduzimos a espera no escalonamento de solicitações, na divisão do áudio em blocos e na execução na GPU, e usamos o Predictor para escalonar o primeiro áudio e os blocos seguintes. Novas solicitações começam rapidamente, enquanto a fala em andamento continua sendo reproduzida.

Visão geral das otimizações do BreezeRT

OtimizaçãoEfeito
1. Escalonamento unificado de múltiplas etapasMenos espera
2. Blocos progressivos e decodificação incrementalTTFB da primeira solicitação: ~2500 → 150 ms
3. Compilação do Depth e CUDA GraphsTTFB da primeira solicitação: ~150 → 100 → 35 ms
4. Predictor: prazos do primeiro áudio e da reproduçãoSolicitações com possíveis pausas em um total de 300: 14 → 0

Comparação de frameworks de inferência

Todos os testes deste artigo foram realizados em uma H100. Comparamos PyTorch, vLLM-Omni, SGLang-Omni e BreezeRT com o mesmo modelo Breeze TTS 2, com 1–16 solicitações simultâneas, focando na latência do primeiro pacote.

Latência do primeiro áudio p50 e p95 com 1–16 solicitações simultâneas em uma H100.
Latência do primeiro áudio (TTFB), p50 e p95. Quanto menor, melhor.

Adaptamos e ajustamos PyTorch, vLLM-Omni e SGLang-Omni para o Breeze TTS 2. O gráfico mostra os melhores resultados que medimos com cada framework. Nossas adaptações ainda não foram incorporadas aos respectivos repositórios de código aberto.

1. Escalonamento unificado de múltiplas etapas

O Breeze TTS 2 primeiro codifica o texto e executa o Backbone Prefill. Para cada quadro de fala, o Backbone prevê o primeiro código de áudio, o Depth Decoder gera os 15 códigos restantes e o Codec produz as amostras da forma de onda. Cada etapa tem diferentes requisitos de processamento, frequências de execução e tamanhos de lote adequados.

Solicitações simultâneas costumam estar em etapas diferentes: A tem códigos de áudio prontos para decodificar, B está gerando seu próximo quadro e C acabou de chegar e espera pelo primeiro áudio. Continuar com A deixaria B e C esperando. O escalonador precisa alternar entre solicitações antes que uma frase inteira seja concluída.

O BreezeRT escalona separadamente o processamento de texto, Backbone Prefill, Backbone Decode, Depth e a decodificação de áudio. Um único ambiente de execução gerencia o progresso das solicitações, os caches e o áudio pendente. Após cada etapa, escolhe qual etapa e quais solicitações executar em seguida.

Linha do tempo da GPU com solicitações compartilhando as etapas Backbone, Depth Decoder e Codec.
As solicitações são executadas em etapas na GPU e retomam a geração após cada bloco de áudio.

O escalonador seleciona solicitações por prioridade e depois agrupa o trabalho em lotes dentro da etapa escolhida. Cada etapa tem seu próprio limite de lote, permitindo que Codec e Backbone usem tamanhos de lote diferentes. Novas solicitações podem entrar entre etapas, e fluxos ativos recebem tempo para decodificar. O mesmo ambiente de execução cuida da divisão em blocos, da execução de grafos capturados e do escalonamento por prazos.

2. Blocos progressivos e decodificação incremental

Um primeiro bloco maior faz o usuário esperar por mais quadros antes de ouvir áudio. Porém, enviar um quadro por vez exige executar o Codec e o escalonador com mais frequência. Esse custo se acumula à medida que as solicitações aumentam.

O BreezeRT aumenta gradualmente o tamanho dos blocos, usando, por exemplo, 1, 2, 4, 8 e 16 quadros. Blocos pequenos iniciam a reprodução rapidamente. Quando já há algum áudio no buffer, blocos maiores reduzem o número de chamadas de decodificação.

O Codec também precisa lembrar o trabalho anterior para não decodificar o mesmo áudio novamente. O BreezeRT armazena, para cada solicitação, os KV do Transformer com janela deslizante, o histórico de convolução causal e a cauda de sobreposição da convolução transposta, e então processa apenas os novos quadros. Esses caches preservam o contexto e suavizam as transições entre blocos quando seus tamanhos mudam.

Na comparação de apoio, a primeira solicitação recebeu áudio após cerca de 2500 ms com um primeiro bloco fixo de 16 quadros e cerca de 150 ms com blocos progressivos.

3. Compilação do Depth e CUDA Graphs

Com um primeiro bloco menor, o custo de lançamento pela CPU fica mais evidente. Gerar um quadro de fala exige muitas operações do modelo. Mesmo uma GPU rápida pode ficar ociosa entre envios. O BreezeRT reduz o custo de processamento por meio de compilação e usa CUDA Graphs para enviar operações de GPU capturadas em uma única execução do grafo.

A execução de CUDA Graphs exige formatos fixos de tensores, mas os comprimentos de texto, os tamanhos de lote e os tamanhos dos blocos de áudio variam. Capturar todas as combinações consumiria memória demais. O BreezeRT mantém um conjunto de formatos comuns e divide ou preenche as entradas para ajustá-las a eles.

O Text Encoder compacta tokens e divide o trabalho por quantidade de solicitações e capacidade de tokens para reduzir o preenchimento. A atenção permanece separada para cada solicitação. O Backbone Prefill trabalha em blocos de tokens, com KV paginados armazenando o histórico de geração de cada solicitação para reutilizar grafos e buffers existentes.

A codificação de texto compactado e o Backbone Prefill em blocos reutilizam CUDA Graphs de formato fixo.
Texto compactado e Prefill em blocos permitem que entradas de diferentes comprimentos reutilizem CUDA Graphs.

O Depth Decoder gera mais 15 códigos de áudio por quadro. O BreezeRT compila a pilha do decodificador e as cabeças de saída como unidades completas, fundindo operações compatíveis elemento a elemento em menos kernels. Isso reduz leituras e gravações de tensores intermediários e lançamentos de kernels, um trabalho repetido que se acumula entre solicitações simultâneas. Os buffers de KV, posição e entrada/saída são alocados antecipadamente. O laço autorregressivo completo é capturado em um único grafo, de modo que uma execução realiza todos os 15 passos na ordem de dependência.

Quinze passos do Depth conduzidos pelo host em comparação com uma única execução de grafo do BreezeRT.
Uma única execução do grafo envia o laço completo de 15 passos do Depth.

A codificação de texto, Backbone, Depth e a decodificação de áudio usam grafos, com os formatos comuns aquecidos na inicialização. Cada solicitação mantém suas próprias configurações de amostragem e estado aleatório. Sem alterar a divisão em blocos, a comparação de apoio mediu uma latência da primeira solicitação de cerca de 150 ms, que caiu para 100 ms com a compilação do Depth e 35 ms com grafos em todas as etapas.

4. Predictor: prazos do primeiro áudio e da reprodução

Novas solicitações esperam para começar a falar; fluxos ativos esperam pelo próximo bloco. Atender sempre às novas solicitações primeiro pode interromper a reprodução. Gerar com antecedência excessiva para fluxos ativos faz as novas solicitações esperarem. O Predictor usa prazos de reprodução para decidir qual trabalho vem primeiro.

Novas solicitações recebem prioridade de início. Para fluxos ativos, o momento da primeira saída e a duração total do áudio fornecem uma estimativa do prazo de reprodução. O Predictor registra os tempos recentes das etapas por tamanho de lote e inclui uma margem ao estimar se o trabalho restante de Backbone, Depth e Codec pode terminar a tempo.

Por exemplo, A está pronta para decodificar, enquanto B precisa de mais um passo de geração para se juntar ao seu lote. O escalonador espera por B se o processamento em lote economizar tempo e ainda cumprir o prazo de reprodução de A. Caso contrário, A é executada primeiro. Se o bloco desejado não puder ser concluído a tempo, o ambiente de execução pode enviar os quadros já gerados para evitar que o player fique esperando.

O Predictor usa uma estimativa simples de tempo. Em uma comparação com 300 solicitações, as solicitações com possíveis pausas na reprodução caíram de 14 para 0. O buffer adicional máximo necessário para evitar uma pausa caiu de cerca de 330 ms para 0 ms.