Engineering · Speech Systems

Optimising autoregressive speech models

Autoregressive speech models generate audio token by token. Expressive, but painfully slow. Here is every major lever we've pulled to get them to real time — ranked by effort.

Read time
24 min
Techniques
11
Max speedup
8—15×
Published
Apr 2026
UR
Udeshya Raj
Speech Systems Engineering

Autoregressive (AR) speech models generate speech token-by-token — each token depending on every token before it. This sequential dependency makes them expressive but painfully slow. A 10-second utterance might take 5+ seconds to generate, blowing past any real-time requirement.

This post covers every major optimisation axis we've explored — from quick wins like mixed-precision inference to deep architectural changes like distillation and RMSNorm swaps. Each section explains the technique, why it matters for AR speech specifically, and practical gotchas you'll hit.

We've organised them by implementation effort so you can pick the right optimisations for your timeline and team.

§00 Context

The open-source AR speech model landscape

Before diving into optimisations, here's every major open-source autoregressive (or hybrid-AR) speech model worth knowing about. These are the models these techniques apply to.

GPT-style autoregressive models

Text tokens in, speech tokens out, one at a time.

ModelOrgParamsArchitectureLanguagesKey strength
XTTS v2Coqui.ai~500MGPT-2 decoder + DVAE + HiFi-GAN vocoder16Simple, proven voice cloning from 6s reference
Tortoise-TTSJames Betker~500MAR acoustic model + diffusion decoder + UnivNetENExpressiveness, speaker interpolation
BarkSuno80M / 300M3-stage GPT pipeline + EnCodecMultiGeneralist audio (speech + music + effects)
VALL-E XCommunity (MS design)~300MCodec language model (AR + NAR stages)EN / ZH / JACross-lingual zero-shot voice cloning

LLM-based speech models (2024—2026 generation)

The new wave — fine-tuned LLMs that treat speech as just another token modality.

ModelOrgParamsArchitectureLanguagesKey strength
Fish Speech v1.5FishAudio~500MDual-AR with dual transformerEN / ZH / JA +Highest TTS Arena ELO (1339), 10M+ hours training
Qwen3-TTSAlibaba (Qwen)0.6B / 1.7BDual-track LM + multi-token prediction1097ms TTFB streaming, description-based voice control
CosyVoice 2.0/3.0FunAudioLLM~500MLLM + FSQ + causal flow matchingMulti150ms streaming, best naturalness
Orpheus TTSCanopy AI~1BLLaMA fine-tuneENEmpathetic / emotional synthesis
Sesame CSMSesame Labs1BLLaMA-based conversational speech modelENTurn-taking, backchannel, conversational flow
Dia / Dia2Nari Labs1B / 2BDialogue-focused ARENMulti-speaker dialogue, nonverbal sounds
OuteTTSOuteAI350M / 1BPure LM (LLaMA-based) + WavTokenizer + CTC6llama.cpp compatible, on-device inference
LlasaResearch1B / 3B / 8BUnified AR transformerEN / ZHScales from 1B to 8B, unified framework
SparkTTSSparkAudio0.5BQwen2.5 LLM + single-stream speech tokensEN / ZHCode-switching, Triton-ready serving
Parler-TTSHugging Face880M / 2.3BFlan-T5 encoder + AR decoder LM + DACENText-controllable (gender, rate, pitch, reverb)
FireRedTTS2FireRedTeam~1BLLM-based foundation TTS7Long-form streaming multi-speaker dialogue
IndexTTS2Index Team~500MLLM-based (XTTS/Tortoise lineage)EN / ZHPrecise duration control, emotional disentanglement
VoxCPM2OpenBMB2BTokeniser-free diffusion-AR in AudioVAE latent3048kHz studio quality, 2M+ hours training
ChatTTS2noise~300MGPT-style, conversational-optimisedEN / ZHToken-level control (laughter, pauses)

Non-autoregressive / hybrid models (for comparison)

Don't decode token-by-token, so not all AR optimisations apply — but many share components where these techniques still help.

