An earlier article explained how an audio transformer hears: waveform to frames, frames to log-Mel features, features to encoder states, states to decoded text. This article is the implementation sequel. It follows the exact Whisper Tiny circuit now running through C-Kernel-Engine: every tensor shape, kernel boundary, memory lifetime, parity gate, and generated-C stage. If you want the conceptual introduction — timestamps, hallucination, why audio is not a vision-language-model patch — start there. If you want to watch the computation execute, stay here.
C-Kernel-Engine (CKE) is my CPU-first model runtime: a model is described as an explicit circuit, the circuit resolves into kernel-map provider selections, lowering passes schedule it into call IR (intermediate representation), and the output is a generated C artifact with a fixed memory plan. The documentation site carries the full architecture. Until now, that pipeline ran language families (decoder-only and hybrid text models) and one vision family (Qwen3-VL, through an encoder-plus-prefix bridge). Whisper adds three things the pipeline had never carried at once: raw signal processing before the transformer, a bidirectional encoder, and a causal decoder bound to that encoder by cross-attention.
One clarification before the details: “v8” is the generation of CKE's execution pipeline — the circuit, kernel-map, lowering, and codegen stack — not a version of Whisper. The model is OpenAI's Whisper Tiny, unchanged. And “end to end” has a precise meaning here: PCM16 (pulse-code modulation, 16-bit) WAV bytes enter generated C, and transcribed tokens leave it. Python manages artifacts and execution; it does not implement the model arithmetic.
WAV bytes
→ normalized samples
→ 80 × 3000 log-Mel features
→ 1500 × 384 encoder memory
→ decoder cross-attention
→ output tokens
→ textWhisper Tiny, one kernel boundary at a time
This is the complete circuit in execution order. Each stage names its kernel, its shapes, and the lifetime of the memory it touches.
1. WAV decode
The artifact receives raw WAV bytes. The decode kernel walks the PCM16 payload and converts each 16-bit integer to a normalized FP32 sample, scaled by 1/32768. The result is a one-dimensional f32 signal of n samples. It is not yet an embedding, not yet a token tensor — it is still just sound, represented as numbers the rest of the circuit can contract over.
2. Resampling
Whisper expects 16 kHz (kilohertz) mono audio. The circuit makes the two paths explicit: audio already at 16 kHz passes through untouched; anything else runs a windowed-sinc resampling kernel. Resampling is a separate numerical contract rather than something hidden inside file loading, because interpolation is arithmetic — it has error behavior, and CKE's rule is that anything with error behavior gets a kernel boundary and a parity test.
3. Pad or truncate
Whisper Tiny was trained on 30-second windows, so the circuit pads or truncates every input to exactly 480,000 samples. Longer recordings are currently truncated rather than chunked — a stated limitation, not a silent one.
4. STFT tables and STFT power
The short-time Fourier transform (STFT) stage is split into two circuit operations: table construction (precomputing the Hann window and the discrete Fourier transform basis for a 400-point fast Fourier transform) and execution. With a 400-sample window and a hop of 160 samples, 480,000 samples become 3,000 frames; the 400-point FFT yields 201 frequency bins. The output is a 201 × 3000 power spectrogram. Table construction and execution are separate ops because they have different memory lifetimes — tables are built once in planned memory and reused by every frame.
5. Mel filter construction
The 80-band Slaney mel filterbank is not a learned tensor and does not ship with the model weights. CKE builds it in planned memory at runtime, from the same Slaney equations the reference implementations use. This is a deliberate architectural choice: a deterministic transform belongs in the circuit, not in the artifact.
6. Log-Mel
The power spectrogram passes through the mel filters, then a log10 compression with Whisper's exact normalization: find the global maximum, clamp every value to at least max − 8, and rescale. The result is the 80 × 3000 log-Mel tensor — the only representation of the audio the transformer ever sees.
7. Conv1D stem
Two one-dimensional convolutions with GELU (Gaussian error linear unit) activations turn spectral channels into model width and halve the time axis:
80 × 3000
→ Conv1D k=3, stride=1 → GELU
→ Conv1D k=3, stride=2 → GELU
→ 384 × 1500Stride two in the second convolution is exactly what halves 3,000 frames into 1,500 positions.
8. Token transpose and position embeddings
The conv stem produces channel-major 384 × 1500; attention wants token-major 1500 × 384. The transpose is an explicit layout seam in the circuit, not an implicit view — memory layout is part of the contract. Learned absolute position embeddings are then added to every token. Whisper predates the rotary fashion: there is no RoPE (rotary position embedding) anywhere in this model.
9. Encoder block, ×4
The encoder is four identical pre-LayerNorm blocks. LayerNorm here is the per-token normalization that keeps activations numerically well-behaved; “pre” means it runs before each sub-layer rather than after. One block expands to:
LayerNorm
→ Q/K/V projections
→ six attention heads
→ dense bidirectional attention
→ output projection → residual
LayerNorm
→ MLP up → exact erf GELU → MLP down
→ residualThe dimensions: 1,500 tokens, model width 384, six heads of 64 dimensions each. The attention score tensor is 6 × 1500 × 1500, and each head computes the familiar scaled dot-product attention:
\[\mathrm{softmax}\!\left(\frac{QK^{\top}}{\sqrt{64}}\right)V\]
The GELU is the exact erf (error function) variant, matching the PyTorch reference bit-for-bit behavior rather than the faster tanh approximation — a numerical-contract decision, not a performance one.
The property that matters most is what is missing: there is no causal mask. Every audio position attends to every other audio position, past and future alike. That is what “dense bidirectional” means, and it is the structural difference between this encoder and every decoder-only family v8 has hardened — those constrain each token to its own past.
10. Encoder memory
After four blocks, the encoder has converted 30 seconds of sound into a fixed 1500 × 384 representation. Two properties of this memory matter for everything downstream: it is not text — it is the audio, digested — and it does not grow during decoding. It is computed once and consulted on every generated token.
11. Decoder self-attention
The decoder is a causal language model over Whisper's token vocabulary. It generates one token at a time, and its self-attention uses a persistent KV (key/value) cache that grows by one row per generated token — the same contract v8 already uses for text models.
12. Decoder cross-attention
Cross-attention is the piece audio adds. For each output token, the decoder produces a single query row (Q = 1) and attends across all K = 1500 encoder positions. This creates two memory lifetimes in one model:
- Decoder self-attention KV grows with generated text — one new row per token.
- Encoder cross-attention K/V is fixed for the entire transcription — the encoder output cannot change while decoding proceeds.
The original implementation ignored that second fact and re-projected the encoder's keys and values on every decode step — the same projections, recomputed per token, per layer. The corrected circuit projects them once during prefill, stores them in a persistent layer-major cache, and lets every decode step read the cache. That single observation is the 79× result, and the performance section below picks up exactly there.
The whole circuit in one table
| Stage | Input shape | Output shape | Kernel or circuit | Memory lifetime | Parity |
|---|---|---|---|---|---|
| WAV decode | PCM16 bytes | [n] f32 | audio_wav_decode | per run | PyTorch |
| Resample | [n] f32 | [m] f32 @ 16 kHz | audio_resample (sinc) | per run | PyTorch |
| Pad / truncate | [m] f32 | [480000] f32 | audio_pad_or_truncate | per run | shape |
| STFT tables | — | window + DFT basis | audio_stft_tables | planned, reused | NumPy |
| STFT power | [480000] f32 | [201 × 3000] | audio_stft (FFT400) | per run | PyTorch |
| Mel filters | — | [80 × 201] | audio_mel_filters (Slaney) | planned, reused | ref |
| Log-Mel | [201 × 3000] | [80 × 3000] | audio_log_mel | per run | nightly |
| Conv stem | [80 × 3000] | [384 × 1500] | conv1d ×2 + GELU | per run | kernels |
| Transpose + positions | [384 × 1500] | [1500 × 384] | layout + embedding add | per run | layout |
| Encoder ×4 | [1500 × 384] | [1500 × 384] | LN, proj, attn, MLP | per run | X-ray |
| Decoder self-attn | tokens so far | [t × 384] | causal attn + KV cache | grows per token | kernels |
| Decoder cross-attn | Q [1 × 384], enc [1500 × 384] | [1 × 384] | cross-attn + persistent cache | fixed after prefill | kernels |
| LM head | [1 × 384] | token id | projection + argmax | per token | JFK |
Every row has its own walkthrough — layouts, contracts, and why the kernel exists — in the audio kernels deep dive.
Shared kernels versus Whisper-specific structure
CKE did not grow twenty new “Whisper kernels” for this milestone. The work divides into four buckets, and the sizes of the buckets are the point:
- Genuinely new signal-processing kernels: WAV decode, windowed-sinc resampling, pad/truncate, STFT tables and execution, Slaney mel construction, log-Mel. Audio needed these; nothing else in the tree did.
- Shared transformer kernels, reused untouched: LayerNorm, GEMM (general matrix multiply) projections, residual addition, softmax, GELU, the MLP shape, KV-cache machinery. Whisper's transformer arithmetic is the same arithmetic every other family runs.
- Whisper-specific structure, no new math: the tensor dimensions, the channel-to-token ordering, the two memory lifetimes, and the numerical contracts that pin them down.
- The circuit itself: the template that stitches reusable kernels into Whisper Tiny — seven frontend ops lowered into one generated entry point,
ck_model_run_audio_wav.
This is the CKE thesis working as designed: a new model family should primarily introduce a new circuit and only the genuinely missing mathematical kernels — not another standalone runtime.
Performance and the theory of constraints
The 79× number, restated with its bookkeeping: decoder cross-attention fell from 16.57 seconds to 0.21 seconds when the repeated encoder K/V projections became a prefill-once persistent cache. Total transcription time for the JFK sample fell from 28.64 to 12.22 seconds, with byte-identical 23 tokens across three repeated runs.
But eliminating one repeated computation only ever exposes the next one. Of the remaining 12.22 seconds, the encoder now consumes roughly 11.15 — dense bidirectional attention over 6 × 1500 × 1500 score tensors, four blocks deep, is the dominant cost of transcribing 30 seconds of audio on a CPU. That is the next optimization target: the encoder's GEMMs and attention, not the decoder. Whisper Tiny on v8 is correct and dramatically less wasteful than it was; it is not yet fully optimized, and the profile now says exactly where the work is.
Evidence
Every claim above is tied to a test that can falsify it:
- The log-Mel frontend runs nightly against a PyTorch reference.
- Kernel-level tests cover Conv1D, exact-erf GELU, layout transforms, self-attention, persistent-KV behavior, and cross-attention.
- The 79-edge encoder X-ray compares the generated encoder against the reference computation edge by edge: for the documented FP32 run, a maximum absolute difference of approximately 5.05e-4 and an RMSE (root-mean-square error) of 6.7e-6.
- The JFK sample transcribes to exactly the same 23 tokens across three repeated runs.
- The full Hugging Face transcription comparison remains an opt-in artifact test, because CI runners do not carry the generated Whisper artifacts.
And the honest contract, unchanged: Whisper Tiny only. FP32 only. English transcription without timestamps. PCM16 mono WAV at 16 kHz — ffmpeg -i your-audio.mp3 -ac 1 -ar 16000 -c:a pcm_s16le out.wav converts anything else. Maximum 30 seconds, truncated rather than chunked. The JFK sample is corpus-exact; arbitrary recordings are functional, not yet corpus-certified. CKE has not “solved audio” — it has carried one audio model through the same gates every other modality passes.
Trying it is two commands — the v8 runbook carries the canonical audio section:
version/v8/scripts/cks-v8-run audio \
--encoder-run-dir /path/to/whisper-tiny-encoder \
--decoder-run-dir /path/to/whisper-tiny-decoder \
--wav /tmp/test-audio.wavWhy this milestone matters
Audio was a v10 roadmap item because I expected the encoder–decoder shape and the signal-processing frontend to fight the v8 pipeline. The opposite happened: the circuit abstraction already knew how to describe the graph, the kernel maps already knew how to select providers, and lowering already knew how to schedule it. The work was writing the audio kernels and their contracts, not reinventing plumbing.
Whisper Tiny is evidence that CKE's circuit, kernel-map, lowering, memory-planning, generated-C, and parity architecture can carry an encoder–decoder model with a signal-processing frontend. The achievement is not merely that Whisper runs. It is that audio joined text and vision without requiring a separate execution engine.
Text was the first sense, vision the second. Audio is the third — and it arrived not with a new engine, but with the same engine proving it was never modality-specific in the first place.
The code is at github.com/C-Kernel-Engine, the documentation at c-kernel-engine.github.io/C-Kernel-Engine, and the contributor path describes how to reproduce, bound, validate, and submit evidence. The engineering Discord is open at discord.gg/J7wNx8xS — bring questions, skepticism, or a model family you want to see run on CPUs.
Related notes and further reading
- How Audio Transformers Work — the conceptual prequel: the encoder path, Whisper timestamps, and why audio is not a VLM patch.
- What Is the C Kernel Engine — the map from the ideas to the code.
- v8 IR Pipeline Codegen — the lowering path the audio circuit travels.
- Templates Are Circuit Maps — how a model family is described so a new modality is new kernels, not a new engine.
- What Numerical Parity Actually Requires — the X-ray method behind the 79-edge comparison.
- CKE documentation: the v8 Whisper Tiny end-to-end page, the audio kernels deep dive, the v8 runbook, and the model–kernel matrix. Runtime work landed in PRs #240 and #246; documentation in PR #244.