Skip to content

Commit a524c12

Browse files
committed
M7: Whisper full-checkpoint conformance (openai/whisper-tiny)
Full-size Whisper-tiny forward pass on Emily.Backend, pinned against a deterministic synthetic 30 s mel window (sin(iota({1, 3000, 80}) * 0.01)) and a short special-token decoder prompt. Asserts the leading 3×3 logits slice at 1e-3 tolerance plus the decoder-last-step argmax (== 50257, <|endoftext|>, which is what the synthetic input collapses to — that's fine for a pin; the assertion is "same backend + same weights + same input reproduces the same token", not "the model says something interesting"). Runtime is ~7 minutes on Apple Silicon because Emily.Backend.conv still routes through Nx.BinaryBackend (deferred to M8). Whisper encoder has two 1-D convs (kernel=3, out=384) over 3000 time-steps, ~940M multiply-adds executed as BEAM loops. The full test is :whisper_full tagged, excluded from --only conformance, and opt-in only; M8 wiring Backend.conv to Native.conv_general will bring this to sub-second. Also picks up formatter fixups on lib/emily/backend.ex (the via_binary assignment from the previous commit) and trailing blank lines on distilbert_test.exs / qwen3_test.exs.
1 parent e891d15 commit a524c12

5 files changed

Lines changed: 96 additions & 6 deletions

File tree

lib/emily/backend.ex

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -966,9 +966,10 @@ defmodule Emily.Backend do
966966
# `Emily.Backend` during conformance tests — and the resulting
967967
# mixed-backend operand list crashes inside BinaryBackend's op.
968968
defp via_binary(%T{} = out, tensors, fun) when is_list(tensors) do
969-
result = Nx.with_default_backend(Nx.BinaryBackend, fn ->
970-
tensors |> transfer_all() |> then(&apply(fun, &1))
971-
end)
969+
result =
970+
Nx.with_default_backend(Nx.BinaryBackend, fn ->
971+
tensors |> transfer_all() |> then(&apply(fun, &1))
972+
end)
972973

973974
from_binary(out, Nx.to_binary(result), [])
974975
end

test/emily/conformance/distilbert_test.exs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,5 +203,4 @@ defmodule Emily.Conformance.DistilbertTest do
203203
end
204204
end
205205
end
206-
207206
end

test/emily/conformance/qwen3_test.exs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,5 +180,4 @@ defmodule Emily.Conformance.Qwen3Test do
180180
Enum.take(tokens, len_val)
181181
end
182182
end
183-
184183
end
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
defmodule Emily.Conformance.WhisperFullTest do
2+
@moduledoc """
3+
Full `openai/whisper-tiny` end-to-end conformance test.
4+
5+
Like the other `*_full` suites, this is excluded even from
6+
`mix test --only conformance`: the model is ~150 MB on first
7+
fetch. Run explicitly:
8+
9+
mix test --only whisper_full
10+
11+
Uses a deterministic synthetic mel-features tensor rather than a
12+
checked-in audio fixture, by the same reasoning as
13+
`vit_full_test.exs`: binary test assets in git are annoying, and
14+
the intent is to catch numerical drift on real-size weight
15+
tensors (encoder conv frontend + 4 encoder blocks + 4 decoder
16+
blocks with cross-attention), not to verify mel-spectrogram
17+
computation. Input shape is Whisper's canonical 30-second window:
18+
3000 time-steps × 80 mel bins.
19+
20+
A failure means the backend has drifted, Bumblebee's Whisper port
21+
has changed, or the HF checkpoint has been republished — all of
22+
which are real signals.
23+
"""
24+
25+
use ExUnit.Case, async: false
26+
use Emily.ConformanceHelper
27+
28+
@moduletag :whisper_full
29+
@moduletag capture_log: true
30+
@moduletag timeout: 600_000
31+
32+
test "openai/whisper-tiny forward pass matches pinned logits slice" do
33+
{:ok, %{model: model, params: params, spec: spec}} =
34+
Bumblebee.load_model({:hf, "openai/whisper-tiny"})
35+
36+
assert %Bumblebee.Audio.Whisper{architecture: :for_conditional_generation} = spec
37+
38+
# Synthetic 30 s mel window. Nx.iota + sin produces a
39+
# fully-deterministic, feature-rich signal — more useful than
40+
# a constant because it exercises the attention pattern rather
41+
# than landing on a degenerate uniform hidden state.
42+
input_features =
43+
Nx.sin(Nx.iota({1, 3000, 80}, type: :f32) |> Nx.multiply(0.01))
44+
45+
# Short decoder prompt: the four Whisper special tokens that open
46+
# every English-language transcription (<|startoftranscript|>,
47+
# <|en|>, <|transcribe|>, <|notimestamps|>) plus two text-token
48+
# placeholders. Exact ids don't matter for a numerical pin —
49+
# they just need to be in-vocab and deterministic.
50+
decoder_input_ids = Nx.tensor([[50_258, 50_259, 50_359, 50_363, 50, 100]])
51+
decoder_attention_mask = Nx.tensor([[1, 1, 1, 1, 1, 1]])
52+
53+
inputs = %{
54+
"input_features" => input_features,
55+
"decoder_input_ids" => decoder_input_ids,
56+
"decoder_attention_mask" => decoder_attention_mask
57+
}
58+
59+
outputs = Axon.predict(model, params, inputs)
60+
61+
assert Nx.shape(outputs.logits) == {1, 6, 51_865}
62+
63+
argmax =
64+
outputs.logits[[.., -1, ..]]
65+
|> Nx.argmax(axis: -1)
66+
|> Nx.backend_transfer(Nx.BinaryBackend)
67+
|> Nx.to_flat_list()
68+
|> hd()
69+
70+
# On the synthetic mel input the decoder collapses to
71+
# <|endoftext|> (50257) by the last position. That's fine for a
72+
# pin — the assertion is "the same backend + the same weights +
73+
# the same input reproduce the same token", not "the model says
74+
# something interesting".
75+
assert argmax == 50_257
76+
77+
assert_all_close(
78+
outputs.logits[[.., 0..2, 0..2]],
79+
Nx.tensor([
80+
[[2.9246, 0.2663, 3.8530], [-4.5523, -8.4833, -4.4232], [17.7350, 16.3070, 13.2149]]
81+
]),
82+
# Whisper-tiny under BinaryBackend fallback for conv produces
83+
# slightly different rounding than a pure Emily path would, so
84+
# loosen the numerical tolerance marginally relative to the
85+
# tiny-random suite. Still three orders of magnitude inside
86+
# f16/bf16 accumulation drift territory.
87+
atol: 1.0e-3,
88+
rtol: 1.0e-3
89+
)
90+
end
91+
end

test/emily/conformance/whisper_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ defmodule Emily.Conformance.WhisperTest do
7171

7272
outputs = Axon.predict(model, params, inputs)
7373

74-
assert Nx.shape(outputs.logits) == {1, 8, 50257}
74+
assert Nx.shape(outputs.logits) == {1, 8, 50_257}
7575

7676
assert_all_close(
7777
outputs.logits[[.., 1..3, 1..3]],

0 commit comments

Comments
 (0)