ModelOrgParamsArchitectureLanguagesKey strength
F5-TTSSwividAI~300MDiffusion transformer (DiT) + flow matchingMultiRTF 0.15, non-autoregressive
KokoroHexGrad82MStyleTTS2-based9Tiny, highest MOS (4.2), Apache 2.0
MeloTTSMyShell~100MVITS/VITS2 + multilingual BERT6+CPU real-time, lightweight
StyleTTS 2Research~100MVITS + style diffusionENStyle transfer, high quality
WhisperSpeechCommunity~300MInverted Whisper + EnCodec + VocosMultiLeverages Whisper representations
MARS5CAMB.AI~500MProsody-focused hybrid ARENProsody preservation (sports, anime)

Which optimisations apply to which model type?

TechniqueGPT-style ARLLM-based ARNAR / hybrid
FP16 / BF16
SDPA / Flash Attention
DeepSpeed
torch.compile / CUDA Graphs
TensorRT
INT8 / INT4 quantisation
vLLM
RMSNorm swap✓ (GPT-2)— already
Distillation
Triton kernels
Note. LLM-based models (Orpheus, Sesame, OuteTTS, Llasa) already use RMSNorm since they're built on LLaMA. The RMSNorm swap applies mainly to GPT-2/BERT-style models like XTTS and Tortoise.
Tier 01
Easy effort

Easy effort

Time
Minutes—hours
Training
Not required
Risk
Very low
These are near-drop-in changes. If you haven't done these yet, stop reading and go do them now.
§01 Precision
1.5—2×

FP16 / BF16 quantisation

Speedup 1.5—2× · Effort ~10 minutes · Risk Near zero

What it is

Quantisation reduces the numerical precision of model weights and activations — FP32 to FP16 or BF16. Fewer bits means less memory bandwidth consumed per token, and bandwidth is the bottleneck in autoregressive decoding.

Why it matters for AR speech

AR decoding is memory-bandwidth bound, not compute bound. Each forward pass reads the entire model's weights to produce a single token. Halving the precision roughly doubles the throughput.

Precision levels

PrecisionBitsMemorySpeedupQuality impact
FP3232BaselineNone
FP16161.5—2×Negligible
BF16161.5—2×Negligible

How to do it

# Option 1: Cast model to FP16 (simplest)
model = model.half().cuda()

# Option 2: Use torch autocast (more flexible)
with torch.cuda.amp.autocast(dtype=torch.float16):
    output = model.generate(input_ids)

# Option 3: BF16 (better numerical stability, requires Ampere+)
model = model.to(dtype=torch.bfloat16).cuda()

Practical notes

  • FP16 is the safe default. On our XTTS v2 (30-layer GPT2, 1024-dim, 16 heads), FP16 TensorRT gave a 2× speedup over FP32 PyTorch with zero quality degradation.
  • BF16 vs FP16: BF16 has wider dynamic range (same as FP32), fewer overflow issues. Use BF16 if you have Ampere+ GPUs (A100, L4, H100). Use FP16 otherwise.
  • There is literally no reason to run inference in FP32 in 2025. If your model is still FP32, fix this first.
§02 Attention
2—4×

Scaled dot-product attention (SDPA)

Speedup 2—4× on prefill · Effort ~30 minutes · Risk Near zero

What it is

torch.nn.functional.scaled_dot_product_attention is PyTorch's built-in attention dispatcher. It automatically routes to the fastest available backend: Flash Attention (if available and shapes are compatible), Memory-Efficient Attention (xFormers-style, broader shape support), or the Math fallback (standard matmul, always works).

Why it matters for AR speech

SDPA is the zero-effort optimisation. Replace your manual attention implementation with one function call and get Flash Attention (or the next best thing) automatically.

# Before (manual attention — 4 lines, slow)
attn_weights = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(head_dim)
attn_weights = attn_weights.masked_fill(causal_mask, float('-inf'))
attn_weights = F.softmax(attn_weights, dim=-1)
attn_output = torch.matmul(attn_weights, value)

# After (SDPA — 1 line, fast)
attn_output = F.scaled_dot_product_attention(query, key, value, is_causal=True)

SDPA vs Flash Attention directly

FeatureSDPAflash-attn library
Automatic backend selectionYesNo
Supports all GPUsYes (fallback)sm_80+ only
Custom attention biasesLimitedYes (v2.4+)
Maximum performance~95% of flash-attn100%
Ease of integrationTrivialModerate
Recommendation: Use SDPA unless you need features only available in the flash-attn library directly. It's a find-and-replace in your attention code.
§03 Attention · IO-aware
2—4×

