Add optional int8 weight-only quantization - #7
Open
Talpik wants to merge 8 commits into
Open
Conversation
The text-frontend wrapper unpacked three values from the backend
generate_tts, which returns only (gen_audio, gen_audio_sr) — matching the
two-value form documented in the README. Every call to
fireredtts3.core.FireRedTTS3Instruct.generate_tts therefore raised
ValueError: not enough values to unpack (expected 3, got 2)
regardless of device.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Device, compute dtype, attention implementation and FFT capability are now
resolved once in fireredtts3/utils/device.py instead of being hard-coded to
CUDA, so the model code stays free of per-backend branches.
Replaced CUDA-only constructs:
- torch.device('cuda') -> get_device()
- @torch.autocast(device_type='cuda', ...) -> @autocast
- @autocast('cuda', enabled=False) -> @disable_autocast
- attn_implementation='flash_attention_2' -> get_attn_implementation()
(flash-attn has no macOS wheel; SDPA covers MPS/CPU)
- unguarded torch.cuda.manual_seed* -> guarded, + torch.mps
Backends without FFT / complex kernels (MPS on older torch) now compute the
RedAE ISTFT and the CAM++ kaldi fbank on CPU, detected by probe rather than by
version check.
Added FIRERED_WEIGHT_DTYPE so checkpoints can be loaded in half precision,
which halves resident memory (11.44 -> 5.73 GiB) at equal speaker similarity.
Autocast alone cannot do this, as it only casts activations. Making that work
required the dtype to follow the weights instead of being pinned to fp32 at
the pipeline entry points (prompt latents, speaker embedding, flow-matching
noise and time span); the ISTFT stays in fp32 since complex half is unusable.
requirements-mps.txt drops flash_attn and adds soundfile, because
torchcodec 0.7 does not load against FFmpeg 8 and torchaudio then has no I/O
backend at all.
Verified on an M4 Max (macOS 26, torch 2.8) across all five paths: Base and
Instruct zero-shot cloning, voice design, acoustic edit and semantic edit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The earlier table timed a single short sentence, where fixed per-call overhead dominates, and wrongly concluded that half weights are slower on MPS. Measured on a 3-sentence paragraph across two voices, 3 interleaved runs each, bfloat16 weights are consistently ~20% *faster* than float32 (RTF 0.73-0.76 vs 0.87-0.96) as well as half the size, at unchanged speaker similarity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Defaulting to float32 overrode what transformers does by itself: with no dtype argument it loads weights in the dtype the checkpoint stores. For the official fp32 weights the result is identical, but the hard default would silently upcast any half-precision checkpoint back to fp32 — exactly defeating the purpose of converting one. Unset now means "as stored". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Talpik
force-pushed
the
feat/int8-quantization
branch
from
August 26, 2026 12:59
f005634 to
b2a6a5b
Compare
Half-precision weights can be had two ways, and the README now spells out both: FIRERED_WEIGHT_DTYPE=bfloat16 casts the official fp32 checkpoints while loading and leaves the disk alone, or convert_to_bf16.py halves them on disk once (19 GB -> 9.7 GB) after which they load as bfloat16 with no variable set. The script verifies every tensor of the new file against the fp32 original for exact bfloat16 rounding before replacing it, skips components that are already converted, and takes --keep-fp32 for machines with room for both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Quantizes the transformer backbones via optimum-quanto, which is imported
lazily and stays an optional dependency: the default FIRERED_QUANT=none does
not touch the model and needs nothing installed.
Scope was chosen by measurement (M4 Max, bf16 weights, 2 voices):
backbone only 4.40 GiB RTF 1.12 sim 0.926
backbone + redae 3.90 GiB RTF 1.16 sim 0.934 <- applied
every nn.Linear 3.59 GiB RTF 2.41 sim 0.921
The DiT flow head is deliberately excluded. It runs once per flow timestep
with a CFG-doubled batch, so per-call dequantization dominates and including
it doubles latency for 8% more savings.
int4 is rejected with an explanatory error rather than silently accepted:
measured 3x slower than bf16 with a clear speaker-similarity drop, and
quanto's int4 path conflicts with the @torch.inference_mode() decorators.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Talpik
force-pushed
the
feat/int8-quantization
branch
from
August 26, 2026 13:36
b2a6a5b to
b728021
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Optional int8 weight-only quantization, for when memory is the binding constraint.
Usage
optimum-quantois imported lazily and stays an optional dependency: the defaultFIRERED_QUANT=nonedoes not touch the model and requires nothing installed. A missing package raises a message naming the install command rather than an ImportError traceback.Results
M4 Max (macOS 26, torch 2.8), Base zero-shot cloning of a 3-sentence paragraph over two reference voices, RTF as the range across 3 interleaved runs, speaker similarity via the bundled CAM++:
float32, no quant (default)bfloat16, no quantbfloat16+int8int8 is 2.9x smaller than the default at ~40% higher latency than plain bf16, with speaker similarity intact. It is a memory play, not a speed play — worth stating plainly, since where the RAM is available bf16 without quantization is both faster and smaller than the fp32 default.
Why this scope
Quantization is applied to the transformer backbones (
tts_core.backbone_llmandredae) and not to the DiT flow head. Chosen by measurement, not assumption:nn.LinearIncluding the DiT doubles latency for 8% more savings: it runs once per flow timestep with a CFG-doubled batch, so per-call dequantization dominates.
int4is rejected with an explanatory error rather than silently accepted — measured 3x slower than bf16 with a clear similarity drop (0.828 vs 0.913), and quanto's int4 path conflicts with the@torch.inference_mode()decorators (RuntimeError: Cannot set version_counter for inference tensor).Also here
One commit corrects the dtype benchmark added in #6. That table timed a single short sentence, where fixed per-call overhead dominates, and wrongly concluded half weights are slower on MPS. On a realistic multi-sentence workload, measured 3 times interleaved, bf16 is consistently ~20% faster than fp32 as well as half the size. #6's own description has been updated accordingly.
Testing
All five inference paths under
FIRERED_QUANT=int8: Base and Instruct cloning, voice design, acoustic edit, semantic edit. Worth noting that quantizing the backbone also quantizeslm_head, so the voice-design CoT was the thing to watch — it comes out identical to the unquantized run:Acoustic edit stays numerically correct under int8 (16.5 s input,
speed 0.8x→ 20.80 s = 16.5/0.8). Invalid values (FIRERED_QUANT=int7) and the default off-path were both checked.Not tested on CUDA — no CUDA machine here. The default is off, so the CUDA path is untouched unless the variable is set.
Carried from #6
Two commits here belong to #6's subject rather than quantization, and are listed for review clarity:
FIRERED_WEIGHT_DTYPEnow defaults to the checkpoint's own dtype instead of hard-coding float32, which was overriding what transformers does by itself and would silently upcast a half-precision checkpoint back to fp32.scripts/convert_to_bf16.pyhalves the checkpoints on disk (19 GB → 9.7 GB), verifying every tensor against the fp32 original for exact bfloat16 rounding before replacing it. It makes the two routes to half precision explicit: cast on load viaFIRERED_WEIGHT_DTYPE, or convert once and load with no variable set. int8 stacks on either.🤖 Generated with Claude Code