Audio Kernels Deep Dive: From WAV to Encoder Tokens, Kernel by Kernel

Kernel Source
src/kernels/audio_kernels.c and include/ckernel_audio.h — numerically explicit FP32 reference kernels. Every stage below is a named C function with a PyTorch oracle, not hidden preprocessing. Pipeline overview: Concepts — Audio Frontend; operator commands: v8 Runbook; evidence: Whisper Tiny End-to-End.

The Whisper Tiny frontend turns a PCM16 WAV into the [1500 × 384] token matrix the audio encoder consumes. CK breaks that transformation into seven circuit operations across six frontend stages — STFT table construction and STFT execution are separate ops — each with one job, one layout contract, and one parity oracle. This page walks each kernel the same way the DeltaNet deep dive walks the recurrent update: the math, the way the C actually computes it, the memory layout, and the evidence that pins it to PyTorch.

Audio frontend kernel dataflow from waveform to encoder tokens

Table of Contents


1. WAV Decode: Bytes With a Contract, Not a Library Call

Most pipelines hand WAV parsing to a media library. CK walks the RIFF container by hand because the frontend's input contract is part of the numerical evidence: PCM (format tag 1), 16-bit, any channel count, any sample rate — anything else is rejected with a distinct error code before a single float is produced. audio_wav_parse_memory validates the container, audio_wav_decode_pcm16_mono_f32 turns it into mono f32, and the frontend circuit's map-owned provider audio_wav_decode_memory_pcm16_mono_f32 fuses the two into the single call the generated code makes. For audio that is already interleaved int16 in memory, audio_pcm_s16_to_mono_f32 applies the same conversion without the container walk.

KERNEL 1 · RIFF CONTAINER WALK Parse, Validate, Then Average Channels "RIFF" size | "WAVE" fmt chunk (≥16 B) tag=1 · ch · rate · 16-bit ...chunks... odd sizes pad +1 data chunk interleaved int16 LE frames = data_bytes / (channels · 2) DECODE LOOP · PER FRAME sum = Σ_c (int16)read_u16_le(pcm + (f·C + c)·2) mono[f] = (sum / channels) · (1 / 32768) little-endian safe by construction — bytes are assembled, never cast REJECTION IS TYPED -2 not RIFF/WAVE · -3 truncated -4 fmt too small -5 not PCM16 · -6 bad frame count every failure mode has its own code

How The C Reads

  • The chunk walker starts at offset 12 and hops chunk-to-chunk, adding a pad byte for odd-sized chunks — RIFF alignment is part of the format.
  • Only the first fmt and data chunks are honored; everything else is skipped, so metadata-heavy WAVs (LIST, cue, bext) parse fine.
  • Multi-channel audio is downmixed by mean, not by taking channel 0 — stereo and mono files produce the same scale.

Why Not libsndfile

The frontend's whole job is to be a numerical contract. A hand-rolled 60-line parser means the exact set of accepted inputs is auditable, the downmix rule is explicit, and no platform codec version can drift the parity gates. The generated artifact owns WAV-to-encoder execution through ck_model_run_audio_wav; the transformer body itself never sees a WAV — it consumes the log-Mel tensors this kernel chain guarantees.


2. Resample: Two Explicit Paths to 16 kHz

Whisper expects 16 kHz mono. CK exposes two named resamplers and the contract records which one a run used: audio_resample_linear_f32 for the fast path, and audio_resample_windowed_sinc_f32 for the band-limited path. Both share one frame-count convention — audio_resampled_frame_count returns 1 + (n_in − 1) · r_out / r_in, preserving both endpoints of the signal, so a file never grows or shrinks silently by a rounding choice.

KERNEL 2 · WINDOWED SINC INTERPOLATION Every Output Frame Is a Normalized, Windowed Weighted Sum input samples (r_in) sinc(cutoff·d) Hann window over radius source = f·r_in/r_out THE WEIGHTS, EXACTLY cutoff = min(1, r_out/r_in) sinc(x) = sin(πx)/(πx) win(d) = ½(1 + cos(π·d/radius)) w = cutoff · sinc(cutoff·d) · win(d) y[f] = Σ w·x[tap] / Σ w Σw normalization keeps the edges exact where taps run off the signal downsampling → cutoff < 1 narrows the sinc: anti-aliasing is a property of the kernel, not a separate filter stage

How The C Reads

  • All position math is double (source = frame · r_in / r_out), so long files do not accumulate phase drift.
  • The linear path does its interpolation in integer numerator arithmetic plus one fmaf lerp — no float division per sample.
  • Out-of-range taps are skipped, not clamped; the Σw denominator makes the surviving window exact at both signal edges.
  • radius is bounded to [2, 128] — the kernel support is part of the validated contract.

Why Two Kernels Instead of One Flag

A boolean fast=true inside one function would hide which numerical path produced the evidence. Two named entry points mean the run report can say which resampler fed the STFT, and the parity gate can pin each one separately. Whisper Tiny at 16 kHz input skips this kernel entirely — but the contract has to exist for the day a 44.1 kHz file shows up.