Flash Attention

Speedup 2—4× on prefill, O(n) memory · Effort ~1 hour · Risk Low

What it is

Flash Attention (Dao et al.) rewrites the attention computation to be IO-aware — it tiles the computation to keep data in SRAM (on-chip) rather than repeatedly reading/writing to HBM (GPU main memory). It fuses the entire QKV → softmax → attention_output pipeline into a single kernel.

Why it matters for AR speech

During the prefill phase, AR speech models process the full conditioning context (speaker embeddings, text tokens, prompt audio). This can be 500—1000+ tokens. Standard attention on this is O(n²) in memory — Flash Attention brings it to O(n) memory with significant wall-clock speedup.

During decode, Flash Attention helps less (seq_len=1 per step), but the prefill speedup of 2—4× is substantial for TTFB (time to first byte).

Memory savings

Standard attention (seq_len=1024, heads=16, dim=64):
  Attention matrix: 1024 × 1024 × 16 × 4 bytes = 64 MB

Flash Attention:
  No materialised attention matrix
  Peak memory: O(seq_len) ~ negligible

Integration

# PyTorch 2.0+ native (recommended — see SDPA above)
from torch.nn.functional import scaled_dot_product_attention

# Or explicit flash-attn library for maximum control
pip install flash-attn --no-build-isolation

from flash_attn import flash_attn_func
output = flash_attn_func(q, k, v, causal=True)

Why this is "Easy" not "Medium"

If you're already using SDPA, you already have Flash Attention where possible. Installing the explicit flash-attn library is only needed when SDPA doesn't dispatch to Flash (custom biases, older PyTorch).

Gotchas

  • Causal masking is critical — AR models need causal=True or is_causal=True. Getting this wrong produces garbage output silently.
  • Flash Attention v2 requires sm_80+ (A100, L4, etc.). V100 and T4 need Flash Attention v1 or SDPA fallback.
  • Custom attention biases (ALiBi, relative position) need Flash Attention v2.4+ or custom implementations.
§04 Runtime · kernels
1.3—2×

DeepSpeed Inference

Speedup 1.3—2× · Effort ~1 hour · Risk Low

What it is

DeepSpeed Inference provides kernel injection, tensor parallelism, and an optimised inference pipeline. It replaces standard PyTorch transformer layers with hand-tuned CUDA kernels.

Why it matters for AR speech

  • Kernel injection replaces nn.Linear + nn.LayerNorm + attention with fused DeepSpeed kernels — similar benefit to TensorRT but without the ONNX export pain.
  • Tensor parallelism splits the model across multiple GPUs, reducing per-GPU memory bandwidth requirements.
  • Runs directly on PyTorch models — no ONNX export, no engine builds. Much faster iteration cycle.

How to do it

import deepspeed

model = deepspeed.init_inference(
    model,
    dtype=torch.float16,
    replace_with_kernel_inject=True,
    mp_size=1,  # tensor parallel degree
    replace_method="auto",
)

That's it. Three lines. DeepSpeed inspects the model, identifies standard transformer layers, and swaps in fused kernels.

When to use it

ScenarioRecommendation
Single GPU, max throughputTensorRT wins
Multi-GPU parallelism neededDeepSpeed
Fast iteration / prototypingDeepSpeed
Production with batchingTensorRT-LLM or vLLM

Gotchas

  • Kernel injection works best with standard transformer architectures. Custom attention patterns (like the conditioning in XTTS) may not be replaceable.
  • DeepSpeed's AR generation support is less mature than its training support. You may need to write a custom generate() loop.
  • The fused kernels don't always outperform TRT on single GPU — benchmark both.
speed
Tier 02
Medium effort

Medium effort

Time
Hours—days
Training
No (except QAT)
Risk
Moderate
These require meaningful engineering work — ONNX exports, infrastructure changes, or careful tuning — but don't need model retraining.
§05 Graph capture
15—40%

torch.compile + CUDA Graph capture

Speedup 15—40% decode · Effort 2—4 hours · Risk Medium

