Downloads · 30 days
10
19% of all-time downloads
armanibadboy/whisper-small-ternary
whisper-small-ternary is a automatic speech recognition model from armanibadboy. Use it when you need speech turned into text. It is set up for transformers. The card lists the license as mit.
192 of this model's weight matrices contain only −1, 0 and +1. Normalised WER goes down against stock whisper-small. Punctuation and multilinguality go away.
Downloads · 30 days
10
19% of all-time downloads
All-time downloads
52
Public
Parameters
242M
1.1 GB on disk
Likes
0
Public
Click a slice to open those files.
.safetensors967 MB · 87%
From the Hugging Face model README
192 of this model's weight matrices contain only −1, 0 and +1. Normalised WER goes down against stock whisper-small. Punctuation and multilinguality go away.
| LibriSpeech (greedy, batch 16, max_new_tokens 440) | this model | stock whisper-small |
|---|---|---|
| normalised WER, test-clean | 2.7145 % | 3.3391 % |
| normalised WER, test-other | 7.3817 % | 7.5002 % |
| long-form, 9 recordings | 3.632 % | 3.891 % |
| utterances with a comma | 0.0–0.7 % | 63.3 % |
| utterances with a capital | 2 % | 98 % |
Two caveats belong here, not further down.
This is a comparison against stock whisper-small. A matched fp16 control — same code path, same data, same schedule, quantisation off — is still ≈0.37 pp better than the ternary branch, and both curves were still descending when the runs ended. The model beats the teacher it was distilled from; that is not the same claim as "ternarisation improved it".
The WER is normalised — case and punctuation are stripped before scoring, so a model can score better while producing worse text for a human. That is what happened here.
| information content of a ternary alphabet, log₂3 | 1.585 bit/weight |
| training layout: 2-bit code + fp16 scale per 128 | 2.125 bpw |
this file, ggml Q2_0, fp16 scale per 64 | 2.25 bpw |
| whole 140 MiB file over all parameters | ≈4.87 bit/param |
Of the 140 MiB, 53 MiB is the ternary matrices and 87 MiB is everything else — the tied embedding head plus convolutions, norms and biases stay fp16.
At runtime it is a hybrid by design: the encoder is expanded Q2_0 → Q8_0 on
load (exact — ternary codes and the same fp16 scale are representable in Q8_0)
because the encoder is compute-bound, while the decoder stays 2-bit. So the
encoder is mathematically ternary and physically 8-bit while running.
Code, training recipe and every measurement file: github.com/AubakirovArman/whisper-ternary
| file | what it is |
|---|---|
ggml-small-wal-ternary-q2_0.bin | 140 MiB — run this. ggml Q2_0 for whisper.cpp, CPU inference |
model.safetensors | 922 MiB — the same weights materialised dense in fp32, for verification in PyTorch |
The dense file is not a different model. It is the ternary codes multiplied by
their learned scales, written out as ordinary weights so you can reproduce the
WER numbers with transformers and confirm they match.
Q2_0 exists in ggml but had never been exercised end to end, so whisper.cpp
needs a patch — it is four small changes and it is in the repo:
git clone https://github.com/ggml-org/whisper.cpp && cd whisper.cpp
git apply ../whisper-ternary/deploy/whisper_cpp_q2_0.patch
cmake -B build && cmake --build build -j
./build/bin/whisper-cli -m ggml-small-wal-ternary-q2_0.bin -f audio.wav -ng
As a service:
./build/bin/whisper-server -m ggml-small-wal-ternary-q2_0.bin \
--host 127.0.0.1 --port 8178 -t 16 -ng -ml 100000
curl 127.0.0.1:8178/inference -F [email protected] -F response_format=json
Pass -ml 100000. Without it the server forces 60-character segments and splits
words in half (fl uttered).
from transformers import WhisperForConditionalGeneration, WhisperProcessor
model = WhisperForConditionalGeneration.from_pretrained("armanibadboy/whisper-small-ternary")
proc = WhisperProcessor.from_pretrained("armanibadboy/whisper-small-ternary")
Every quantised matrix holds at most 3 distinct values per group of 128 — check it yourself:
import torch
W = model.model.encoder.layers[0].fc1.weight
g = W[:, :128]
print(torch.unique(g / g.abs()[g != 0].min()).round()) # -> tensor([-1., 0., 1.])
Same weights, 66 s of audio, 16 threads, batch 1, Xeon:
| fp16 | ternary Q2_0 | |
|---|---|---|
| file | 465 MiB | 140 MiB |
| encoder / 30 s window | 578 ms | 473 ms |
| decode / step | 6.8 ms | 6.0 ms |
Against an H200 running the same weights in PyTorch, a single decoder step is faster on the CPU — 6.0 ms against 8.3 ms — because at batch 1 the GPU is launch-bound (439 kernel launches per decoder pass, ~15 µs each) rather than compute-bound. But end to end the H200 still wins by 2.3× (1.17 s against 2.7 s for the same 66 s of audio): the encoder is a dense matmul over 1500 frames and there the GPU is 77× ahead. Note this compares runtimes as well as hardware — hand-written C/AVX2 against PyTorch.
Stated plainly, because a quantisation result without a failure list has not been examined hard enough.
Beating stock whisper-small answers "is this model good", not "what did quantisation cost" — this model was also fine-tuned. So an fp16 control was trained on the same code path, same data, same schedule, quantisation off:
| base | openai/whisper-small |
| steps | 80 000 (70k packed LibriSpeech 951 h, then 10k repair with timestamps) |
| quantised | 192 matrices, 198 180 864 weights, alphabet {−1, 0, +1}, group 128 |
| bits per weight | 2.125 |
| method | quantisation-aware distillation, straight-through estimator |
| loss | KL against bf16 teacher + cross-entropy + feature matching |
Codes are initialised by an activation-weighted projection, not by rounding: per group of 128 inputs we solve for the codes and scale minimising the error weighted by how large the activations multiplying them actually are.
One trap worth naming, specific to this design: in classical QAT the high-precision master weights are genuinely the source the deployed checkpoint is rebuilt from. Here they are not — the group scales are separately learned parameters, so re-projecting from the latent tensor recomputes scales that were never trained, and the latent tensor is only a gradient surrogate free to drift outside its quantisation cells. Re-projecting from it produced 1000.56 % WER. The deployment representation is the trained (codes, scales) pair, from the start.
batch_size = 16
max_new_tokens = 440
greedy, num_beams = 1
Batch size shifts WER by 0.013 pp on identical code (3.4298 % / 3.4224 % / 3.4168 % at batch 16 / 8 / 1) — systematic implementation sensitivity, not random noise. Batch size is therefore pinned and differences under 0.02 pp are not treated as signal. No confidence intervals are reported yet; the test-other gap in particular has not been shown to exceed its uncertainty.
test-clean and test-other are selection sets, not sealed tests. They took
no part in gradient training, but were measured 21 times across four runs, and
this checkpoint was chosen over its own step-7500 sibling by looking at them. A
publication-grade claim needs a fresh one-shot set; that has not been done.
The CPU path was verified independently: all 2620 utterances of test-other
through patched whisper.cpp give 7.3754 %, against 7.3817 % in the PyTorch
harness — within the noise floor.
@software{aubakirov2026ternarywhisper,
author = {Aubakirov, Arman},
title = {Ternary Whisper: whisper-small at 2.125 bits per weight},
year = {2026},
url = {https://github.com/AubakirovArman/whisper-ternary}
}
MIT.