3. Pad / Truncate: The Fixed 480,000-Sample Contract

Everything downstream of resampling assumes a fixed time extent: the STFT frame count, the encoder's positional embedding table, and the 1500-token cross-attention bridge all derive from exactly 480,000 samples (30 s at 16 kHz). audio_pad_or_truncate_f32 is the kernel that makes that true for arbitrary input — short clips are zero-padded, long clips are truncated (not chunked), and the function returns how many frames were real.

KERNEL 3 · EXTENT NORMALIZATION Every Clip Becomes Exactly 30 Seconds SHORT CLIP · n < 480,000 memmove the n real frames, then memset the tail to 0.0 LONG CLIP · n > 480,000 memmove the first 480,000 frames; the rest is dropped — not chunked THE WHOLE KERNEL copied = min(input_frames, output_frames) out[copied:] = 0 · return copied WHY IT IS A CIRCUIT OP the extent is a numerical contract, so the compiler owns it — not Python

How The C Reads

  • memmove, not memcpy — the copy is overlap-safe even if caller and callee ever share a buffer.
  • Only the tail beyond copied is zeroed; real samples are never touched, so a full 30 s clip is a pure pass-through.
  • The return value is the real frame count — downstream stages can distinguish signal from padding without a side channel.

Why Not Implicit In Resample

Extent normalization is a separate circuit operation (audio_pad_or_truncate) because the 30 s limit is a policy with numerical consequences — zero-padding changes the STFT energy and truncation loses content. Naming it keeps the policy auditable, keeps the resampler's contract purely about rate, and lets the limitation docs point at one honest line: longer files are truncated, not chunked.


4. STFT Power: Whisper Geometry as a 20×20 Mixed-Radix FFT

The spectrum is where the frontend spends nearly all of its FLOPs, and where parity is easiest to break. Whisper's geometry is fixed: n_fft = 400, hop = 160, 201 bins, a periodic Hann window, and center = true with reflect padding. CK has three implementations of the same contract — a table-driven direct DFT (audio_stft_power_precomputed_f32), a Whisper-specialized mixed-radix FFT (audio_stft_power_fft400_f32), and the fully self-contained reference (audio_whisper_stft_power_reference_f32) that the nightly gate runs against PyTorch.

KERNEL 4 · SHORT-TIME FOURIER TRANSFORM, POWER SPECTRUM Frame → Reflect-Pad → Window → 20×20 FFT → |X|² FRAME t 400 samples, hop 160 centered: pad 200 each side reflect_index mirrors edges x[-1]=x[1] · x[n]=x[n-2] · no zeros × HANN w[n] = ½ − ½cos(2πn/400) periodic, not symmetric MIXED-RADIX 400 = 20×20 stage 1: 20-pt DFTs over x[p+20q] stage 2: combine over p, twiddle f·p twiddles precomputed [201×400] ≈6× fewer FMAs than direct DFT POWER P = re² + im² one fmaf, no sqrt [201 bins × T frames] T = n_samples / 160 WHY THESE CHOICES MATCH TORCH.STFT EXACTLY • periodic Hann — torch.hann_window(400, periodic=True) divides by n_fft, not n_fft−1; the table does the same • reflect pad + drop last frame — center=True pads 200 both sides; T = n/160 mirrors the oracle's spectrum[:, :-1] • fmaf accumulation — real/imag accumulate in fp32 fused multiply-add, matching the validated error budget (rel max ≤ 2e-5)

How The C Reads

  • audio_stft_precompute_tables_f32 builds the window and the full [bins × n_fft] cos/sin tables once — the per-frame kernels never call cosf/sinf in the hot loop.
  • The direct kernel is one fmaf per (sample, bin) per component: simple, obviously correct, and the reference every other variant is checked against.
  • The fft400 kernel treats 400 as 20×20: stage 1 computes twenty 20-point DFTs over the stride-20 subsequences, stage 2 combines them per output bin with a second twiddle lookup. Scratch is exactly 2 × 400 floats.
  • reflect_index is a loop, not an abs — double reflections at very short signals still land in range.

Why Keep Three Implementations

Same pattern as the DeltaNet reference-first policy: the reference kernel is what the nightly PyTorch gate pins (tones, noise, and impulse signals). The precomputed and fft400 kernels are performance variants whose outputs must agree with that reference — so a faster FFT can never silently change the numbers the model was validated with. The circuit picks the variant; the contract keeps them equal.


5. Mel Filters: The Slaney Filterbank, Built Once

Between the STFT power grid and the log-Mel projection sits the filterbank itself: 80 triangular filters over the 201 power bins, spaced evenly on the Slaney mel scale between 0 Hz and Nyquist. audio_whisper_mel_filters_slaney_f32 constructs the matrix once at table-build time — in double precision — so the per-frame work stays a single f32 matmul and the triangle geometry can never drift between runs.