What it is

CUDA Graphs capture a sequence of GPU operations (kernel launches, memory copies) into a static graph that can be replayed without CPU involvement. This eliminates CPU-side overhead — kernel launch latency, Python overhead, framework dispatch.

torch.compile with mode="reduce-overhead" does this automatically plus additional graph-level optimisations.

Why it matters for AR speech

Each decode step in an AR model is a small, fast GPU operation — often finishing in <1ms. But launching it from Python through PyTorch takes 0.1—0.5ms of CPU overhead. Over 300 decode steps, that's 30—150ms of pure CPU waste. CUDA Graphs eliminate this entirely.

The decode loop problem

Without CUDA Graphs:
  Step 1: [CPU dispatch 0.3ms] [GPU compute 0.8ms] [CPU dispatch 0.3ms] [GPU compute 0.8ms] ...
  Total for 300 steps: 300 × (0.3 + 0.8) = 330ms

With CUDA Graphs:
  Step 1: [GPU compute 0.8ms] [GPU compute 0.8ms] [GPU compute 0.8ms] ...
  Total for 300 steps: 300 × 0.8 = 240ms  (27% faster)

The easy way (torch.compile)

# One line — torch.compile handles CUDA Graph capture for you
model.decode_step = torch.compile(model.decode_step, mode="reduce-overhead")

This is the recommended path. It handles graph capture, shape specialisation, and kernel fusion automatically.

The manual way (explicit CUDA Graphs)

Use this when torch.compile doesn't work (custom ops, dynamic control flow):

import torch

# Warmup (required — CUDA Graphs capture exact memory addresses)
for _ in range(3):
    output = model.decode_step(input_ids, kv_cache)

# Capture
static_input = torch.zeros(1, 1, dtype=torch.long, device="cuda")
static_kv = kv_cache.clone()

g = torch.cuda.CUDAGraph()
with torch.cuda.graph(g):
    static_output = model.decode_step(static_input, static_kv)

# Replay (fast path — no CPU involvement)
for step in range(max_steps):
    static_input.copy_(current_token)
    static_kv.copy_(current_kv)
    g.replay()
    next_token = static_output.clone()

Why this is "Medium" not "Easy"

  • Static shapes only. The captured graph replays the exact same operations on the exact same memory. Any dynamic shape breaks it.
  • For AR decode, each step has shape (batch, 1) which is fine. But you need a separate graph per batch size or pad to a fixed batch size.
  • KV cache updates must happen inside the graph. If your model updates the cache in Python, that won't be captured.
  • Debugging graph capture failures is non-trivial — errors are often cryptic.
  • CUDA Graphs + TensorRT = maximum performance for fixed-shape decode steps.
§06 Compiler · NVIDIA
1.5—2×

TensorRT

Speedup 1.5—2× over PyTorch · Effort 1—3 days · Risk Medium-high

What it is

NVIDIA's TensorRT (TRT) compiles a neural network into a GPU-optimised execution plan — fusing operations, selecting the fastest kernel per layer, and optimising memory layout for the specific GPU.

Why it matters for AR speech

TRT's layer fusion eliminates kernel launch overhead between small ops (LayerNorm + Linear + GELU becomes one kernel). For a 30-layer transformer doing hundreds of decode steps, this overhead adds up fast.

Our real-world results (XTTS v2 on L4 GPU)

ConfigurationRTF (real-time factor)TTFBNotes
PyTorch FP32~1.0~500msBaseline
TRT FP16 (FP32 I/O)0.48271msBest config, concurrency=2
TRT FP16 (FP16 I/O)0.48~270msNo improvement — TRT already computes FP16 internally

Two-engine pattern for AR models

AR speech models have two distinct phases with very different compute profiles:

┌─────────────┐     ┌──────────────┐
│   Prefill   │────▶│    Decode    │
│  (no KV$)   │     │  (with KV$)  │
│  batch=1    │     │  seq_len=1   │
│  seq=prompt │     │  per step    │
└─────────────┘     └──────────────┘

Build separate TRT engines for prefill and decode. The decode engine can be optimised for seq_len=1 with static KV cache shapes, giving much better kernel selection.

Implementation

# Simplified TRT engine build
import tensorrt as trt

