zipformer.com

An independent explainer of the ICLR 2024 paper

Speech, squeezed to 6.25 Hz and back

Zipformer runs its encoder at four frame rates. This page takes it apart, from the paper and the icefall code.

Rendered in real time (a still with reduced motion). Each lane is the same stretch of audio; slower stacks hold fewer, longer frames.

Five changes to the Conformer

Zipformer keeps the Conformer's ingredients, attention, convolution and feed-forward, and changes how they are arranged, normalized and optimized. The paper lists five changes; each gets a section below.

ConformerZipformerSection
Frame rate25 Hz throughout50, 25, 12.5, 6.25, 12.5, 25 Hz stacks; 25 Hz outputEncoder
BlockFF, self-attention, conv, FFAttention weights computed once, used by NLA and two SA modules; three FFs, two convs, two bypassesBlock
NormalizationLayerNorm per moduleOne BiasNorm per blockBiasNorm
ActivationSwishSwooshR and SwooshLSwoosh
OptimizerAdam, Noam scheduleScaledAdam, Eden scheduleScaledAdam

The encoder

Six stacks, four frame rates

Conv-Embed brings 100 Hz features to 50 Hz. Then each stack picks its own rate: 50, 25, 12.5, 6.25, 12.5, 25 Hz.

The six Zipformer stacks Conv-Embed turns 100 Hz features into a 50 Hz sequence. Six stacks follow, at 50, 25, 12.5, 6.25, 12.5 and 25 Hz. Each stack is downsampled from the 50 Hz path and upsampled back into it. A final downsample gives the 25 Hz encoder output. 50 Hz path between stacks Conv-Embed 100 Hz fbank in 50 Hz out 50 Hz stack 1, 24 frames ÷2 ×2 25 Hz stack 2, 12 frames ÷4 ×4 12.5 Hz stack 3, 6 frames ÷8 ×8 6.25 Hz stack 4, 3 frames ÷4 ×4 12.5 Hz stack 5, 6 frames ÷2 ×2 25 Hz stack 6, 12 frames ÷2 25 Hz encoder output The six Zipformer stacks at 50, 25, 12.5, 6.25, 12.5 and 25 Hz, then the 25 Hz output. 50 Hz stack 1 24 frames 25 Hz stack 2 12 frames 12.5 Hz stack 3 6 frames 6.25 Hz stack 4 3 frames 12.5 Hz stack 5 6 frames 25 Hz stack 6 12 frames 25 Hz output 12 frames

The same 0.48 s of audio in every stack. Paper Figure 1 and Section 3.1.

A Conformer runs every block at 25 Hz. Zipformer subsamples only by 2 up front, so stack 1 sees 50 Hz, and the factors (1, 2, 4, 8, 4, 2) set the rest. Between stacks the sequence is back at 50 Hz; a final Downsample gives a 25 Hz output, the rate a Conformer hands its decoder.

Downsampling by k averages each group of k frames with k softmax-normalized learned weights. Upsampling repeats each frame k times. A Bypass with a learned per-channel weight c mixes the stack's output with its input:

Bypass⁡(x,y)=(1−c)⊙x+c⊙y\operatorname{Bypass}(x, y) = (1 - c) \odot x + c \odot y

c starts limited to [0.9, 1.0] and the floor drops to 0.2 after 20,000 steps (Section 3.2), so every stack has to learn something before it may switch itself mostly off.

Widths differ per stack, largest in the middle where frames are cheapest; the sequence is truncated or zero-padded between them. Try any clip length and model size:

10.0 s

StackRateFramesWidth LayersHeadsScore entries

Where the output channels come from

Sequence lengths follow the code exactly. Feature frames are taken as 100 per second; Conv-Embed gives (T - 7) // 2 frames, a stack with factor k sees ceil(T50 / k), the output has (T50 + 1) // 2. Score entries are the size of the attention-weight matrices one forward pass builds (layers x heads x frames squared), counted from the configuration, not measured. Widths, layers and heads are paper Table 1 and Section 4.0.1.

Inside a block

One block, thirteen steps

Attention weights are computed once and used three times. That reuse pays for a block twice as deep as a Conformer's.

  1. 1 Attention weights computes W
  2. 2 Feed-forward 1
  3. 3 Non-linear attention uses W, head 0
  4. 4 Self-attention 1 uses W, all heads
  5. 5 Convolution 1
  6. 6 Feed-forward 2
  7. 7 Mid bypass
  8. 8 Self-attention 2 uses W, all heads
  9. 9 Convolution 2
  10. 10 Feed-forward 3
  11. 11 BiasNorm
  12. 12 Block bypass
  13. 13 Balancer, Whiten

Feed-forward slabs are drawn at their hidden sizes: 3/4, 1 and 5/4 of the stack's feed-forward dimension. Order from zipformer.py lines 781 to 879.

Self-attention does two quadratic things: it computes attention weights and it uses them to mix frames. Zipformer splits these into a Multi-Head Attention Weight module (MHAW) and Self-Attention modules (SA). The paper gives weight reuse as the main reason the deeper block is affordable (Section 3.2). Step through it in code order; the 3D stack above follows along.

Attention weights

zipformer.py line 781

Multi-head attention weights are computed once from the block input: softmax over query-key products plus a relative position term, 32 query dimensions per head. Nothing is aggregated yet. The weights are kept and handed to three later modules.

Attention weights: computed here.

1 of 13
One Zipformer block. Thirteen steps, one attention-weight computation, three consumers. Every module except the bypasses, the norm and the constraints adds its output to a residual stream. Paper Figure 2 (left) and Section 3.2.

Non-linear attention

NLA uses the weights for more than value averaging. With projections A, B, C of the input, each 3/4 of its width (paper Figure 2, right):

NLA⁡(x)=linear⁡(A⊙attention⁡(tanh⁡(B)⊙C))\operatorname{NLA}(x) = \operatorname{linear}\big(A \odot \operatorname{attention}(\tanh(B) \odot C)\big)

Here attention means mixing over time with one head of the shared weights, the first (attn_weights[0:1]). It is a convolution module with the convolution swapped for attention-weighted mixing.

What the ablations say

Zipformer-M, LibriSpeech test-clean / test-other WER, paper Table 5. Full model: 2.21 / 4.79. Two Conformer-style blocks in place of each Zipformer block: 2.18 / 4.95, with 73.9M parameters instead of 65.6M. No NLA: 2.16 / 4.97. No Bypass: 2.25 / 4.86. No NLA and no weight sharing: 2.20 / 5.10, slower with no gain, which the authors read as the block's two attention patterns being similar enough to share.

Normalization

BiasNorm keeps the length

LayerNorm makes every vector the same length. BiasNorm lets a learned bias decide how much length survives.

LayerNorm every output lands on the same circle 1 in 0.15in 0.4in 1in 2.5in 6 BiasNorm small inputs stay small, large ones are capped 1 in 0.15in 0.4in 1in 2.5in 6

Output RMS for inputs of RMS 0.15 to 6, using the example settings of the figure below (bias 4 on one of 12 channels).

The authors saw two habits in trained Conformers that they blame on LayerNorm (Section 3.3). Some set one channel to a large constant, for example 50, to keep length information alive. Some modules go "dead", with outputs near 10-6: the LayerNorm scale γ\gamma drifts toward zero and, if it oscillates around zero, the module's gradient keeps flipping sign.

LayerNorm⁡(x)=x−E[x]Var⁡[x]+ϵ⊙γ+βBiasNorm⁡(x)=xRMS⁡[x−b]⋅exp⁡(γ)\begin{aligned}\operatorname{LayerNorm}(x) &= \frac{x - \mathrm{E}[x]}{\sqrt{\operatorname{Var}[x] + \epsilon}} \odot \gamma + \beta \\[6pt] \operatorname{BiasNorm}(x) &= \frac{x}{\operatorname{RMS}[x - b]} \cdot \exp(\gamma)\end{aligned}

The per-channel bias b only enters the RMS, so a large entry plays the constant channel's role without using a real one. exp⁡(γ)\exp(\gamma) is always positive, so the scale cannot flip sign, and there is no mean subtraction. Zipformer has one BiasNorm per block and one in Conv-Embed, and no per-module LayerNorms.

  • LayerNorm
  • LayerNorm with an extra constant channel (value 4)
  • BiasNorm with b = 4 on channel 1, exp(γ) = 1
A 12-channel vector, scaled up and down. Left: RMS of the 12 output channels as the input RMS changes. LayerNorm output is flat: length is gone. A constant channel or a BiasNorm bias keeps output size proportional to input size when the input is small, and normalizes it when it is large. Right: the output channels at the current input size. The bias value 4 is an example, not a trained value.

SwooshR and SwooshL

Activations that never go flat

Swish flattens out for negative inputs. Both Swoosh functions keep a slope of -0.08 there.

SwooshR SwooshL Swish

Curves computed in your browser from Equation 4 and Swish (Equation 3), over x = -8 to 8.

SwooshR⁡(x)=log⁡(1+ex−1)−0.08x−0.313261687SwooshL⁡(x)=log⁡(1+ex−4)−0.08x−0.035\begin{aligned}\operatorname{SwooshR}(x) &= \log(1 + e^{x-1}) - 0.08x - 0.313261687 \\ \operatorname{SwooshL}(x) &= \log(1 + e^{x-4}) - 0.08x - 0.035\end{aligned}

SwooshR's constant puts it through the origin. SwooshL's was tuned and slightly beat the value that would, so SwooshL(0) is about -0.017. The letter names which zero crossing sits near x = 0 (Section 3.4).

The left-hand slope is the point. With Swish, a unit whose input stays negative gets almost no gradient, and the paper notes that the Adam denominator can then get dangerously small.

-3.00
  • SwooshR
  • SwooshL
  • Swish
  • ReLU
Swoosh keeps a gradient where Swish loses it. Curves are computed in your browser from the formulas above. Paper Appendix A.2 (Figure A.3) shows the same three curves over [-10, 10].

Placement follows a training observation. With SwooshR everywhere, the modules behind a bypass, feed-forward and the ConvNeXt layer, learned a large negative bias to be "normally off". SwooshL is near zero around x = 0, so they get SwooshL; convolution modules and the rest of Conv-Embed get SwooshR. On Zipformer-M, SwooshR everywhere costs 0.11 / 0.42 WER (test-clean / test-other), and Swish everywhere gives 2.27 / 5.37 against 2.21 / 4.79 (Table 5).

Balancer and Whiten

Guard rails in the backward pass

Two modules that do nothing going forward and nudge the gradient going back, so activations stay in range.

Before: one direction holds 98% of the variance, metric 1.94
Whiter: the largest direction holds 56%, metric 1.02

Illustration in two dimensions with 160 sampled points. Whiten works on a whole layer's channels, with the metric defined below.

Without per-module normalization, something has to keep activations in range. Balancer and Whiten add the gradient of an auxiliary loss to the real gradient, kept small next to it (Appendix A.3). The paper says their placement follows no clean rule: they were added where a module trained badly or diverged.

Balancer

Per channel, it bounds the proportion of positive values and the mean absolute value, to catch values that grow too large or small (a mixed-precision risk) and "dead" channels that are almost always negative. Under a Gaussian assumption these become limits on E[x]/Var⁡[x]\mathrm{E}[x]/\sqrt{\operatorname{Var}[x]} and RMS⁡[x]\operatorname{RMS}[x] (Equation 9). The extra gradient is scaled to 0.04 times the real gradient's magnitude at each element, so padding frames get tiny corrections.

0.60
1.00
One channel, modelled as a Gaussian. Limits are the ones in the code: block output min_positive=0.45, max_positive=0.55, min_abs=0.2, max_abs=4.0; feed-forward 3 output 0.3, 0.7, min_abs 0.2 (after 4,000 batches), max_abs=4.0. The code converts them with the same approximations as the paper: RMS limit = 1.2533 x abs limit, mean/std limit = 0.8140 x atanh(2p - 1).

Whiten

The second failure is a layer whose covariance is dominated by one or a few directions, seen in models about to diverge. Whiten measures it as

Lwhiten=∑iλi2/D(∑iλi/D)2=∑i,jCij2/D(∑iCii/D)2\mathcal{L}_{\text{whiten}} = \frac{\sum_i \lambda_i^2 / D}{\left(\sum_i \lambda_i / D\right)^2} = \frac{\sum_{i,j} C_{ij}^2 / D}{\left(\sum_i C_{ii} / D\right)^2}

over the eigenvalues of the mean-subtracted covariance C: 1 when white, D when one direction holds everything, and computable without an eigendecomposition. It penalizes only above a limit: 10 in the paper, a schedule from 4 to 12 over 20,000 batches for the block output in the code.

0.90

A 256-channel covariance with eigenvalues proportional to i-a. Bars are the eigenvalues, largest first, on a linear scale. The metric is computed exactly from them. Paper Table 6: on Zipformer-M trained 40 epochs, removing Whiten moves test-other WER from 4.91 to 5.15; removing Balancer moves it to 4.97, and the authors keep Balancer mainly as protection against divergence.

ScaledAdam and Eden

An optimizer that respects scale

Every tensor moves by about the same fraction of its size per step, and its learning rates are numerically much larger than Adam's.

0.000.010.020.030.04 015k30k45k60k75k Eden + ScaledAdam: peak 0.045 Adam, Noam schedule: peak about 0.0033 training step (2,500 steps per epoch assumed)

Eden with the icefall LibriSpeech recipe values (base 0.045, lr_batches 7500, lr_epochs 3.5, warm-up from 0.1 over 500 steps), assuming 2,500 steps per epoch. Dashed: the Adam schedule from paper Section 4.0.3 with base 7.5.

Adam's step on each element is roughly the learning rate whatever the parameter's size: large in relative terms for a tensor with RMS 0.01, small for one with RMS 1. ScaledAdam multiplies the update by the tensor's current RMS rt-1 (Section 3.5, Equation 6):

Δt=−αt⋅rt−1⋅1−β2t1−β1t⋅mtvt+ϵrt−1=RMS⁡[θt−1]\begin{aligned}\Delta_t &= -\alpha_t \cdot r_{t-1} \cdot \frac{\sqrt{1-\beta_2^t}}{1-\beta_1^t} \cdot \frac{m_t}{\sqrt{v_t} + \epsilon} \\ r_{t-1} &= \operatorname{RMS}[\theta_{t-1}]\end{aligned}

It also learns the scale directly: treat each tensor as θ=r⋅θ′\theta = r \cdot \theta', run Adam on r with gradient ht=gt⋅θt−1h_t = g_t \cdot \theta_{t-1}, and step along the tensor itself (Equation 7):

Δt,r′=−η⋅αt⋅1−β2t1−β1t⋅ntwt+ϵ θt−1η=0.1\begin{aligned}\Delta'_{t,r} &= -\eta \cdot \alpha_t \cdot \frac{\sqrt{1-\beta_2^t}}{1-\beta_1^t} \cdot \frac{n_t}{\sqrt{w_t} + \epsilon} \, \theta_{t-1} \\ \eta &= 0.1\end{aligned}

Shrinking a tensor with plain gradient steps is hard, since step noise tends to grow its norm. This term handles it, and it is why Zipformer can drop most normalization layers. The code adds limits the paper does not spell out: scale updates every 4 steps, each clamped to [-0.1, 0.1]; tensor RMS kept in [1e-5, 3.0]; scalars get plain Adam at 0.1 times the learning rate; gradients clipped at twice the recent median norm (clipping_scale=2.0).

0.010

Relative change per step, in units of the learning rate. With a normalized gradient of about 1 per element, Adam changes a tensor by about lr / RMS in relative terms; ScaledAdam by about lr for every tensor down to param_min_rms. A schematic of Equation 5 against Equation 6, not a training measurement.

The Eden schedule

ScaledAdam is less prone to divergence, so it needs no long warm-up, and its learning rates are numerically larger because parameter RMS values are usually well below 1. Eden decays with both step t and epoch e (Equation 8):

αt=αbase⋅(t2+αstep2αstep2)−0.25⋅(e2+αepoch2αepoch2)−0.25⋅linear⁡(αstart,twarmup,t)\begin{aligned}\alpha_t = \alpha_{\text{base}} &\cdot \left(\frac{t^2 + \alpha_{\text{step}}^2}{\alpha_{\text{step}}^2}\right)^{-0.25} \cdot \left(\frac{e^2 + \alpha_{\text{epoch}}^2}{\alpha_{\text{epoch}}^2}\right)^{-0.25} \\ &\cdot \operatorname{linear}(\alpha_{\text{start}}, t_{\text{warmup}}, t)\end{aligned}

Two factors keep parameter change per hour of data roughly constant when the batch size changes. The paper uses base 0.045, start 0.5 and a 500-step warm-up; the icefall recipe uses lr_batches = 7500, lr_epochs = 3.5 and a warm-up start of 0.1. The epoch counter is 0 during the first epoch (scheduler.step_epoch(epoch - 1)).

Eden, computed exactly as optim.py does. Warm-up is 500 steps. Steps per epoch depend on your data and batch size; 2,500 is only a starting value. The Adam comparison uses the schedule the paper gives in Section 4.0.3, αbase⋅512−0.5⋅min⁡(t−0.5, t⋅10000−1.5)\alpha_{\text{base}} \cdot 512^{-0.5} \cdot \min(t^{-0.5},\, t \cdot 10000^{-1.5}), which peaks near 0.0033 at step 10,000. Paper Figure A.2 shows both families on the same axis.

The result in Table 5: with Adam, Zipformer-M needed a BiasNorm in every module to avoid divergence, and its best run reached 2.38 / 5.51 WER on test-clean / test-other against 2.21 / 4.79 with ScaledAdam. Learning-rate factors 0.025, 0.035, 0.045 and 0.055 were tried for ScaledAdam and 2.5, 5.0, 7.5 and 10.0 for Adam; the best of each is reported.

Chunks, left context, caches

Streaming, one chunk at a time

Each frame sees its own chunk and a few chunks back, never ahead. The encoder runs once per chunk.

keys (time) queries (time)

Illustration: 24 frames, chunks of 4, two chunks of left context. Raised cells are attended; chunks light up in the order a streaming encoder processes them.

The paper evaluates full-context models only; streaming lives in the icefall code. With --causal 1, each batch draws a chunk size from --chunk-size (default 16,32,64,-1) and a left context from --left-context-frames (default 64,128,256,-1), in 50 Hz frames; -1 means no limit. One model can then run at several chunk sizes.

masked(i,j)=(cj>ci)  ∨  (cj<ci−L)ci=⌊i/chunk⌋,L=max⁡ ⁣(1,⌊left/chunk⌋)\begin{aligned}\text{masked}(i, j) &= \big(c_j > c_i\big) \;\lor\; \big(c_j < c_i - L\big) \\ c_i &= \lfloor i / \text{chunk} \rfloor, \qquad L = \max\!\big(1, \lfloor \text{left} / \text{chunk} \rfloor\big)\end{aligned}

The mask is built at 50 Hz and each stack takes every k-th row and column, so chunk boundaries line up across stacks. Hence the code's asserts: the chunk size must divide by every downsampling factor, and the left context must cover half of each convolution kernel.

Chunk
Left context
Seen by
keys (past to future)

attended masked

The attention mask for 192 frames at 50 Hz (3.84 s), rows are queries. Switch the stack to see the same mask subsampled by 2, 4 or 8: each stack sees the same chunk boundaries in time, with fewer frames per chunk. Computed with the formula above.

Convolution without lookahead

Convolutions look ahead too: kernel 31 sees 15 frames into the future. Causal Zipformer swaps in a chunk-causal depthwise convolution: a causal convolution of half the kernel width plus one that runs only inside the current chunk, scaled by a learned, position-dependent factor. A frame can still use frames to its right within its own chunk, never beyond.

What a streaming step costs and carries

Each call takes T=2⋅chunk+13T = 2 \cdot \text{chunk} + 13 feature frames: twice the chunk because Conv-Embed halves the rate, plus 7 for its convolutions and 6 for the 3 frames of right padding its ConvNeXt layer needs at 50 Hz. It returns chunk / 2 frames at 25 Hz. Every layer carries six tensors between calls: cached keys, NLA inputs, values for both SA modules, and the left context of both convolutions, with left_context_frames // k frames for a stack with factor k. The ONNX export adds a Conv-Embed cache and a processed-frame count.

Chunk
Left context
Latency follows the chunk. A frame cannot be encoded until its chunk is complete, so the first frame of a chunk waits a full chunk and the last one almost nothing; the small fixed lookahead inside the 13 extra feature frames and the compute time come on top. Cache sizes are computed from get_init_states for the Zipformer-M configuration (paper Table 1), 4 bytes per value. Chunk 8 is not in the default training list; it is shown because some deployments train with it.

Smaller chunks mean lower latency and less right context; longer left context means more state and more attention work per step. The accuracy cost of a chunk size is something to measure on your own data.

What the paper reports

Less compute, lower error

Zipformer-M beats the authors' reproduced Conformer-L on LibriSpeech test-other with about a fifth of the encoder GFLOPs.

  1. Zipformer-Mpruned transducer 62.9 GFLOPs4.79% test-other
  2. Zipformer-Lpruned transducer 107.7 GFLOPs4.63% test-other
  3. E-Branchformer-LCTC/AED 284.4 GFLOPs4.55% test-other
  4. Conformer-L, reproducedpruned transducer 294.2 GFLOPs5.55% test-other
  5. Squeezeformer-LCTC 333.7 GFLOPs5.97% test-other

Paper Table 2. GFLOPs are for the encoder on 30 s of audio, measured with DeepSpeed.

Every number here is copied from the paper (arXiv 2310.11230v4): pruned transducer unless stated, beam search of size 4, no external language model. The authors' summary: "In terms of efficiency, Zipformer converges faster during training and speeds up the inference by more than 50% compared to previous studies while requiring less GPU memory." (Section 1)

The paper flags a baseline problem: neither its own Conformers nor the WeNet and ESPnet ones reach the original Conformer paper's 2.1 / 4.3 for Conformer-L. Its longer-trained Zipformer-L reaches 2.00 / 4.38, "the first model to approach Conformer-L to the best of our knowledge."

Encoder GFLOPs against LibriSpeech test-other WER for the models in paper Table 2 that report GFLOPs. 4.5 5.5 6.5 7.5 8.5 9.5 20 50 100 200 400 encoder GFLOPs for 30 s of audio (log scale) test-other WER % Squeezeformer-XS (CTC): 9M params, 18.2 GFLOPs, 3.74 / 9.09 Squeezeformer-S (CTC): 18.6M params, 33.7 GFLOPs, 3.08 / 7.47 Squeezeformer-SM (CTC): 28.2M params, 47.6 GFLOPs, 2.79 / 6.89 Squeezeformer-M (CTC): 55.6M params, 88.4 GFLOPs, 2.56 / 6.5 Squeezeformer-ML (CTC): 125.1M params, 183.3 GFLOPs, 2.61 / 6.05 Squeezeformer-L 5.97 Squeezeformer-L (CTC): 236.3M params, 333.7 GFLOPs, 2.47 / 5.97 E-Branchformer-B (CTC/AED): 41.1M params, 78.1 GFLOPs, 2.49 / 5.61 Branchformer (CTC/AED): 116.2M params, 238.3 GFLOPs, 2.4 / 5.5 E-Branchformer-L 4.55 E-Branchformer-L (CTC/AED): 148.9M params, 284.4 GFLOPs, 2.14 / 4.55 Conformer-S (reproduced) (pruned transducer): 9.8M params, 29.1 GFLOPs, 3.75 / 9.24 Conformer-M (reproduced) (pruned transducer): 28.4M params, 77 GFLOPs, 2.96 / 7.11 Conformer-L (reproduced) 5.55 Conformer-L (reproduced) (pruned transducer): 122.5M params, 294.2 GFLOPs, 2.46 / 5.55 Zipformer-S 5.73 Zipformer-S (pruned transducer): 23.3M params, 40.8 GFLOPs, 2.42 / 5.73 Zipformer-M 4.79 Zipformer-M (pruned transducer): 65.6M params, 62.9 GFLOPs, 2.21 / 4.79 Zipformer-L 4.63 Zipformer-L (pruned transducer): 148.4M params, 107.7 GFLOPs, 2.06 / 4.63
  • Zipformer, pruned transducer
  • Conformer reproduced by the authors, pruned transducer
  • (E-)Branchformer, CTC/AED
  • Squeezeformer, CTC
Paper Table 2, LibriSpeech. Every row that reports encoder GFLOPs (measured with DeepSpeed for a 30 s input). The original Conformer rows have no GFLOPs and are not plotted. Hover or focus a point for parameters and test-clean WER. Paper Figure 3 makes the same comparison with measured inference time and peak memory.
Selected rows of Table 2. WER %, LibriSpeech.
ModelTypeParams (M)GFLOPstest-cleantest-other
Conformer-L (Gulati et al.)transducer118.8-2.14.3
Conformer-L, reproducedpruned transducer122.5294.22.465.55
E-Branchformer-LCTC/AED148.9284.42.144.55
Squeezeformer-LCTC236.3333.72.475.97
Zipformer-Spruned transducer23.340.82.425.73
Zipformer-Mpruned transducer65.662.92.214.79
Zipformer-Lpruned transducer148.4107.72.064.63
Zipformer-L, longer trainingpruned transducer148.4107.72.004.38

Zipformer also trains with other heads. In Table 8, CTC-only Zipformer-S/M/L reach 2.85 / 6.91, 2.51 / 6.02 and 2.49 / 5.7, and CTC/AED versions with a 6-layer Transformer decoder reach 2.46 / 6.04, 2.22 / 4.97 and 2.09 / 4.59.

Change from the base modeltest-other WER %

  1. Zipformer-M (base) 4.79 clean 2.21, 65.6M
  2. No temporal downsampling 5.09 clean 2.23, 94.2M
  3. Double Conformer-style blocks 4.95 clean 2.18, 73.9M
  4. No NLA 4.97 clean 2.16, 58.7M
  5. No NLA, no attention weight sharing 5.10 clean 2.20, 60.9M
  6. No Bypass 4.86 clean 2.25, 65.5M
  7. LayerNorm instead of BiasNorm 4.97 clean 2.29, 65.6M
  8. Only SwooshR 5.21 clean 2.32, 65.5M
  9. Swish 5.37 clean 2.27, 65.5M
  10. Adam instead of ScaledAdam 5.51 clean 2.38, 65.6M
Paper Table 5: one change at a time on Zipformer-M. Dot is test-other WER %, the dashed line is the base model. Every change makes test-other worse; test-clean moves much less, and two variants are slightly better on test-clean (No NLA 2.16, double Conformer blocks 2.18).

Mandarin

Table 3, Aishell-1, CER %.
ModelParams (M)DevTest
Conformer, ESPnet46.24.54.9
Conformer, WeNet46.3-4.61
E-Branchformer, ESPnet37.94.24.5
Branchformer45.44.194.43
Zipformer-S30.24.44.67
Zipformer-M73.44.134.4
Zipformer-L157.34.034.28
Table 4, WenetSpeech, CER %.
ModelParams (M)DevNetMeeting
Conformer, ESPnet116.99.708.9015.90
Conformer, WeNet116.98.889.7015.59
Conformer-MoE (16e)4257.678.2813.96
Zipformer-S32.37.968.613.97
Zipformer-M75.97.327.6112.35
Zipformer-L160.97.297.2412.06

Running it

From checkpoint to phone

Export a streaming model to ONNX and run it with sherpa-onnx, on servers, phones and in the browser.

  1. icefall checkpoint PyTorch, trained with --causal 1
  2. export-onnx-streaming.py fixed chunk and left context
  3. encoder, decoder, joiner three .onnx files with metadata
  4. sherpa-onnx fbank, caches, decoding

AndroidiOSWindowsmacOSLinuxHarmonyOSWebAssembly

C++CPythonJavaScriptJavaC#KotlinSwiftGoDartRustPascal

Platforms and API languages as listed in the sherpa-onnx README.

Training lives in icefall; deployment usually goes through k2-fsa/sherpa-onnx. A streaming export bakes one chunk size and left context into the graph.

Export a streaming transducer

The architecture flags must match training. Chunk size and left context are picked from the values the model was trained with (from 16,32,64 and 64,128,256 for the default recipe):

# in icefall/egs/librispeech/ASR, with $repo holding exp/epoch-99.pt
./zipformer/export-onnx-streaming.py \
  --tokens $repo/data/lang_bpe_500/tokens.txt \
  --use-averaged-model 0 --epoch 99 --avg 1 \
  --exp-dir $repo/exp \
  --num-encoder-layers "2,2,3,4,3,2" \
  --downsampling-factor "1,2,4,8,4,2" \
  --feedforward-dim "512,768,1024,1536,1024,768" \
  --num-heads "4,4,4,8,4,4" \
  --encoder-dim "192,256,384,512,384,256" \
  --query-head-dim 32 --value-head-dim 12 \
  --pos-head-dim 4 --pos-dim 48 \
  --encoder-unmasked-dim "192,192,256,256,256,192" \
  --cnn-module-kernel "31,31,15,15,15,31" \
  --decoder-dim 512 --joiner-dim 512 \
  --causal True --chunk-size 16 --left-context-frames 128

This writes an encoder, a decoder and a joiner. The encoder carries its streaming contract as ONNX metadata: model_type=zipformer2, decode_chunk_len (twice the chunk, in feature frames), T (decode_chunk_len + 13), left_context_len, and the per-stack layers, widths, kernels and head sizes. A runtime reads these instead of hard-coding them; check them first when a port produces garbage.

Decode a stream

import numpy as np, sherpa_onnx, soundfile as sf

rec = sherpa_onnx.OnlineRecognizer.from_transducer(
    tokens="tokens.txt",
    encoder="encoder-epoch-99-avg-1-chunk-16-left-128.onnx",
    decoder="decoder-epoch-99-avg-1-chunk-16-left-128.onnx",
    joiner="joiner-epoch-99-avg-1-chunk-16-left-128.onnx",
    num_threads=2, sample_rate=16000, feature_dim=80,
    decoding_method="greedy_search",
)
samples, sr = sf.read("test.wav", dtype="float32")
s = rec.create_stream()
s.accept_waveform(sr, samples)           # call repeatedly with live audio
s.accept_waveform(sr, np.zeros(int(0.66 * sr), dtype=np.float32))  # tail padding, as in the official example
s.input_finished()
while rec.is_ready(s):
    rec.decode_stream(s)
print(rec.get_result(s))

A CTC-only Zipformer uses OnlineRecognizer.from_zipformer2_ctc(tokens=..., model=...) instead, with one ONNX file. sherpa-onnx computes the 80-dimensional fbank features itself from 16 kHz audio, so the front end matches what icefall trained on.

On a device

Zipformer in the field

Case study: Qur'an recitation, phoneme by phoneme

Quran Lab's streaming phoneme recognizer for recitation, built on Zipformer2.

#1

on the open Quranic ASR leaderboard

zipformer_p-arabic-v3, a streaming Zipformer2 phoneme model with a CTC head, ranked first of 33 entries on 3 November 2026. See the leaderboard.

Everything in this section comes from the public model card of Quran-Lab/zipformer_p-arabic-v3. The current release is v3.1, a fine-tune of v3 that retunes madd (elongation) length; it keeps the architecture, streaming interface and token table, so it drops in for v3 in every format.

The card describes a 65.5M-parameter streaming phoneme recognizer for Hafs 'an 'Asim: Zipformer2 with a CTC head, emitting a 251-symbol Qur'anic phonetic alphabet that encodes madd length, gemination, ghunna, ikhfaa, qalqalah and the emphatic consonants. It uses no language model anywhere, because a language model silently corrects a reciter's mistakes, which an assessment system must never do.

How the architecture shows up in the release

The card also lists measured limitations, for example that a model trained almost entirely on correct recitation with canonical labels tends to transcribe the rule rather than a reciter's deviation from it. Read the card before building on it; the model is released under the Quran-Lab No-Profit License 1.2 with gated access.

References

  1. Z. Yao, L. Guo, X. Yang, W. Kang, F. Kuang, Y. Yang, Z. Jin, L. Lin, D. Povey. Zipformer: A faster and better encoder for automatic speech recognition. ICLR 2024. arXiv 2310.11230, PDF (v4)
  2. k2-fsa/icefall, LibriSpeech Zipformer recipe: zipformer.py, scaling.py, optim.py, subsampling.py, train.py, export-onnx-streaming.py. Line links on this page point to commit 9c96676. icefall, zipformer.py, scaling.py, optim.py
  3. k2-fsa/sherpa-onnx: speech recognition runtime with ONNX Runtime. GitHub, online-decode-files.py
  4. A. Gulati et al. Conformer: Convolution-augmented Transformer for speech recognition. Interspeech 2020. arXiv 2005.08100
  5. S. Kim et al. Squeezeformer: An efficient Transformer for automatic speech recognition. NeurIPS 2022. arXiv 2206.00888
  6. F. Kuang et al. Pruned RNN-T for fast, memory-efficient ASR training. 2022. arXiv 2206.13236
  7. Quran Lab. zipformer_p-arabic-v3 model card (v3.1 current). Hugging Face
  8. Quran Lab. Quranic ASR leaderboard. Hugging Face Space

Where the paper and the current code differ, this page says so: Eden's warm-up start (0.5 in the paper, 0.1 in the LibriSpeech train.py) and the Whiten limit (10 in the paper, a 4 to 12 schedule for the block output in the code).