KERNEL 5 · SLANEY TRIANGULAR FILTERBANK 80 Triangles, Evenly Spaced in Mel, Area-Normalized 0 Hz 8 kHz · 201 bins narrow at low Hz wider at high Hz 82 mel points (n_mels + 2) → 80 triangles · peak = 2 / (right − left) CONSTRUCTION · DOUBLE PRECISION mel(hz) — Slaney: linear < 1 kHz, log above · points at i/(n_mels+1) lower = (hz − left)/(center − left) upper = (right − hz)/(right − center) w = max(0, min(lower, upper)) × norm norm = 2/(right−left) — Slaney area weighting, so loudness sums comparably across bands

How The C Reads

  • All geometry is computed in double — mel conversions, triangle edges, and the area normalization — then stored as f32; the table is built once and reused for every frame.
  • Triangles come from n_mels + 2 mel-spaced points converted back to Hz; each bin's weight is max(0, min(lower, upper)) scaled by 2/(right−left).
  • Arguments are validated with typed rejections (−1 null, −2 bad geometry: odd n_fft, non-positive rates or counts).

Why A Separate Circuit Op

The filterbank used to live inside the composite log-Mel reference. Promoting it to its own operation (audio_mel_filters) makes the Slaney identity a named, parity-gated artifact: the mel scale choice, the +2 point convention, and the area normalization each appear in the numerical contract, and the generated frontend passes the matrix into log-Mel as data — the way PyTorch passes a buffer, not a re-computation.


6. Log-Mel: Perceptual Compression With a Global Clamp

audio_whisper_log_mel_from_power_reference_f32 turns the [201 × T] power spectrum into the [80 × T] log-Mel matrix the conv stem consumes. Three distinct numerical decisions live here, and all three match whisper.log_mel_spectrogram exactly: a Slaney-style triangular mel filterbank, a floored log10, and a global dynamic-range clamp before the final affine rescale. The fused entry point audio_whisper_log_mel_reference_f32 is just STFT + this kernel composed — the nightly gate checks the composition as well as the stages.

KERNEL 6 · MEL FILTERBANK + LOG COMPRESSION 201 Bins → 80 Mels → log10 → Clamp max−8 → (x+4)/4 MEL FILTERBANK [80 × 201] triangles, denser at low Hz — pitch resolution normalized by 2/(f_i+2 − f_i) (Slaney area) M = F · Pᵀ fmaf over 201 bins per (mel, frame) output is mel-major [80×T] already channel-major for the conv stem L0 = log10(max(M, 1e-10)) GLOBAL CLAMP |← max−8 →|← kept →| floor = global_max − 8 L = max(L0, floor) 80 dB dynamic range, one pass finds max RESCALE (L+4)/4 ≈ [−1, 1] [80 × 3000] for 30 s of audio The clamp uses the global max of the whole utterance — not per-frame — so quiet frames stay quiet relative to loud ones. Nightly tolerances vs PyTorch: log-Mel max ≤ 1.1e-3, RMSE ≤ 9e-5 (tones / noise / impulse + stage composition).

How The C Reads

  • The filterbank is constructed in planned memory by audio_whisper_mel_filters_slaney_f32 and passed in as a [n_mels × 201] matrix — this kernel owns the contraction, while the mel-filter op upstream owns the design.
  • Pass one computes log10(max(M, 1e-10)) per entry while tracking the global maximum; pass two applies the floor and the (x+4)/4 rescale in place.
  • The output layout is mel-major (log_mel[mel · T + frame]) — which is precisely the channel-major layout the Conv1D stem wants, so no copy sits between the two kernels.

Why The Clamp Is Global

A per-frame clamp would renormalize silence up to the same range as speech and the encoder would see phantom structure in quiet regions. Whisper's max − 8 floor (80 dB below the loudest frame of the utterance) preserves relative energy across time. It is one line of C — and one of the easiest places for a reimplementation to quietly diverge, which is why the impulse-signal oracle exists.


7. Conv1D Stem: The Only Learned Layers Before Attention

audio_conv1d_channel_major_f32 is the frontend's first weighted kernel — everything before it is fixed signal processing. The Whisper stem is two 3-tap convolutions with GELU between them: 80 → 384 at stride 1, then 384 → 384 at stride 2, which halves the time axis from 3000 log-Mel frames to the 1500 audio tokens the encoder (and the decoder's cross-attention) is built around. The kernel is deliberately generic — channels, kernel size, stride, and padding are parameters — because the same contraction serves any 1D conv frontend.

