Skip to content

feat: add MLX inference for Apple Silicon (CTC, RNNT, E2E) - #62

Open
misteral wants to merge 6 commits into
salute-developers:mainfrom
misteral:feat/mlx-clean
Open

misteral wants to merge 6 commits into
salute-developers:mainfrom
misteral:feat/mlx-clean

Conversation

@misteral

@misteral misteral commented Mar 15, 2026 •

Copy link
Copy Markdown

Summary

Native MLX inference for GigaAM v3 on Apple Silicon: v3_ctc, v3_rnnt, v3_e2e_ctc, v3_e2e_rnnt.
Transcripts are identical to the PyTorch model on 200/200 evaluation clips for all four models.

Supersedes #61 (rebased on main, dev artifacts removed).

What's included

File Description
mlx_convert/gigaam_mlx.py MLX model: Conformer encoder (RoPE), CTC + RNNT heads, mel front-end, SentencePiece decoding, streaming
mlx_convert/convert_gigaam_to_mlx.py PyTorch → MLX conversion (safetensors + config.json [+ tokenizer.model])
mlx_convert/gigaam-cli, gigaam-stream, gigaam-transcribe CLI: file, live mic / file streaming, shell wrapper
mlx_convert/eval/ Reproducible parity / accuracy / speed evaluation
mlx_convert/README.md Python API, CLI, evaluation results

Evaluation

Apple M4 Pro (24 GB), mlx 0.32.2, torch 2.14.0; 200 Golos Crowd test clips (823.85 s) — the sample of
garrrikkotua/gigaam-vs-whisper-russian, verified by SHA-256.
Full tables: mlx_convert/eval/results/RESULTS.md.

Parity vs PyTorch (CPU, fp32): identical transcripts 200/200 for all four models; encoder output max |Δ| ≤ 9e-5
per clip; CTC frame labels 100% identical. v3_rnnt: 15 errors / 1001 words, the same hypotheses as the archived
PyTorch run of that benchmark.

WER (E2E output normalized; last row excludes clips where E2E output contains digits/Latin — formatting, not recognition):

v3_ctc v3_rnnt v3_e2e_ctc v3_e2e_rnnt
WER 1.70% 1.50% 6.49% 5.69%
WER, 168-clip subset 1.35% 1.23% 2.70% 2.09%

Speed, × realtime (short = 200 clips one by one, median 3.9 s / long = ≈20 s segments):

MLX PyTorch MPS PyTorch CPU
v3_ctc 201 / 285 201 / 268 10 / 41
v3_rnnt 91 / 102 37 / 41 10 / 39
v3_e2e_ctc 205 / 289 202 / 271 10 / 41
v3_e2e_rnnt 99 / 112 45 / 48 10 / 41

For CTC models MLX is on par with PyTorch MPS; for RNNT models MLX is 2.3–2.5× faster. The earlier figures in this
PR (139× / 48×) were not produced by a reproducible benchmark and are replaced by the above.

Changes since the first version of this PR

  • Fix: RoPE base is 5000. RotaryPositionalEmbedding(d_model // n_heads, pos_emb_max_len) passes
    pos_emb_max_len as base; the port used 10000. Transcripts still matched on most clips (195–199/200 for
    CTC/RNNT, WER unchanged), but encoder outputs differed by up to ~1.8 — found by the tensor-level parity check.
  • E2E models: SentencePiece decoding without the sentencepiece package, verified against
    SentencePieceProcessor.decode (eval/test_decode.py); converter takes the vocabulary from the model's tokenizer
    (v3_e2e_ctc previously got the 33-character vocabulary).
  • Mel filterbank / STFT window kept in fp32; load_model upcasts the fp16 weights once at load
    (activations are fp32 anyway; ~20% faster), dtype=None keeps them.
  • gigaam-stream caps MLX's buffer cache (1 GB): with a new input length every step it otherwise grows towards
    most of unified memory.

Architecture notes

  • RoPE is applied to the raw input before the Q/K projections, with base 5000 — both differ from the usual RoPE.
  • Mel filterbank and STFT window are taken from the PyTorch model, not recomputed.
  • Conv1d weights [out, in, K] → [out, K, in]; LSTM bias_ih + bias_hh → one bias.
  • Upstream checkpoints are stored in fp16, so fp16 conversion of the encoder is lossless.

Add native MLX inference for GigaAM v3 on Apple Silicon:

- Full Conformer encoder (16 layers, 768d) with RoPE attention
- CTC and RNNT heads with greedy decoding
- PyTorch → MLX weight conversion (safetensors + config.json)
- Streaming transcription (growing buffer, live mic + file)
- CLI tools: gigaam-cli, gigaam-stream, gigaam-transcribe
- Python API: load_model, transcribe, stream_generate, stream_live

Performance on Apple M4:
- CTC: 139x realtime (11s audio in 81ms), fp16 421 MB
- RNNT: 48x realtime (11s in 230ms), ~9% lower WER than CTC

New files in mlx_convert/:
  gigaam_mlx.py              — MLX model + inference + streaming
  convert_gigaam_to_mlx.py   — PyTorch → MLX conversion
  gigaam-cli                 — single-file transcription CLI
  gigaam-stream              — real-time streaming CLI
  gigaam-transcribe          — shell wrapper
  README.md                  — documentation, API, benchmarks
@tadatuta

tadatuta commented Sep 1, 2026

Copy link
Copy Markdown

any chances for this PR to be merged?

misteral and others added 4 commits September 1, 2026 23:13
- RoPE base: upstream builds RotaryPositionalEmbedding(d_model // n_heads,
  pos_emb_max_len), whose second positional parameter is `base`, so the base
  is 5000. The port used 10000: encoder outputs were off by up to ~1.8 while
  transcripts mostly still matched. Now max |Δ| ≤ 9e-5 vs PyTorch.
- E2E decoding: E2E CTC / RNNT output SentencePiece ids; the runtime joined
  raw pieces. decode_sentencepiece reproduces SentencePieceProcessor.decode
  (control ids, <unk>, <0xNN> byte fallback, dummy prefix) without the
  sentencepiece package.
- Converter takes the vocabulary from model.decoding.tokenizer: fixes E2E CTC
  (previously got the 33-char vocabulary) and writes tokenizer.model from the
  loaded model instead of guessing a cache path.
- Keep mel_filterbank / stft_window in fp32 in the fp16 file.
- load_model upcasts weights to fp32 by default (dtype=None keeps stored):
  activations are fp32 anyway and fp16 weights cost a cast per op, ~20%.
- gigaam-stream caps MLX's buffer cache at 1 GB: every step has a new input
  length and the cache otherwise grows towards most of unified memory.
eval/ runs upstream PyTorch and the MLX port on the 200-clip Golos Crowd
sample of garrrikkotua/gigaam-vs-whisper-russian (downloaded from the HF
mirror and verified by SHA-256; audio not redistributed) and reports:

- parity: identical transcripts 200/200 for v3_ctc, v3_rnnt, v3_e2e_ctc,
  v3_e2e_rnnt; encoder max |Δ| ≤ 9e-5; CTC frame labels 100% identical;
- WER/CER with a documented normalization (v3_rnnt: 15/1001, identical to
  the archived PyTorch result of that benchmark);
- speed vs PyTorch MPS / CPU, one subprocess per run, MLX cache capped.

README: replace the unmeasured benchmark table with these results.
@misteral misteral changed the title feat: add MLX inference for Apple Silicon (CTC + RNNT) feat: add MLX inference for Apple Silicon (CTC, RNNT, E2E) Sep 27, 2026
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.

2 participants