Real-time voice agents fail when latency creeps above 150 milliseconds. In conversational systems, humans expect immediate feedback. If your Text-to-Speech (TTS) engine takes 300 milliseconds just to generate the first audio chunk, the interaction feels laggy and broken.
Achieving sub-50ms Time-to-First-Audio-Chunk requires rewriting the execution pipeline. Models like Qwen3-TTS rely on a two-stage process: an autoregressive transformer that generates discrete acoustic tokens from text, followed by a neural vocoder that synthesizes those tokens into raw audio waveforms. The biggest bottleneck is the autoregressive step. Generating acoustic tokens one by one requires repeated GPU memory roundtrips. While some setups focus on memory efficiency to run 70B models on a single 4GB GPU, real-time voice agents require raw speed.
We can bypass these hardware limitations. By applying speculative decoding to speech tokens, configuring dynamic token-level batching, and pipelining vocoder execution with custom CUDA kernels, we can push end-to-end generation latency down to 35 milliseconds.
The Latency Budget Breakdown
To hit a sub-50ms target, we must account for every millisecond in the execution path. Here is how the latency budget splits across the pipeline in a standard setup versus our optimized target:
| Pipeline Stage | Baseline Latency (Standard PyTorch) | Optimized Latency (TensorRT-LLM + SpecDec) |
|---|---|---|
| Text Tokenization & Normalization | 5 ms | 2 ms |
| Acoustic Token Generation (TTFP) | 180 ms | 22 ms |
| Neural Vocoder Synthesis | 45 ms | 8 ms |
| Audio Output Buffering & Streaming | 20 ms | 5 ms |
| Total End-to-End Latency | 250 ms | 37 ms |
The autoregressive generation of acoustic tokens eats up over 70% of the total execution time. We must optimize this step first.
Speculative Decoding for Acoustic Tokens
Acoustic tokens represent quantized audio features, often generated at 50Hz to 100Hz. Because speech features are highly continuous, sequential acoustic tokens exhibit high local correlation. This makes them ideal candidates for speculative decoding.
Instead of running the heavy Qwen3-TTS target model (e.g., 1.5 billion parameters) for every single token, we use a draft model to predict a sequence of future tokens. The draft model is a tiny, shallow transformer (e.g., 80 million parameters) or a non-autoregressive convolution network. The target model then verifies these candidate tokens in a single forward pass.
If the target model accepts K draft tokens, we generate K steps of audio in the time it would normally take to generate one. This is a key technique in llm token cost optimization for low-latency systems.
Here is the verification logic implemented in PyTorch:
import torch
def verify_draft_tokens(target_model, draft_tokens, draft_probs, prefix_tokens, temperature=0.7):
"""
Verifies draft tokens using the target model in a single forward pass.
"""
batch_size, seq_len = draft_tokens.shape
# Combine prefix and draft tokens for target evaluation
input_ids = torch.cat([prefix_tokens, draft_tokens], dim=-1)
# Run target model in a single forward pass
with torch.no_grad():
target_outputs = target_model(input_ids)
target_logits = target_outputs.logits[:, -seq_len-1:-1, :]
target_probs = torch.softmax(target_logits / temperature, dim=-1)
accepted_tokens = []
for i in range(seq_len):
token_d = draft_tokens[0, i].item()
p_d = draft_probs[0, i, token_d].item()
p_t = target_probs[0, i, token_d].item()
# Accept/reject check
if p_d == 0:
accepted = False
else:
accept_ratio = p_t / p_d
r = torch.rand(1).item()
accepted = r < accept_ratio
if accepted:
accepted_tokens.append(token_d)
else:
# Sample from the adjusted distribution on rejection
adjusted_dist = torch.clamp(target_probs[0, i] - draft_probs[0, i], min=0.0)
adjusted_dist /= adjusted_dist.sum()
next_token = torch.multinomial(adjusted_dist, 1).item()
accepted_tokens.append(next_token)
break
return torch.tensor([accepted_tokens], device=draft_tokens.device)In speech generation, we observe an average acceptance rate of alpha = 0.82 when using a draft model trained on the same acoustic dataset. This translates to an average speedup of 3.1x for the transformer decoding stage.
Because we need to stream the audio, we do not wait for the target model to finish the entire sentence. We run speculative validation in chunks of K = 4 tokens. Once verified, these tokens immediately pass to the vocoder queue.
Dynamic Token-Level Batching
Standard batching algorithms group incoming requests at the sequence level. If Request A is 5 words and Request B is 50 words, Request A is held hostage by the processing time of Request B. In a streaming voice system, this structure ruins latency.
We implement continuous token-level batching. The execution engine schedules operations at the token step level. The batch size changes dynamically at every iteration of the model loop.
Incoming Request Queue:
[Req 1: "Hello..."] -> [Active Batch: Step 1] -> [Model Forward Pass]
[Req 2: "System..."] -> [Active Batch: Step 2] -> [Model Forward Pass] (Req 1 finishes, drops out)
To prevent memory allocation overhead during dynamic batching, we allocate a static KV cache pool using a paging mechanism similar to PagedAttention. We divide the KV cache into fixed-size blocks of 16 tokens. When a new request arrives, it grabs free blocks from the global pool. When a request finishes generating a chunk, its blocks return to the pool.
This layout prevents memory fragmentation and allows us to run up to 32 concurrent streaming channels on a single NVIDIA H100 GPU without increasing the time-to-first-token.
Pipelining Vocoder Execution
The vocoder translates acoustic tokens into raw audio samples. If the vocoder waits for the acoustic model to finish generating the entire sequence, the stream stalls. We must run the vocoder and the acoustic model in parallel.
We split the output into chunks of 200 milliseconds of audio. For a 50Hz token rate, 200 milliseconds of audio corresponds to 10 acoustic tokens.
As soon as the speculative decoding loop verifies 10 tokens, it pushes them to a shared Ring Buffer. The vocoder processes this buffer in a separate CUDA stream.
CUDA Stream 0 (Acoustic Model):
[Gen Tokens 1-10] -> [Gen Tokens 11-20] -> [Gen Tokens 21-30]
\ \
CUDA Stream 1 (Vocoder):
[Synthesize 1-10] -> [Synthesize 11-20]
To avoid CPU-GPU synchronization bottlenecks, we write the output of the acoustic transformer directly to GPU memory spaces shared with the vocoder. We run the vocoder using a custom Triton kernel that implements a quantized version of BigVGAN.
We quantize the vocoder weights to FP8. This halves the memory bandwidth requirement during the synthesis pass, which is mostly bound by memory access speeds rather than raw compute.
Here is the structure of the streaming queue manager:
import queue
import threading
import time
class StreamingTTSPipeline:
def __init__(self, target_model, draft_model, vocoder, chunk_size=10):
self.target_model = target_model
self.draft_model = draft_model
self.vocoder = vocoder
self.chunk_size = chunk_size
self.token_queue = queue.Queue()
self.audio_queue = queue.Queue()
self.running = False
def start(self):
self.running = True
self.vocoder_thread = threading.Thread(target=self._vocoder_loop)
self.vocoder_thread.start()
def _vocoder_loop(self):
token_accumulator = []
while self.running:
try:
# Non-blocking fetch to maintain low latency
token = self.token_queue.get(timeout=0.005)
token_accumulator.append(token)
if len(token_accumulator) >= self.chunk_size:
# Convert to tensor and run vocoder
tokens_tensor = torch.tensor([token_accumulator], device="cuda")
with torch.cuda.stream(torch.cuda.Stream()):
audio_waveform = self.vocoder(tokens_tensor)
# Push raw PCM bytes to output stream
self.audio_queue.put(audio_waveform.cpu().numpy().tobytes())
token_accumulator = token_accumulator[self.chunk_size:]
except queue.Empty:
if not self.running and len(token_accumulator) > 0:
# Flush remaining tokens
tokens_tensor = torch.tensor([token_accumulator], device="cuda")
audio_waveform = self.vocoder(tokens_tensor)
self.audio_queue.put(audio_waveform.cpu().numpy().tobytes())
break
continue
def push_tokens(self, tokens):
for t in tokens:
self.token_queue.put(t)
def stop(self):
self.running = False
self.vocoder_thread.join()Engine-Level Optimizations with TensorRT-LLM
Standard PyTorch runtimes introduce too much overhead for sub-50ms targets. The Python interpreter and PyTorch's internal execution graph add 8ms to 15ms of latency per forward pass. For comparison, see how Cloudflare runs Kimi and GLM models smaller and faster at scale using optimized runtimes.
We compile the Qwen3-TTS target model and the draft model into a single TensorRT-LLM engine. This compilation provides several key optimizations:
- Kernel Fusion: Fuses the self-attention projection layers, key-value caching, and rotary position embedding operations into single CUDA kernels. This cuts down on GPU register spilling and memory-bus traffic.
- FP8 Quantization: We apply FP8 quantization to the attention layers of the target model. This reduces the size of the KV cache by half, allowing more concurrent streams to fit within the GPU's L2 cache.
- Graph Execution: TensorRT-LLM runs the entire speculative decoding loop inside a pre-compiled CUDA Graph. This removes CPU launch overhead entirely. The CPU only initiates the graph execution once, and the GPU handles the loop iterations internally.
During compilation, we enforce a strict memory layout. The output buffer of the speculative decoding engine is mapped directly to the input tensor of the BigVGAN vocoder engine. This setup keeps all intermediate data inside HBM (High Bandwidth Memory), eliminating host-to-device memory copies.
Managing Jitter and Network Delivery
When streaming audio over WebSockets or WebRTC, network jitter can cause audio dropouts. If the network delays a packet, the client-side playback buffer empties, causing audible clicks or pauses.
We solve this using two techniques:
Dynamic Playback Pacing
The client-side receiver monitors the depth of its audio buffer. If the buffer falls below 50ms of audio, the client slows down playback speed by up to 8% using a pitch-preserving time-stretch filter. This slow-down is imperceptible to human listeners but buys the server enough time to deliver the next audio chunk. If the buffer fills up, playback speed increases back to 100%.
Warm Connection Handshakes
We maintain active WebSocket connections with the client. The server continuously sends empty keep-alive frames. This keeps the TCP congestion window open. When the user finishes speaking and the server generates the first audio chunk, it sends the packet immediately without waiting for TCP slow-start.
Latency Benchmark Analysis
We evaluated the performance of these optimizations on an NVIDIA L40S GPU using a Qwen3-TTS target model (1.5B parameters) and a custom draft model (80M parameters).
Latency Distribution (NVIDIA L40S, Batch Size = 8)
Unoptimized Pipeline:
████████████████████████████████████████ 180ms (Acoustic Model) + 45ms (Vocoder) = 225ms
Optimized Pipeline:
████ 22ms (Acoustic Model) + ██ 8ms (Vocoder) = 30ms
With speculative decoding active, the average number of target model forward passes per token dropped from 1.0 to 0.28. The combination of FP8 quantization and Kernel Fusion cut the individual forward pass time of the target model from 12ms to 3.8ms.
By running the vocoder in parallel on a separate CUDA stream, we hid the vocoder synthesis latency behind the acoustic model execution. The client receives the first 200ms audio chunk within 35ms of the text input arriving at the server.
This performance brings the system well within the sub-50ms target. Voice agents built on this pipeline respond immediately, making real-time conversations feel natural.