builder = trt.Builder(logger)
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
parser = trt.OnnxParser(network, logger)

parser.parse_from_file("gpt_decode.onnx")

config = builder.create_builder_config()
config.set_flag(trt.BuilderFlag.FP16)
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 4 << 30)  # 4GB

# Set dynamic shape profiles
profile = builder.create_optimization_profile()
profile.set_shape("input_ids", min=(1, 1), opt=(1, 1), max=(1, 1))
profile.set_shape("past_key_values", min=(1, 16, 0, 64), opt=(1, 16, 256, 64), max=(1, 16, 608, 64))
config.add_optimization_profile(profile)

engine = builder.build_serialized_network(network, config)

Why this is "Medium" effort

The hard part isn't TensorRT itself — it's everything before it:

  • ONNX export is the real bottleneck. Models with custom attention masks, dynamic KV caches, or conditional logic need careful torch.onnx.export with dynamic_axes correctly specified. Expect 1—2 days debugging export issues alone.
  • Dynamic shapes require careful profiling. Set min/opt/max shapes on every dynamic axis. Get these wrong and TRT silently picks bad kernels.
  • Build time is long. A 30-layer model takes 10—20 minutes to compile per engine. Cache your engines.
  • TensorRT-LLM is the better path for INT8/INT4 AR models — it has native KV cache management and in-flight batching built in.
§07 Quantisation · aggressive
1.5—2×

INT8 / INT4 quantisation

Speedup 1.5—2× on top of FP16 · Effort 1—3 days · Risk Medium-high

What it is

Going beyond FP16 to 8-bit or 4-bit integer representations. This is a separate section from FP16 because the effort and risk profiles are fundamentally different.

Precision levels

PrecisionBitsMemorySpeedupQuality impact
INT8 (W8A8)84× vs FP322—3×Minor
INT4 (GPTQ/AWQ)48× vs FP323—4×Moderate

Approaches

Post-training quantisation (PTQ) — quickest path:

# Example: bitsandbytes 4-bit loading
from transformers import AutoModelForCausalLM
import torch

model = AutoModelForCausalLM.from_pretrained(
    "xtts-v2-gpt",
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.float16,
    bnb_4bit_quant_type="nf4",
)

Quantisation-aware training (QAT) — recovers 0.5—1.0 MOS points over PTQ at INT8, but requires a training run (pushes this towards "High effort").

Why this is "Medium" not "Easy"

  • INT8 is tricky. TensorRT 10.x deprecated implicit INT8 calibration, and models with complex dynamic shapes (like KV caches with 60+ inputs) often fail silently.
  • ONNX Runtime INT8 can fall back to CPU without warning — we saw a 6.9× slowdown instead of a speedup.
  • Speech models are more sensitive than text LLMs. The codebook/mel predictions degrade faster with aggressive quantisation. Always run MOS (Mean Opinion Score) evaluations, not just perplexity.
  • Tooling fragmentation. Tools: nvidia-modelopt (requires CUDA 13+), auto-gptq, bitsandbytes, llm-compressor — each has different compatibility requirements.
  • Calibration data matters. Bad calibration set = bad quantisation. You need representative audio prompts across languages and speakers.
§08 Serving · throughput
2—3×

vLLM

Speedup 2—3× throughput at scale · Effort 2—5 days · Risk Medium

What it is

vLLM is a high-throughput inference engine originally built for text LLMs. Its key innovation is PagedAttention — managing KV cache memory like virtual memory pages, eliminating fragmentation and enabling much higher batch sizes.

Why it matters for AR speech

AR speech models have long KV caches (500—1000+ tokens for a 10-second utterance). With naive KV cache allocation, you pre-allocate the maximum possible length per request, wasting 40—60% of GPU memory. PagedAttention allocates KV cache blocks on-demand, fitting 2—3× more concurrent requests.

Key features for speech

  • Continuous batching: New requests join the running batch without waiting. Critical for real-time TTS serving where requests arrive continuously.
  • PagedAttention: 2—3× memory efficiency on KV cache.
  • Prefix caching: If multiple requests share the same speaker conditioning prefix, the KV cache for that prefix is computed once and shared.
  • Speculative decoding: Built-in support for draft model acceleration.

Architecture fit

vLLM works best when your AR speech model follows the standard LLM pattern:
  ✓  Transformer decoder with KV cache
  ✓  Token-in, token-out autoregressive generation
  ✓  Standard attention patterns

It doesn't fit well when:
  ✗  Custom cross-attention to conditioning (e.g., speaker encoder output)
  ✗  Non-standard token representations (multi-codebook like SoundStorm)
  ✗  Tightly coupled vocoder (need to stream tokens to vocoder separately)

Making it work for speech

Most AR speech models need adaptation to fit vLLM's model interface:

  1. Wrap the AR speech model as a vLLM-compatible nn.Module
  2. Register the KV cache layers with vLLM's cache manager
  3. Handle conditioning tokens as a prefix in the sequence
  4. Stream output tokens to the vocoder externally
from vllm import LLM, SamplingParams

# If your model is compatible
llm = LLM(model="your-ar-tts-model", dtype="float16", gpu_memory_utilization=0.9)
params = SamplingParams(temperature=0.7, top_p=0.9, max_tokens=600)
outputs = llm.generate(prompts, params)

Why this is "Medium" effort

  • The model adaptation layer (fitting a speech model into vLLM's interface) is the bulk of the work.
  • Single-request latency may not improve — vLLM shines at throughput under concurrency.
  • If your speech model has non-standard architecture (cross-attention, multi-codebook), the adaptation can balloon to "High effort".
Tier 03
High effort

High effort

Time
Days—weeks
Training
Often required
Risk
High
These optimisations deliver the biggest potential gains but require training infrastructure, GPU programming expertise, or architectural changes to the model.
§09 Architecture swap
5—10%

LayerNorm → RMSNorm

Speedup 5—10% decode · Effort 2—5 days (includes fine-tuning) · Risk Medium

What it is

Standard LayerNorm computes mean and variance, then normalises:

LayerNorm(x) = γ · (x − mean) / sqrt(variance + ε) + β

RMSNorm (Root Mean Square Normalisation) drops the mean-centering:

RMSNorm(x) = γ · x / sqrt(mean(x²) + ε)

Why it matters for AR speech

  • One fewer reduction operation per layer per token. In a 30-layer model doing 300 decode steps, that's 18,000 fewer reduction ops.
  • 5—10% end-to-end speedup in our benchmarks on AR decode. Small per-op saving, but it compounds.
  • Reduction operations (computing mean) are expensive on GPUs because they require cross-thread synchronisation within a warp.

How to swap

class RMSNorm(nn.Module):
    def __init__(self, dim, eps=1e-6):
        super().__init__()
        self.weight = nn.Parameter(torch.ones(dim))
        self.eps = eps

    def forward(self, x):
        norm = torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
        return x * norm * self.weight


# Swap in existing model
for name, module in model.named_modules():
    if isinstance(module, nn.LayerNorm):
        rms = RMSNorm(module.normalized_shape[0], module.eps)
        rms.weight.data = module.weight.data  # transfer gamma
        # Note: bias (beta) is dropped — fine-tune briefly to recover
        parent = dict(model.named_modules())[name.rsplit('.', 1)[0]]
        setattr(parent, name.split('.')[-1], rms)

Why this is "High" effort

The swap itself takes 30 minutes. But:

  • Dropping the bias term (β) means this is not a drop-in replacement. You need 1,000—5,000 fine-tuning steps to recover quality — that means training infrastructure, data pipeline, and quality evaluation.
  • Some models (GPT-2 style) rely on the mean-centering for training stability. Test on a validation set before shipping.
  • Modern architectures (LLaMA, Mistral) already use RMSNorm. This optimisation applies mainly to older GPT-2/BERT-style speech models like XTTS.
  • The 5—10% speedup is modest compared to the effort. Prioritise this only after exhausting medium-effort options.
§10 Training · teacher—student
3—8×

Knowledge distillation

Speedup 3—8× · Effort 1—3 weeks · Risk High

What it is

Train a smaller "student" model to mimic a larger "teacher" model's output distribution, rather than training on ground-truth data directly. The student learns from the teacher's soft probability distributions, which contain richer information than hard labels.

Why it matters for AR speech

AR speech models are often over-parameterised. A 30-layer, 1024-dim GPT can be distilled into a 12-layer, 512-dim model with 80—90% of the quality at 5—8× the speed.

Distillation strategies for AR speech

1. Logit-level distillation. The student learns to match the teacher's token-level probability distribution.

# KL divergence between teacher and student logits
loss_kd = F.kl_div(
    F.log_softmax(student_logits / temperature, dim=-1),
    F.softmax(teacher_logits / temperature, dim=-1),
    reduction='batchmean'
) * (temperature ** 2)

# Combined loss
loss = alpha * loss_kd + (1 - alpha) * loss_ce

2. Hidden-state distillation. Map student hidden states to teacher hidden states at corresponding layers. This transfers internal representations, not just outputs.

# Project student hidden dim to teacher hidden dim
proj = nn.Linear(student_dim, teacher_dim)
loss_hidden = F.mse_loss(proj(student_hidden), teacher_hidden)

3. Speculative decoding (inference-time distillation). Use a small draft model to generate candidate tokens in parallel, then verify with the large model in a single forward pass. This gives you the quality of the large model at closer to the speed of the small model.

Draft model (fast):  generates 4 candidate tokens
Teacher model:       verifies all 4 in one forward pass
Accept/reject:       keep accepted tokens, resample from rejection point

Speculative decoding is especially promising for AR TTS because speech token distributions tend to be peaky (low entropy), meaning the draft model's acceptance rate is high.

Why this is "High" effort

  • Requires full training infrastructure — data pipeline, GPU compute, hyperparameter tuning.
  • Student architecture design is non-trivial — how many layers? What dimension? Which layers to align?
  • Quality evaluation is expensive — you need MOS tests, not just loss curves.
  • Training time: 1—2 weeks of GPU compute for a full distillation run.
  • The student might fail on edge cases (rare languages, unusual speaker characteristics) that the teacher handles.

Practical tips

  • Start with 50% layer reduction and 75% dimension — this is the sweet spot for quality/speed.
  • Use the teacher's generated audio (not ground truth) as training data — this reduces train/inference mismatch.
  • Fine-tune the student on real data after distillation for 10—20% of the original training steps.
§11 Kernels · GPU
10—20%

Custom Triton kernels

Speedup 10—20% on specific bottlenecks · Effort 3—7 days per kernel · Risk Medium-high

What it is

Triton is a Python-based GPU programming language from OpenAI that lets you write custom CUDA kernels without touching C++/CUDA directly. You write Python-like code that compiles to PTX (GPU assembly).

Why it matters for AR speech

Standard PyTorch ops leave performance on the table in two ways:

  1. Unfused operations: A sequence like layernorm → linear → gelu → linear launches 4 separate kernels, each reading/writing to HBM.
  2. Suboptimal kernels: Generic kernels can't exploit model-specific properties (fixed head dim, known sparsity patterns, etc.).

Triton lets you write fused, model-specific kernels that keep data in SRAM.

Key kernels worth writing for AR speech

1. Fused RMSNorm + Linear

@triton.jit
def fused_rmsnorm_linear_kernel(
    X, W, Out,
    stride_xm, stride_xn, stride_wn, stride_wk,
    M, N, K,
    eps: tl.constexpr, BLOCK_N: tl.constexpr, BLOCK_K: tl.constexpr,
):
    row = tl.program_id(0)

    # RMSNorm
    x_ptrs = X + row * stride_xm + tl.arange(0, BLOCK_N)
    x = tl.load(x_ptrs, mask=tl.arange(0, BLOCK_N) < N)
    rms = tl.sqrt(tl.sum(x * x) / N + eps)
    x_norm = x / rms

    # Linear (simplified — real impl tiles over K)
    for k in range(0, K, BLOCK_K):
        w = tl.load(W + tl.arange(0, BLOCK_N)[:, None] * stride_wn
                     + (k + tl.arange(0, BLOCK_K))[None, :])
        acc = tl.sum(x_norm[:, None] * w, axis=0)
        tl.store(Out + row * K + k + tl.arange(0, BLOCK_K), acc)

2. Fused softmax + top-K sampling. AR decoding always ends with softmax → sampling. Fusing this saves one full read/write of the vocabulary logits.

@triton.jit
def fused_softmax_topk_kernel(logits, output_token, temperature, top_k, V: tl.constexpr):
    # Load logits, apply temperature, compute softmax, sample — all in SRAM
    offs = tl.arange(0, V)
    x = tl.load(logits + offs) / temperature
    x = x - tl.max(x)  # numerical stability
    exp_x = tl.exp(x)
    probs = exp_x / tl.sum(exp_x)
    # ... top-k filtering and sampling

3. KV cache update kernel. The KV cache append operation (insert new K, V at position t) is a memory-bound operation that benefits from a custom kernel.

@triton.jit
def kv_cache_append(
    cache, new_kv, position,
    num_heads, head_dim,
    BLOCK: tl.constexpr,
):
    head = tl.program_id(0)
    offs = tl.arange(0, BLOCK)
    mask = offs < head_dim
    src = tl.load(new_kv + head * head_dim + offs, mask=mask)
    tl.store(cache + head * MAX_SEQ * head_dim + position * head_dim + offs, src, mask=mask)

Triton vs hand-written CUDA

AspectTritonCUDA C++
Development speed5—10× fasterSlow
Performance85—95% of CUDA100%
MaintainabilityPython, readableComplex
Auto-tuningBuilt-in triton.autotuneManual
DebuggingEasierPainful

Why this is "High" effort

  • Each kernel takes 3—7 days to write, test, and tune.
  • Requires GPU programming knowledge (memory hierarchy, tiling, occupancy).
  • Gains are incremental (10—20% per kernel) — you need to profile first to identify the actual bottleneck.
  • Triton kernels can't be used inside TensorRT engines — choose one path or the other.
  • For AR speech inference, Triton gets you 90% of the way with 10% of the CUDA effort. But it's still significant effort.
§12 Synthesis

Putting it all together

Effort vs impact

1.5× EASY MEDIUM HIGH SPEEDUP → IMPLEMENTATION EFFORT → FP16/BF16 SDPA / Flash Attn DeepSpeed TensorRT INT8/INT4 CUDA Graphs vLLM Distillation RMSNorm Triton kernels

Compatibility matrix

TensorRTDeepSpeedvLLMtorch.compileCUDA Graphs
Flash Attention✓*
SDPA✓*
Quantisation (INT8)
Quantisation (INT4)
RMSNorm
CUDA Graphs✓ auto
Triton kernels

* TRT has its own attention fusion — Flash/SDPA is baked in during engine build.

Recommended stack by use case

01 · Today
Just getting started
PyTorch + FP16 + SDPA
1 hour effort · 2—3× speedup
02 · Research
Prototyping
PyTorch + SDPA + FP16 + torch.compile
2—4 hours · 2.5—4× speedup
03 · Production
Single-GPU serving
TensorRT FP16 (two-engine: prefill + decode) + CUDA Graphs
3—5 days · 3—4× speedup
04 · Scale
Multi-GPU / high throughput
vLLM + FP16 + continuous batching + speculative decoding
1—2 weeks · 5—10× throughput
05 · Maximum
Quality / speed tradeoff pushed to the edge
Distilled smaller model + TensorRT INT8 + CUDA Graphs + custom Triton sampling
3—6 weeks · 8—15× speedup

Benchmarking tips

Whatever you optimise, measure correctly:

  • RTF (real-time factor) = generation time / audio duration. Below 1.0 means faster than real-time.
  • TTFB (time to first byte) = time until the first audio chunk is playable. For streaming TTS, this matters more than total RTF.
  • Measure at realistic concurrency. A model that's 3× real-time at concurrency=1 might be 0.8× at concurrency=4.
  • Always measure quality alongside speed. Use NISQA, PESQ, or UTMOS for automated quality checks. A 2× speedup means nothing if MOS drops from 4.2 to 3.5.
  • Warm up before measuring. First inference is always slow (JIT compilation, memory allocation, TRT engine loading). Run 10 warmup iterations, then measure 50+.

The fastest token is the one you don't generate. Before optimising inference, consider whether you can reduce sequence length — lower audio token rates, fewer codebook levels, or a non-autoregressive first pass with autoregressive refinement.

UR
Udeshya Raj
Author