KERNEL 7 · CHANNEL-MAJOR 1D CONVOLUTION Two 3-Tap Convs; Stride 2 Halves Time INPUT [C_in × T] k=3 window rows = channels — contiguous per row, so a tap read is one cache-friendly walk CONV 1 80 → 384 · k=3 s=1 p=1 + bias, fmaf inner loop + GELU (separate kernel) [384 × 3000] CONV 2 384 → 384 · k=3 s=2 p=1 every other time step kept + GELU (separate kernel) 3000 → 1500 frames [384 × 1500] PER TAP y[o,t] = b[o] + Σ_i Σ_k w·x out-of-range taps skipped = zero-pad (T+2p−k)/s+1 checked GELU IS A NEIGHBOR, NOT A FUSION The erf form (PyTorch approximate='none') runs as its own kernel between the convs — the generated circuit stitches conv → gelu → conv → gelu, so each stage keeps its own oracle: Whisper stem-1 and stem-2 PyTorch Conv1D gates in unittest/test_audio_encoder.py.

How The C Reads

  • Output-frame count is validated up front: (T + 2p − k)/s + 1 — a mismatched caller gets -3, not a buffer overrun.
  • Zero padding is a bounds check (in_frame >= 0 && in_frame < T), not a padded buffer — no scratch allocation, no memset.
  • The accumulation starts from bias[o] and runs fmaf over (in_channel, tap) — deterministic FP32, one rounding per multiply-add.
  • Channel-major layout means each input row is contiguous, so the inner loop streams memory instead of striding across planes.

Why Stride 2 Is The Whole Point

Attention cost grows quadratically with sequence length. Halving the time axis before the transformer body is what makes 30 seconds of audio a 1500-token problem instead of a 3000-token one — a 4× attention saving, bought with a single 3-tap convolution. It is also the origin of the number 1500 that shows up everywhere downstream: encoder rows, cross-attention K/V rows, the Q=1, K=1500 decode oracle.


8. Token Transpose: The Named Layout Seam

audio_transpose_channel_to_token_f32 is eight lines of C and one of the most important kernels in the frontend. Everything upstream is channel-major ([C × T]) because convolutions want contiguous channels; everything downstream is token-major ([T × C]) because attention wants one row per sequence element. This kernel is the single explicit point where the layout flips — after it, the learned positional embedding is added and the matrix is literally the 1500-token sequence the encoder reads.

KERNEL 8 · LAYOUT TRANSFORM Column t Becomes Row t — One Token, 384 Features CHANNEL-MAJOR [384 × 1500] time slice t channel 0 → out[t·C+c] = in[c·T+t] TOKEN-MAJOR [1500 × 384] row t = token t (384 contiguous features) THEN, DOWNSTREAM + learned positional embedding (1D, time) → encoder self-attn rows → H_enc [1500 × 384] → decoder cross-attn K/V Same contract as the vision lane's layout ops: one named kernel at the seam, so no implicit reshape can drift between frontend and transformer body.

How The C Reads

  • Two loops, one assignment: out[t·C + c] = in[c·T + t] — the reference is deliberately naive so the layout contract is unmistakable.
  • It is the only place the audio pipeline reorders memory; an optimized tile/blocked variant can replace it later as long as the contract (and the parity gate) holds.
  • The positional embedding kernel operates token-major immediately after — the same position_embeddings_add_* family the vision encoder uses, reused, not duplicated.

Why A Kernel, Not A Comment

Layout bugs are the silent killer of bring-up: the model runs, the shapes match, and the output is plausible-but-wrong. Making the transpose a named kernel means the circuit JSON can declare logical_layout: token_major at its output checkpoint, the x-ray probe can compare that checkpoint against PyTorch directly, and any future fused frontend must still honor the same boundary.



9. Supporting Kernels: The Shared Glue

Three library kernels complete the audio path without being Whisper-specific. position_embeddings_add applies the learned 1D table after the token transpose — the same kernel the vision lane uses for its 2D table. gelu_pytorch_erf_f32_inplace is the exact erf GELU the stem contracts pin (note the naming trap: gelu_exact_inplace is historically the tanh approximation despite its name). And attention_forward_query_key_head_major_f32 serves both attention oracles — the batched causal self-attention transition and the Q=1, K=1500 cross-attention decode step — with head-major rows that keep the cached K/V slices contiguous.

The three shared kernels the Whisper path relies on: position_embeddings_add, gelu_pytorch_erf_f32_inplace, and attention_forward_query_key_head_major_f32

Why They Are Not Whisper Kernels

  • Each already has a numerical contract from the text or vision lanes — the audio path inherits parity rather than re-proving it.
  • This is the concrete form of "no Whisper model-name branch in codegen": the encoder is assembled from library pieces whose oracles predate Whisper.

Where They Are Pinned

unittest/test_audio_encoder.py exercises the erf GELU oracle (max diff ≤ 5e-7) and the head-major attention oracles — including the Q=1, K=1500 cross case — against PyTorch softmax(Q·Kᵗ/√d)·V. The positional add is pinned by the same suite's encoder checkpoint-by-checkpoint X-ray.

10. The Encoder Body: Four Bidirectional Blocks

