Add Apple Silicon (MPS) and CPU support - #6
Open
Talpik wants to merge 5 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>
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>
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.
Runs FireRedTTS3 on Apple Silicon (MPS) and on CPU, without changing anything on CUDA.
What was CUDA-only
Device, compute dtype, attention implementation and FFT capability are now resolved once in a new
fireredtts3/utils/device.py, so the model code stays free of per-backend branches:torch.device('cuda')(base + instruct)get_device()— CUDA → MPS → CPU@torch.autocast(device_type='cuda', dtype=bfloat16)(×3)@autocast@autocast('cuda', enabled=False)(×2, rotary)@disable_autocastattn_implementation='flash_attention_2'(×3)get_attn_implementation()torch.cuda.manual_seed*torch.mps.manual_seedflash_attnhas no macOS wheel at all, so off CUDA this falls back to PyTorch SDPA (both core models already declare_supports_sdpa = True).Backends without FFT / complex kernels — MPS on older torch — additionally need the RedAE ISTFT and the CAM++ kaldi fbank on CPU. That is detected by a runtime probe rather than a version check, so it stays dormant where MPS does support FFT (torch 2.8 on macOS 26 does) and switches on automatically where it does not.
Memory:
FIRERED_WEIGHT_DTYPEAutocast only casts activations, so it cannot shrink resident memory — relevant when 3.06B fp32 params do not fit. This adds a knob to load the checkpoints in half precision. Making it work required the dtype to follow the weights rather than being pinned to fp32 at the pipeline entry points (prompt latents, speaker embedding, flow-matching noise
x0,t_span, emptylatents_out); without that, MPS aborts withThe ISTFT deliberately stays in fp32, since complex half is not a usable dtype.
Measured on an 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(default)bfloat16On MPS
bfloat16weights are the setting to want: half the memory and ~20% faster, at effectively unchanged similarity. The default staysfloat32because it matches the checkpoints and leaves CUDA untouched.Autocast, by contrast, buys nothing on MPS (RTF 0.88–0.93 vs 0.85–0.91 over fp32 weights) — the DiT flow head, the real bottleneck, sits outside the autocast region. So autocast off is the default off CUDA, and CUDA keeps its bf16 autocast defaults unchanged.
Dependencies
requirements-mps.txtis the macOS set: noflash_attn, plussoundfile.torchcodec==0.7.0refuses to load against FFmpeg 8 (it supports 4–7), and without it torchaudio has no I/O backend at all on macOS — eventorchaudio.savefails.Testing
All five inference paths, 24 kHz output, Russian reference voices (12.6–17.9 s prompts, ~10 s targets):
speed 0.8x<|edit|>mask lands on the intended wordSpeaker similarity (bundled CAM++, matched pairs on the diagonal) — the fp32 numbers are unchanged before and after the dtype refactor:
Note that MPS is not bit-reproducible run to run: two runs of identical code produce different bytes, so equivalence is established via durations and similarity scores rather than checksums.
Not tested on CUDA — I have no CUDA machine. The CUDA path is intended to be byte-identical to before:
get_device()returnscudafirst,get_attn_implementation()returnsflash_attention_2, autocast defaults to bf16,FIRERED_WEIGHT_DTYPEdefaults to the checkpoint's fp32, and the FFT probe passes so no CPU fallback engages. A confirmation run on CUDA would be welcome.scripts/convert_to_bf16.pyis a user-facing tool the README points at, not test code: it halves the checkpoints on disk (19 GB → 9.7 GB), verifying every tensor against the fp32 original for exact bfloat16 rounding before replacing it, so the two routes to half precision — cast on load viaFIRERED_WEIGHT_DTYPE, or convert once and load with no variable set — are both documented and supported.scripts/mps_smoke_test.pyandscripts/spk_similarity.py, by contrast, are just the harness used above; happy to drop those two if you would rather keep the repo lean.🤖 Generated with Claude Code