Add int8 dynamic quantization support (#147)

# Add int8 dynamic quantization
 
Closes #7
 
## Summary
 
This PR implements mixed-precision int8 dynamic quantization for
pocket-tts, as requested in issue #7. It adds a `quantize=True` flag to
`TTSModel.load_model()` that reduces runtime memory usage by ~48% and
improves inference speed by ~27% on x86 and up to 23% on ARM, with no
measurable impact on audio quality.
This commit is contained in:
nabil-tazi
2026-03-24 11:29:14 +01:00
committed by GitHub
parent 0dcf6cb442
commit 670e777260
8 changed files with 1506 additions and 512 deletions
+8 -2
View File
@@ -188,11 +188,14 @@ def serve(
help="Path to locally-saved model config .yaml file or model variant signature"
),
] = DEFAULT_VARIANT,
quantize: Annotated[
bool, typer.Option(help="Apply int8 quantization to reduce memory usage")
] = False,
):
"""Start the FastAPI server."""
global tts_model, global_model_state
tts_model = TTSModel.load_model(config)
tts_model = TTSModel.load_model(config, quantize=quantize)
# Pre-load the voice prompt
global_model_state = tts_model.get_state_for_audio_prompt(voice)
@@ -236,6 +239,9 @@ def generate(
max_tokens: Annotated[
int, typer.Option(help="Maximum number of tokens per chunk.")
] = MAX_TOKEN_PER_CHUNK,
quantize: Annotated[
bool, typer.Option(help="Apply int8 quantization to reduce memory usage")
] = False,
):
"""Generate speech using Kyutai Pocket TTS."""
log_level = logging.ERROR if quiet else logging.INFO
@@ -248,7 +254,7 @@ def generate(
logger.error("No input received from stdin.")
raise typer.Exit(code=1)
tts_model = TTSModel.load_model(
config, temperature, lsd_decode_steps, noise_clamp, eos_threshold
config, temperature, lsd_decode_steps, noise_clamp, eos_threshold, quantize=quantize
)
tts_model.to(device)
+13 -2
View File
@@ -33,6 +33,7 @@ from pocket_tts.modules import mimi_transformer
from pocket_tts.modules.dummy_quantizer import DummyQuantizer
from pocket_tts.modules.seanet import SEANetDecoder, SEANetEncoder
from pocket_tts.modules.stateful_module import StatefulModule, increment_steps, init_states
from pocket_tts.quantization import RECOMMENDED_CONFIG, apply_dynamic_int8
from pocket_tts.utils.config import Config, load_config
from pocket_tts.utils.utils import (
PREDEFINED_VOICES,
@@ -192,6 +193,7 @@ class TTSModel(nn.Module):
lsd_decode_steps: int = DEFAULT_LSD_DECODE_STEPS,
noise_clamp: float | int | None = DEFAULT_NOISE_CLAMP,
eos_threshold: float = DEFAULT_EOS_THRESHOLD,
quantize: bool = False,
) -> Self:
"""Load a pre-trained TTS model with specified configuration.
@@ -210,6 +212,11 @@ class TTSModel(nn.Module):
is applied. Helps prevent extreme values in generation.
eos_threshold: Threshold for end-of-sequence detection. Higher values
make the model more likely to continue generating.
quantize: If True, apply dynamic int8 quantization to the transformer's
attention and FFN layers. Reduces runtime memory by ~48% and improves
inference speed by ~27% on x86 (FBGEMM).
No measurable impact on speech quality (WER unchanged).
For optimized performance, install torchao: ``pip install pocket-tts[quantize]``
Returns:
TTSModel: Fully initialized model with loaded weights on cpu, ready for
@@ -227,8 +234,8 @@ class TTSModel(nn.Module):
# Load with default settings
model = TTSModel.load_model()
# Load with custom parameters
model = TTSModel.load_model(variant="b6369a24", temp=0.5, lsd_decode_steps=5, eos_threshold=-3.0)
# Load with int8 quantization
model = TTSModel.load_model(quantize=True)
```
"""
if str(config).endswith(".yaml"):
@@ -241,6 +248,10 @@ class TTSModel(nn.Module):
tts_model = TTSModel._from_pydantic_config_with_weights(
config, temp, lsd_decode_steps, noise_clamp, eos_threshold
)
if quantize:
apply_dynamic_int8(tts_model.flow_lm, RECOMMENDED_CONFIG)
return tts_model
def _run_flow_lm_and_increment_step(
+128
View File
@@ -0,0 +1,128 @@
"""
Dynamic int8 quantization for pocket-tts.
Uses torchao if available (torch 2.10+ with C++ extensions), otherwise
falls back to torch.ao.quantization (deprecated but functional on torch 2.5-2.9).
Quantizes attention (Q/K/V/output projections) and FFN (linear1/linear2) layers
in the FlowLM transformer. The flow matching network and Mimi VAE decoder
remain in float32.
"""
import logging
import platform
import torch
import torch.nn as nn
logger = logging.getLogger(__name__)
# Used by load_model(quantize=True) to specify which layer groups to quantize.
RECOMMENDED_CONFIG = {"attention", "ffn"}
def _get_backend():
"""Detect the best available quantization backend.
Returns "torchao" if torchao is installed with working C++ extensions,
otherwise returns "torch.ao".
"""
try:
import importlib.util
if importlib.util.find_spec("torchao") is None:
return "torch.ao"
import torchao
if hasattr(torchao, "_C") or not getattr(torchao, "_SKIPPED_CPP_EXTENSIONS", False):
return "torchao"
except ImportError:
pass
return "torch.ao"
def _quantize_module_torchao(module: nn.Module):
"""Apply int8 dynamic quantization using torchao."""
from torchao.quantization import Int8DynamicActivationInt8WeightConfig, quantize_
quantize_(module, Int8DynamicActivationInt8WeightConfig())
def _ensure_quantization_engine():
"""Set the quantization engine for torch.ao (QNNPACK for ARM, FBGEMM for x86)."""
if platform.machine() in ("arm64", "aarch64"):
torch.backends.quantized.engine = "qnnpack"
elif torch.backends.quantized.engine == "none":
torch.backends.quantized.engine = "fbgemm"
def apply_dynamic_int8(flow_lm: nn.Module, quantize_groups: set[str]) -> nn.Module:
"""
Apply dynamic int8 quantization to the specified layer groups of a FlowLM model.
Automatically selects the best available backend:
- torchao (torch 2.10+): optimized C++ kernels, faster on both ARM and x86
- torch.ao (torch 2.5-2.9): deprecated but functional fallback
Args:
flow_lm: The FlowLM model (model.flow_lm)
quantize_groups: Set of group keys to quantize.
Valid keys: "attention", "ffn", "flow_net"
Returns:
The quantized model (modified in-place).
"""
if not quantize_groups:
logger.info("No quantization groups specified, returning model unchanged.")
return flow_lm
backend = _get_backend()
logger.info("Using quantization backend: %s", backend)
if backend == "torchao":
_apply_torchao(flow_lm, quantize_groups)
else:
_apply_torch_ao(flow_lm, quantize_groups)
return flow_lm
def _apply_torchao(flow_lm: nn.Module, quantize_groups: set[str]) -> None:
"""Apply quantization using torchao backend."""
if "flow_net" in quantize_groups:
_quantize_module_torchao(flow_lm.flow_net)
for layer in flow_lm.transformer.layers:
if "attention" in quantize_groups:
_quantize_module_torchao(layer.self_attn)
if "ffn" in quantize_groups:
wrapper1 = nn.Sequential(layer.linear1)
wrapper2 = nn.Sequential(layer.linear2)
_quantize_module_torchao(wrapper1)
_quantize_module_torchao(wrapper2)
layer.linear1 = wrapper1[0]
layer.linear2 = wrapper2[0]
def _apply_torch_ao(flow_lm: nn.Module, quantize_groups: set[str]) -> None:
"""Apply quantization using deprecated torch.ao backend (fallback)."""
from torch.ao.quantization import quantize_dynamic
_ensure_quantization_engine()
if "flow_net" in quantize_groups:
quantize_dynamic(flow_lm.flow_net, {nn.Linear}, dtype=torch.qint8, inplace=True)
for layer in flow_lm.transformer.layers:
if "attention" in quantize_groups:
quantize_dynamic(layer.self_attn, {nn.Linear}, dtype=torch.qint8, inplace=True)
if "ffn" in quantize_groups:
layer.linear1 = quantize_dynamic(
nn.Sequential(layer.linear1), {nn.Linear}, dtype=torch.qint8
)[0]
layer.linear2 = quantize_dynamic(
nn.Sequential(layer.linear2), {nn.Linear}, dtype=torch.qint8
)[0]