Everything so far produced tokens. The encoder's job is to contextualize them: four identical pre-LayerNorm blocks turn the raw [1500 × 384] frame sequence into a memory where each frame's vector has absorbed meaning from the whole 30 seconds. The block contract is explicit in audio_transformer_encoder.json: layernorm → q/k/v_proj → attn → out_proj → residual → layernorm → mlp_up → gelu → mlp_down → residual, then one final LayerNorm.

The attention itself is where the audio encoder differs from the decoder you already know. Three facts, all declared in the circuit's attention_contract: it is dense bidirectional (causal: false) — the score grid is the full 1500×1500 square, not the decoder's causal triangle, so the frame under the word "country" borrows from frames before and after it. There is no RoPE — position comes from the absolute learned table added back at the transpose. And the K/V are ephemeral (ephemeral_full_context) — computed per layer, used once, freed; caching belongs to the decoder, not here. Per head (6 heads × 64 dims) the math is the usual softmax(Q·Kᵗ/√64)·V — softmax row-normalizes the square, so each frame's output is a weighted mixture of all 1500 value rows.

The Whisper Tiny audio encoder: 1500 tokens through four pre-LayerNorm blocks with dense bidirectional self-attention and a GELU MLP, producing the immutable encoder memory

What The Encoder Buys The Decoder

  • Context per frame: after 4 layers, row t is no longer "20 ms of spectrum" — it is that spectrum disambiguated by its acoustic surroundings, which is what makes cross-attention retrieval meaningful.
  • A stable retrieval target: because the memory is immutable for the segment, the decoder can project K/V from it once and cache them — the measured 79× decode win lives downstream of exactly this property.

Where It Is Pinned

The 79-edge PyTorch X-ray (compare_whisper_encoder_pytorch_v8.py) checkpoints the encoder block by block — stem, positional add, per-layer attention and MLP, final norm — with max abs ≈ 5.05e-4 and RMSE ≈ 6.7e-6 in FP32. The encoder is the dominant stage at ≈11.15 s of the ≈12.22 s end-to-end run, which is why it carries the deepest parity evidence on this page.

11. Transformer-XL Relative Attention: One Position Table, Two Score Kernels

The FastConformer encoders in Parakeet and Cohere do not use learned absolute positions or RoPE. Position enters only through the attention scores, Transformer-XL style: a sinusoidal table over relative offsets, two learned per-head bias vectors bias_u/bias_v, and a score that is the sum of a content term and a position term. CKE implements this as three providers. audio_relative_sinusoidal_position_f32 builds the [2T-1, E] table once per forward (descending offsets T-1 .. -(T-1), sin/cos interleaved). Each layer projects it with the existing GEMM provider into a head-interleaved relative_key. Then either the scores are computed fused — Parakeet's audio_conformer_relative_attention_f32 — or a full raw position-score matrix is materialized and re-indexed by audio_relative_shift_f32, which is the composition Cohere's encoder uses.

transformer-xl relative attention - one position table, two score kernels (worked shape: T=93, E=1024, 8 heads x 128) 1. position table - audio_relative_sinusoidal_position_f32 row r encodes offset p = T-1-r (descending +92 .. -92) inv = 1 / 10000^(c/E), c = even channel index out[r][c] = sin(p * inv) out[r][c+1] = cos(p * inv) output [2T-1, E] = [185, 1024] fp32 serial, no allocation; built once per encoder forward 2. per-layer projection - existing GEMM provider relative_key = positions . relative_k_proj^T (no bias) [185, 1024] - 8 heads x 128 dims, head-interleaved row T-1+k-q is the embedding of offset k-q each of the 24 layers has its own relative_k_proj no RoPE: position enters only through the scores 3. fused per-head scores - audio_conformer_relative_attention_f32 (Parakeet) Q, K, V [93, 1024] token-major, head-interleaved bias_u, bias_v [1024] one learned 128-vector per head relative_key [185, 1024] from stage 2 per head h, query q, key k: content = sum_d (q+u)[d] * k[d] position = sum_d (q+v)[d] * r[92+k-q][d] score = (content + position) * 128^-0.5 the Transformer-XL four-term expansion, pre-combined into two dot products softmax + value product row max tracked in score pass p = expf(s - max) / sum(p) out[q][d] = sum_k p[k]*V[k][d] output [93, 1024] scratch: one fp32 score row per head [8, 93], caller-owned - the kernel never touches the heap heads scheduled independently on ck_threadpool | full bidirectional 93 x 93 grid, no causal mask the standalone shift - audio_relative_shift_f32 (Cohere encoder) scores[h][q][k] = raw[h][q][T-1+k-q] - pure index copy [H,T,2T-1] -> [H,T,T] threadpool over H*T rows, zero arithmetic - exactness of the index map is the contract Cohere materializes the shifted matrix for its attn.pos_bias_u/v composition; Parakeet folds the same index math into stage 3 and never builds the raw matrix raw row (T=4), offsets k-q -3 -2 -1 0 +1 +2 +3 k=0 k=1 k=2 k=3 q=1: copy raw[2..5]
click / tap the diagram to expand

How The C Reads

  • The position kernel is serial and allocation-free: one powf per channel pair, one sinf/cosf per element, rows written position-major so the attention kernel's r[T-1+k-q] row is one contiguous 1024-float read.
  • The attention kernel partitions heads across the threadpool, then walks queries and keys; content and position dots accumulate in scalar FP32 over the 128-dim head slice, and the row max is tracked during the score pass so softmax is exactly two passes with no third pass.
  • The score scratch is [heads, frames] and caller-owned — one row per head, no heap allocation inside the kernel (a v8 runtime invariant).
  • The shift kernel does no arithmetic at all: output[key] = raw[origin+key] with origin = T-1-q, threadpool over H*T rows — its contract is the exactness of the index map, which the map states as raw[h,q,T-1+k-q] -> scores[h,q,k].

Why Two Score Kernels for One Idea

Cohere's pinned encoder composes Transformer-XL the textbook way — a full raw [H,T,2T-1] position-score product followed by a shift — so the shift is a named provider with its own oracle. Parakeet's pinned reference computes the same scores by direct indexing, so CKE fuses bias-u/v, the relative lookup, softmax, and the value product into one head-parallel provider and never materializes the shifted matrix. Both are pinned against PyTorch in unittest/test_audio_encoder.py (check_relative_sinusoidal_position, check_relative_shift, check_conformer_relative_attention), and the Parakeet end-to-end encoder RMSE is 9.08e-6 against a 2e-5 gate. Claim boundary: this is eager FP32 reference math — head-parallel, not flash — and the checkpoint's 5,000-position limit bounds the full-attention path.


12. The FastConformer Convolution Module: GLU, Depthwise Conv1D, Inference BatchNorm

Every FastConformer block carries a convolution module between the attention and the second Macaron FFN: pointwise Conv1D (1024→2048) → GLU → depthwise Conv1D (K=9) → BatchNorm → SiLU → pointwise Conv1D (1024→1024), wrapped in a residual add. Three of its stages are the new providers — audio_conv1d_channel_major_grouped_f32 (one kernel covering pointwise, grouped, and depthwise via the groups argument, with an optional bias), audio_glu_split_channel_major_f32, and audio_batch_norm_inference_channel_major_f32. All of them operate on the same channel-major [C, T] layout: one contiguous 93-float row per channel on the worked fixture (width 1024, 93 frames after subsampling).

fastconformer convolution module - token-major at the boundary, channel-major inside (worked shape: [1024, 93]) input [93, 1024] token-major -> transpose -> every stage below is [C, T] channel-major -> transpose back -> residual add (scale 1.0) pointwise_conv1 audio_conv1d_channel_major_grouped_f32 groups=1, K=1, no padding 1024 -> 2048 channels [2048, 93] GLU split audio_glu_split_channel_major_f32 value = ch 0..1023 | gate = ch 1024..2047 out = value * sigmoid(gate) [1024, 93] depthwise conv same provider, groups=1024 K=9, pad=4, stride=1 - T preserved weight [1024, 1, 9], bias NULL (Parakeet) [1024, 93] inference BatchNorm scale[c] = w[c] / sqrt(var[c] + 1e-5) offset[c] = b[c] - mean[c]*scale[c] y[c][t] = x[c][t]*scale[c] + offset[c] audio_batch_norm_inference_channel_major_f32 SiLU + pointwise_conv2 recurrent_silu_forward: x * sigmoid(x) then K=1 conv, 1024 -> 1024 (SiLU is elementwise - layout-free) [1024, 93] residual + transpose back audio_scaled_residual_add_f32, scale 1.0 branch joins the block residual stream (the 0.5 scale is only on FFN halves) [93, 1024] token-major how the depthwise conv reads out[c][t] = sum_j w[c][j] * x[c][t + j - 4] group = c_out / (C_out/G) - G = C is depthwise interior frames: AVX2 + FMA, 8 outputs per iteration edge frames: scalar fmaf, taps bounds-checked zero padding is index math - never materialized one provider: pointwise, grouped, depthwise why BatchNorm is inference-only running mean/var are frozen checkpoint tensors folds to a per-channel affine - one fma per element no batch statistics, no momentum, no counter updates the 24 BatchNorm training counters are excluded at conversion: 699 mapped + 24 excluded + 0 left invalid variance (nonfinite or var+eps <= 0) fails closed Evidence: conv1d (groups), GLU, and batch_norm oracles in unittest/test_audio_encoder.py (make test-audio); encoder RMSE 9.08e-6 vs 2e-5 gate. Caveat: shapes are the 7.435 s fixture (93 frames, width 1024); Cohere's 48-layer encoder runs the same module at width 1280 with biased convs.
click / tap the diagram to expand

How The C Reads

  • The grouped Conv1D derives the group from the output channel (group = c_out / (C_out/G)); a depthwise conv is just G = C, so one provider covers pointwise, grouped, and depthwise — validated against torch.nn.functional.conv1d's groups parameter.
  • Interior output frames run AVX2+FMA eight at a time (with a contiguous stride-2 load path and a gather fallback); the edge frames use the scalar fmaf loop with bounds-checked taps, so zero padding is index math and never materialized.
  • The GLU is layout-free elementwise over C*T: because the input is [2C, T] channel-major, value and gate are two contiguous C*T blocks — out[i] = value[i] * sigmoid(gate[i]), threadpool over elements.
  • BatchNorm folds to a per-channel affine: scale and offset are computed once per channel, then the frame loop is one multiply-add per element. Running statistics are read-only — the checkpoint's 24 training counters are excluded at conversion by contract, and a nonfinite or non-positive var+eps returns an error instead of producing NaNs.

Why Channel-Major Inside

Depthwise conv and BatchNorm both stream one channel across all frames; in channel-major that is one contiguous 93-float row — unit-stride loads for the AVX2 interior and trivial prefetch. Token-major would straddle 1024 floats per tap. The module pays two transposes at its boundary (host-side value.T copies in the runner) so every kernel inside keeps the layout it is vectorized for — the layout seam is explicit in the orchestration, not hidden behind a kernel flag. Parakeet's depthwise weights carry no bias, so the provider's bias pointer is optional; the same contract serves Cohere's biased convolutions. Pinned in unittest/test_audio_encoder.py (check_grouped_conv1d, check_split_glu, check_batch_norm_inference, check_conv_stride2_production_equivalence).


13. The LSTM Step: IFGO Gates in PyTorch Order, One Token at a Time

Parakeet's prediction network is a two-layer, 640-wide LSTM — the language-model half of the TDT transducer. audio_lstm_step_f32 advances it by exactly one token: one gate preactivation over the fused [4H, I]/[4H, H] weight matrices, one IFGO nonlinearity pass in PyTorch's gate order, and an in-place state update. The kernel is deliberately a step: the next input token depends on an argmax over the joint head, which depends on this LSTM's output, so there is no sequence to batch. State (h, c) is session-owned and persists across decode steps; the 4H gate buffer is caller-owned scratch shared by both layers; the kernel never touches the heap.

lstm step - ifgo gates in pytorch order, one token at a time (worked shape: I = H = 640, 4H = 2560) 1. gate preactivation gates[4H] = b_ih + b_hh + W_ih . x + W_hh . h W_ih [2560, 640], x [640] W_hh [2560, 640], h [640] one serial fp32 dot per gate row: input dims first, then hidden dims, in index order - deterministic accumulator seeded with b_ih + b_hh 2. ifgo nonlinearities (pytorch gate order) i = sigmoid(gates[0 : 640]) f = sigmoid(gates[640 : 1280]) g = tanh(gates[1280 : 1920]) o = sigmoid(gates[1920 : 2560]) 3. state update c' = f*c + i*g h' = o*tanh(c') output = h' h and c updated in place (old h feeds the gates; new h' is the output) sigmoid/tanh via expf/tanhf ownership and scheduling - the decode-loop contract session-owned state: h[2, 640] and c[2, 640], zero-initialized, persisting across decode steps caller-owned scratch: gates [4H = 2560] fp32, reused by both layers - the kernel never touches the heap layer 0 input: embedding row dec.embedding.weight[token] (8193 x 640 table; blank = id 8192) layer 1 input: layer 0 output h' - then the 640 -> 640 decoder projector (GEMM + bias) stepped only after a NONBLANK token (or on step 1); after a blank the cached projection is reused two layers per decoder_step call; state slices are per-layer, so sessions never share mutable state Evidence: check_lstm_step oracle vs torch.nn.LSTM single-step FP32 CPU; 50/50 tokens and 50/50 durations exact on the pinned fixture (PR #504). Caveat: single-step eager FP32 by design; sequence-fused or quantized LSTM variants are out of scope for this provider.
click / tap the diagram to expand

How The C Reads

  • The gate preactivation is 4H independent serial dot products — input dims first, then hidden dims, single FP32 accumulator seeded with b_ih + b_hh — so the result is deterministic regardless of thread count (the map declares the kernel serial).
  • The nonlinearity pass walks IFGO slices of the same scratch buffer — PyTorch's gate order, which is exactly what the checkpoint's fused weight_ih_l* tensors assume. Getting the order wrong still produces finite numbers, just the wrong transcript; the oracle exists for that reason.
  • h and c are updated in place from their old values, and the runner passes distinct per-layer state slices — two decode sessions never share mutable state (guarded by tests/test_v8_parakeet_native.py).

Why a Step Kernel, Not a Sequence Kernel

The transducer's next input token depends on an argmax over the joint head, which depends on this LSTM's output — a classic recurrent decode dependency with nothing to batch. A single-step provider with session-owned state is the honest shape: the prediction network runs only after nonblank emissions (44 times on the 50-step fixture trajectory), state lifetime is the decode session, and reset/reuse semantics live in the runner rather than in kernel flags. Evidence: check_lstm_step against torch.nn.LSTM single-step FP32 CPU in unittest/test_audio_encoder.py, and 50/50 token IDs plus 50/50 durations exact against the pinned Transformers reference end to end.


14. One Log-Mel Family, Three Contracts: Whisper vs Parakeet vs Cohere

Sections 4–6 walked the Whisper log-mel. Parakeet and Cohere reuse the same Conformer frontend provider family — pre-emphasis, centered STFT, Slaney mel, natural-log projection, per-feature normalization — but the contract differs at five points, and each difference changes the numbers the encoder was trained to expect. The table below is the exact divergence map, grounded in the two circuit contracts (parakeet_tdt.json, cohere_transcribe.json) and the two session runners that select the provider arguments.

one log-mel family, three contracts - what actually differs whisper (tiny / base) parakeet tdt cohere transcribe pre-emphasis none 0.97 FIR: y[t] = x[t] - 0.97*x[t-1] 0.97 FIR (same provider) STFT geometry n_fft 400, hop 160 n_fft 512, win 400, hop 160 n_fft 512, win 400, hop 160 window + padding periodic Hann reflect padding nonperiodic Hann zero padding checkpoint tensor fe.window zero padding mel bank 80 bins, Slaney computed at runtime 128 bins, Slaney computed, librosa-pinned 128 bins checkpoint tensor fe.mel_fb log rule log10(max(s, 1e-10)) ln(s + 2^-24) ln(s + 2^-24) final transform global clamp: floor at max-8, then (x + 4) / 4 per-feature norm, sample var (N-1), live frames only per-feature norm, all frames incl. padding, provider N-1 * output layout mel-major [80, 1500] then token transpose (sec. 8) time-major [744, 128] time-major [T, 128] * contract string says population_variance; the selected provider computes sample variance (N-1) - the behavior certified exact vs CrispASR shared conformer-family providers: audio_preemphasis_f32 | audio_stft_power_centered_window_f32 | audio_whisper_mel_filters_slaney_f32 | audio_log_mel_time_major_f32 | audio_feature_normalize_per_feature_f32 - whisper uses its own pair (sections 4 and 6) Evidence: whisper log-mel max <= 1.1e-3 (make test-audio); parakeet frontend RMSE 4.06e-4 vs 5e-4 gate; cohere 32/32 IDs exact vs CrispASR. Caveat: contracts are not interchangeable - log base, guard epsilon, normalization span, and layout each change what the encoder expects.
click / tap the diagram to expand

Why Seven Providers Cover All Three

The frontend family is dimension-general by design: audio_preemphasis_f32 takes the coefficient, audio_stft_power_centered_window_f32 takes n_fft/window/hop/centering, audio_whisper_mel_filters_slaney_f32 builds any bank size (its legacy name notwithstanding), audio_log_mel_time_major_f32 takes its epsilon as an argument, and audio_feature_normalize_per_feature_f32 takes the frame span. What differs per lane is contract, not code: which epsilon, which window source, which frames count as live. Whisper is the odd one out — log10, a global data-dependent clamp, no normalization — because its (x+4)/4 squash is the scaling contract the Whisper encoder was trained with.

Claim Boundary

Three distinct numerical contracts, three distinct oracles: Whisper against the torch.stft-style reference (log-Mel max ≤ 1.1e-3), Parakeet against the pinned Transformers feature extractor (frontend RMSE 4.06e-4, gate 5e-4), Cohere against CrispASR stage dumps (32/32 token IDs exact). Feeding one model's audio through another's contract is wrong by construction, not by tolerance. One wrinkle worth knowing: the cohere_transcribe circuit contract string says per_feature_population_variance_all_frames, but the selected provider computes a sample variance (N−1) over all frames — the certified behavior, pinned exact against CrispASR; the contract label and the implementation disagree, and the oracle pins the implementation.


Parity & Nightly Coverage

Where Every Kernel Above Is Pinned

Operator commands live in the v8 Runbook; the pipeline and evidence live in Whisper Tiny End-to-End; the kernel-level math is summarized in Concepts — Audio Frontend & Cross-Attention. For the same treatment of a recurrent kernel family, see the Gated DeltaNet Deep Dive.

The Parakeet TDT lane reuses this frontend family (pre-emphasis, centered STFT, Slaney log-mel, per-feature normalization, grouped Conv2D subsampling) and adds five FastConformer/transducer providers — grouped Conv1D, relative sinusoidal position, relative attention, inference BatchNorm, and a stateful LSTM step — whose PyTorch oracles run in the same unittest/test_audio_encoder.py suite. Sections 11–14 walk those kernels and the three-way log-mel contract split; see Parakeet TDT Native CPU Bring-up for the architecture, decode semantics, and runbook.

Image
100% | |
Scroll to zoom | Drag to pan | W/H to fit | 0 to reset | ESC to close