GigaAM-v3 e2e CTC, Core ML
A Core ML conversion of Sber's GigaAM-v3 v3_e2e_ctc Russian speech recognition model. It outputs
punctuated, normalized Russian text. It was converted for echo99, a macOS call recorder that
transcribes on device. The weights are the upstream ones, stored in fp16 (the fp16 variant) or as
per-channel int8 (the int8 variant); the Results section names the variant of this upload. Nothing
was retrained or fine-tuned.
Licence and attribution
MIT, like the original. The model was made by Sber / SaluteDevices (the GigaChat team):
- Model card: ai-sage/GigaAM-v3
- Source and checkpoint loader: salute-developers/GigaAM @
7447938d791c4f3e643386ee22c33777004293a5
MIT License
Copyright (c) 2024 GigaChat Team
The full licence text is in the upstream repository's LICENSE file at that commit.
Evaluation audio: FLEURS ru_ru test split
(CC-BY 4.0), at dataset revision 70bb2e84b976b7e960aa89f1c648e09c59f894dd.
Files
| File | Contents |
|---|---|
GigaAM.mlmodelc/ |
The compiled model (macOS 14+). This is what the app loads. |
GigaAM.mlpackage/ |
The same model, uncompiled. |
gigaam-melfb.f32 |
Mel filterbank: 64 × 161 float32 little-endian, row-major [mel][bin], the table stored in the checkpoint. |
gigaam-window.f32 |
Analysis window: 320 float32 little-endian, the table stored in the checkpoint. |
gigaam-vocab.txt |
256 SentencePiece pieces, UTF-8, one per line; line number = token id. |
requirements-linux.lock |
The exact Python packages of the Linux conversion run (not used by the app). |
variant.json |
int8 variant only: the quantization settings and the sha256 of the fp16 package it was made from. |
compiled.json |
The sha256 of every file of GigaAM.mlmodelc and of the package it was compiled from (not used by the app). |
Input and output
Audio is 16 kHz mono float. One prediction covers at most 25.0 s (400,000 samples).
Features are computed outside the model, with the upstream preprocessor's parameters (steps 2–4):
- Input shorter than 1,600 samples (0.1 s) is zero-padded at the end to 1,600 samples.
- Frames:
⌊(n − 320) / 160⌋ + 1frames of 320 samples, hop 160, no centering. - Each frame is multiplied by
gigaam-window.f32, then a 320-point real DFT gives a power spectrum (re² + im²) of 161 bins. - Mel: multiply by
gigaam-melfb.f32(64 × 161), thenlog(clamp(x, 1e-9, 1e9)).
Load the window and the filterbank from the files. They are the checkpoint's own tables, which need not equal a textbook Hann window or a freshly built mel filterbank.
Model inputs:
features:[1, 64, 2499]float32, the log-mel frames[mel][frame], zero-padded after the real frames.feature_lengths:[1]int32, the real frame count.
Model outputs:
logits:[1, 625, 257]float32, CTC log-probabilities (log-softmax). Class 256 is the blank; class 0 is<unk>.logits_length:[1]int32, the number of valid output frames. One output frame is 40 ms.
Frames past logits_length are padding and must be ignored. The encoder masks the padding in
attention, so a padded clip matches the same clip run at its own length (see Results).
Decoding (greedy CTC): take the argmax of each valid frame, collapse repeats, drop the blank (256)
and <unk> (0), join the pieces, read ▁ as a space and trim. A token spans its run of frames:
start = firstFrame × 0.04 s, end = (lastFrame + 1) × 0.04 s.
Conversion
Upstream builds the encoder's attention mask only for batches larger than one
(gigaam/encoder.py, if audio_signal.shape[0] > 1:). With one clip padded to 25 s, real frames
would then attend to the padding. The export wrapper builds the mask for every batch size; nothing
else changes, and unpadded input gives the same result as upstream.
The conversion runs on two machines. PyTorch 2.7 publishes no Intel macOS wheels, and Core ML can only compile and predict on macOS. From the echo99 repository root:
Linux (x86_64, CPU only, Python 3.12): the PyTorch half. It checks the preprocessor's
parameters, writes the side files, records unpadded upstream reference outputs on FLEURS, checks the
patched model against them, exports with torch.export, and converts with coremltools 9 (fp16,
minimum_deployment_target=macOS14). The ops that compute the output length and the masks from
feature_lengths stay fp32: fp16 is exact only up to 2,048, and upstream computes the length in
floating point, so in fp16 2,499 frames would report 626 output frames instead of 625.
The CTC head (the projection onto the 257 classes, its transpose, the softmax and the log) also
stays fp32: in fp16 a log-softmax can give −inf. Both subgraphs are selected by what they depend on,
not by op name, and a static check fails the conversion if any of their values is fp16.
Boolean operators on the masks are converted as Core ML logical ops, and the export fails, listing
every node, if any op in the exported graph has no coremltools converter.
Once tools/gigaam/requirements-linux.lock is committed, install from it, so the run uses exactly the
packages of the verified run:
python3.12 -m venv tools/gigaam/.venv
uv pip install --python tools/gigaam/.venv "setuptools>=77" "packaging>=24.2" wheel
uv pip install --python tools/gigaam/.venv --no-build-isolation-package gigaam -r tools/gigaam/requirements-linux.lock
tools/gigaam/.venv/bin/python -I tools/gigaam/export_linux.py # ends with ALL PYTORCH CHECKS PASSED
The first install, before a lock exists, uses the version ranges instead: the same commands with
-r tools/gigaam/requirements-linux.txt.
GigaAM must be built without build isolation, in a venv that already has setuptools>=77 and
packaging>=24.2. Its pyproject.toml declares license = "MIT" as a licence expression, and an
isolated build fails with "ImportError: Cannot import packaging.licenses".
A passing run writes out/requirements-linux.lock: every installed package at its exact version, and
GigaAM at the pinned commit. Copy it to tools/gigaam/requirements-linux.lock and commit it; the run
prints whether the committed lock still matches the environment. The run also hashes the checkpoint
and the tokenizer that gigaam.load_model downloads. While EXPECTED_CHECKPOINT_SHA256 and
EXPECTED_TOKENIZER_SHA256 in common.py are empty, it prints both hashes for pinning; once they
are set, any other file fails the run before it is loaded.
Then copy the results to the Mac, from the Mac's repository root:
rsync -a <linux-host>:<path>/recc/tools/gigaam/{out,reference} tools/gigaam/
Mac (Python 3.12, no torch): the Core ML half. It checks that the package is the one the Linux
run recorded (sha256 of model.mlmodel and weight.bin), compiles GigaAM.mlmodelc, compares Core
ML with the PyTorch references, and writes tools/gigaam/results/<arch>-<variant>.{json,md}.
In this order, on the Intel Mac (it has Xcode, which provides coremlcompiler):
python3.12 -m venv tools/gigaam/.venv
tools/gigaam/.venv/bin/pip install -r tools/gigaam/requirements-mac.txt
tools/gigaam/.venv/bin/python -I tools/gigaam/finish_mac.py --write-fixtures # ends with ALL CORE ML CHECKS PASSED
tools/gigaam/.venv/bin/python -I tools/gigaam/quantize_mac.py # out-int8/: per-channel int8 weights
tools/gigaam/.venv/bin/python -I tools/gigaam/finish_mac.py --variant int8
tools/gigaam/.venv/bin/python -I tools/gigaam/pack_artifacts.py # dist/ for the Apple-silicon run
finish_mac.py runs every check on the compiled GigaAM.mlmodelc, the bundle the app downloads.
- One compile per package.
coremlcompilerdoes not produce byte-identical bundles (itscoremldata.bindiffers between runs), so the first run records the bundle incompiled.jsonand later runs reuse it while it matches, on this Mac and on the Apple-silicon Mac (which runs with--shipped-bundle: never compile). So the bundle checked on both Macs is the bundle uploaded.--recompilereplaces it; then repeat every later step, including the Apple-silicon run. - Bars, per compute-unit setting: at most max(1, ⌊2 % of the valid frames⌋) argmax mismatches
against unpadded PyTorch on each duration clip and on the test fixture; the WER within 0.3 points;
max abs logsumexp ≤ 1e-2; every log-probability finite; the fixture predicted twice gives identical
ids.
logits_lengthis swept at 13 lengths. - Compute units: on Intel the bars run on CPU_ONLY (what the app uses: CPU_AND_GPU returned NaN
from the second prediction on); on Apple silicon on ALL, CPU_AND_NE and CPU_AND_GPU. The sweep also
runs CPU_ONLY.
--compute-unitsreplaces the bars list. Each setting runs in a child process, so a Core ML crash fails that setting and the run goes on. - Results files (committed) also hold the load times, the prediction latencies and the compute plan (ops per device).
--write-fixtures(Intel only; refused on Apple silicon) also writesecho99Tests/Fixtures/GigaAM/and the results below.
quantize_mac.py applies coremltools.optimize.coreml.linear_quantize_weights (linear symmetric,
int8, per output channel) to out/GigaAM.mlpackage. It quantizes every model parameter of rank ≥ 2
with more than 2,048 elements: the conv and linear weights, the CTC head's conv weight included.
coremltools never compresses rank-1 constants, so biases and norms keep their precision. Constants
computed during the conversion, such as the rotary cos/sin tables [625, 1, 1, 48], are not
parameters (their names lack torch.export's p_) and stay as they are; variant.json lists them. Only stored weights change: the length path and the
head still compute in fp32. The script checks all of this on the quantized program.
For a run on another Mac, pack_artifacts.py writes dist/gigaam-artifacts-<commit>.tar (out/,
out-int8/, reference/, with the compiled bundles) and dist/SHA256SUMS; tar -xf in
tools/gigaam/ restores the same paths. It refuses a variant unless the Intel run passed on that
exact package (results/x86_64-<variant>.json) and the bundle matches compiled.json.
APPLE_SILICON_TEST.md is the step-by-step run on Apple silicon.
Both halves reuse cached downloads and finished stages. For a clean run, delete
tools/gigaam/out, tools/gigaam/out-int8 and tools/gigaam/reference.
Upload. Upload either out/ (fp16) or out-int8/ (int8), whichever variant the results
favour, without dotfiles and without the uncompiled package. For int8, first run
finish_mac.py --variant int8 --write-fixtures on the Intel Mac, so the fixtures and this card
describe that variant (it reuses the bundle the Apple-silicon run checked). With hf from huggingface_hub 1.x (each --exclude adds a pattern; 0.x
keeps only the last one):
hf upload echo99app/gigaam-v3-e2e-ctc-coreml tools/gigaam/out . --exclude ".*" --exclude "*/.*" --exclude "*.mlpackage/*"
Then manifest.py --commit <sha> writes the app's pinned file list
(echo99/Models/GigaAMManifest.swift); add --models-dir tools/gigaam/out-int8 for the int8 variant.
Provenance
Recorded by export_linux.py on Linux (Python 3.12.15, torch 2.7.0+cpu, coremltools 9.0). gigaam.load_model("v3_e2e_ctc") at GigaAM 7447938d791c4f3e643386ee22c33777004293a5 downloads these files:
| File | Source | Bytes | sha256 |
|---|---|---|---|
| checkpoint | https://cdn.chatwm.opensmodel.sberdevices.ru/GigaAM/v3_e2e_ctc.ckpt | 442404646 | 73e27ec8f904e486ab063d5bb7c6ab701838b7e5a967ee79b71ccc5aefde146f |
| tokenizer | https://cdn.chatwm.opensmodel.sberdevices.ru/GigaAM/v3_e2e_ctc_tokenizer.model | 240941 | 0b9a1960898fbfdf5424ab852ea17445eb3da960fba23e977ff100eb0054fbc8 |
The Linux environment is requirements-linux.lock (sha256 b210f7574d7dd6808d9c108c828a4fc97b0c697c205fb33ddcbba5701e70a372), written by the same run (uv pip freeze).
Results
Measured 2026-10-13 on Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz (x86_64, macOS 15.8.1) with the compiled GigaAM.mlmodelc of the fp16 variant, coremltools 9.0, compute units CPU_ONLY, against unmodified upstream PyTorch (2.7.0+cpu, fp32) run on each clip at its own length. The patched column is the export wrapper in PyTorch, padded to 25 s.
| Clip | Length | Valid frames | Patched PyTorch, padded | Core ML fp16, padded | Core ML mismatches (allowed) | Core ML max abs logsumexp |
|---|---|---|---|---|---|---|
| fleurs-1s | 1.00 s | 25 | 100.00 % | 100.00 % | 0 (1) | 2.6e-07 |
| fleurs-3s | 3.00 s | 75 | 100.00 % | 100.00 % | 0 (1) | 3.8e-07 |
| fleurs-7s | 7.00 s | 175 | 100.00 % | 100.00 % | 0 (3) | 2.0e-07 |
| fleurs-15s | 15.00 s | 375 | 100.00 % | 99.73 % | 1 (7) | 2.4e-07 |
| fleurs-24.9s | 24.90 s | 623 | 100.00 % | 100.00 % | 0 (12) | 3.8e-07 |
| synthetic-25s | 25.00 s | 625 | 100.00 % | 100.00 % | 0 (12) | 5.1e-07 |
Per-frame argmax agreement over the valid frames. Bars: patched ≥ 99.5 %; Core ML at most max(1, ⌊2 % of the valid frames⌋) mismatches at every length; abs logsumexp ≤ 1e-2; every Core ML log-probability finite; the same model predicting twice gives identical ids.
logits_length matched the expected frame count at 13 feature lengths (9, 10, 100, 399, 1000, 2047, 2048, 2049, 2050, 2051, 2402, 2498, 2499) on CPU_ONLY.
| WER, FLEURS ru_ru test, 100 utterances | |
|---|---|
| PyTorch, unpadded | 4.16 % |
| Core ML fp16 | 4.16 % |
WER is on lowercased text without punctuation, "ё" read as "е" (bar: within 0.3 points). 100/100 Core ML hypotheses are identical to PyTorch's.
Test fixture: FLEURS 18363036360322471934.wav (sentence 1908, CC-BY 4.0), 4.0 s from 0.50 s. Core ML vs unpadded PyTorch: 0 mismatches (2 allowed); text identical. Numpy extractor port vs the model's preprocessor: max abs diff 1.4e-04 where the reference is ≥ −15, 5.7e-06 below.
Per compute unit, timings and the compute plan: tools/gigaam/results/x86_64-fp16.md in the echo99 repository.
Model tree for echo99app/gigaam-v3-e2e-ctc-coreml
Base model
ai-sage/GigaAM-v3