Skip to content

Add optional int8 weight-only quantization - #7

Open
Talpik wants to merge 8 commits into
FireRedTeam:mainfrom
Talpik:feat/int8-quantization
Open

Add optional int8 weight-only quantization#7
Talpik wants to merge 8 commits into
FireRedTeam:mainfrom
Talpik:feat/int8-quantization

Conversation

@Talpik

@Talpik Talpik commented Aug 26, 2026

Copy link
Copy Markdown

Optional int8 weight-only quantization, for when memory is the binding constraint.

Stacked PR — contains #5 and #6 and should be merged last. Only the final two commits are new here; everything rebases cleanly if the earlier ones land first.

Usage

pip install optimum-quanto
FIRERED_QUANT=int8 FIRERED_WEIGHT_DTYPE=bfloat16 python your_script.py

optimum-quanto is imported lazily and stays an optional dependency: the default FIRERED_QUANT=none does 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++:

Weights Resident weights RTF Speaker sim
float32, no quant (default) 11.42 GiB 0.87–0.96 0.947 / 0.935
bfloat16, no quant 5.71 GiB 0.73–0.76 0.941 / 0.933
bfloat16 + int8 3.90 GiB 1.03–1.05 0.937 / 0.934

int8 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_llm and redae) and not to the DiT flow head. Chosen by measurement, not assumption:

Scope Resident weights RTF Speaker sim
backbone only 4.40 GiB 1.12 0.926
backbone + redae 3.90 GiB 1.16 0.934
every nn.Linear 3.59 GiB 2.41 0.921

Including the DiT doubles latency for 8% more savings: it runs once per flow timestep with a CFG-doubled batch, so per-call dequantization dominates.

int4 is 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 quantizes lm_head, so the voice-design CoT was the thing to watch — it comes out identical to the unquantized run:

[gender] male [age] middle_age [pitch] low [texture] warm [volume] normal
[accent] american [emotion] calm [fluency] fluent [speed] slow [clarity] clear
[tone] narrative [personality] gentle

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_DTYPE now 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.py halves 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 via FIRERED_WEIGHT_DTYPE, or convert once and load with no variable set. int8 stacks on either.

🤖 Generated with Claude Code

vladimir.talpa and others added 4 commits August 26, 2026 13:37
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
Talpik force-pushed the feat/int8-quantization branch from f005634 to b2a6a5b Compare August 26, 2026 12:59
vladimir.talpa and others added 4 commits August 26, 2026 16:35
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
Talpik force-pushed the feat/int8-quantization branch from b2a6a5b to b728021 Compare August 26, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant