Skip to content

ci: add ASR test workflow for Whisper transcription - #799

Merged
tawei-amd merged 6 commits into
mainfrom
ci/asr-test
Oct 16, 2026
Merged

tawei-amd merged 6 commits into
mainfrom
ci/asr-test

Conversation

@tawei-amd

@tawei-amd tawei-amd commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Adds an ASR Test workflow that measures speech recognition on real NPU hardware whenever ASR-related code changes. It also gives PRs #510, #794 and #798 a way to be validated on hardware. CI currently has no hardware coverage for ASR, because the model sweep's audio task is disabled.

What it measures (.github/script/asr_test.py):

  • Accuracy: WER, or CER for ja and zh, on 84 FLEURS files with human transcripts. Each set and the pooled total are reported, with each file's errors capped at its reference length so that one runaway transcript can't swamp a set.
  • Runaways: transcripts longer than 1.5× the reference, which is what a decoder stuck in a repetition loop produces.
  • Speed: seconds of processing per second of audio.
  • Formats: json, no response_format, and verbose_json must agree.
  • Chat transcription: WER of gemma4-it:e2b (whose audio front end E4B and the flash variants share) and gemma4-it:12b (which has its own, modeling_gemma4_12b_audio.cpp).

Feature groups. These only fail the run when required; otherwise they are reported as warnings:

Group What it checks
verbose_json Segments are ordered, inside the clip, non-empty, and join into the text; duration equals the clip length
timestamp_rules Segments never overlap; the first starts within 1 s
window_tail No phrases repeated across 30 s windows
language Forcing the language doesn't hurt accuracy; it is reported back; unknown codes are rejected
prompt A prompt doesn't hurt accuracy, and names in it are spelled as given
quiet_log No transcripts in the server log

Only prompt is required by default, because it is the only group main passes on every platform. main's Linux HRX build loops on the long-form file, so it fails window_tail; a PR that adds repeats is still caught by the baseline comparison. A PR that makes main pass another group adds it to ASR_REQUIRE in the workflow.

Comparison. On a pull request, main's latest build runs first on the same runner and becomes the baseline. The run fails if:

  • the pooled error rate rises by more than 1 point;
  • any set rises by more than 5 points (sets have 10 files, so one flipped decode moves them a point or more);
  • or the number of runaways grows.

The report also shows how many files changed and the drift from main's transcripts. Pushes to main and the weekly run compare against asr_eval/expected.json instead, with 2 points of slack on the pooled rate (versus 1 point against main) and the same 5 points per set. Its rates are kept per platform, because identical sources decode differently on each: main scores 18.55% pooled on Linux, 22.63% on Linux HRX, 18.61% on Windows and 19.44% on Windows HRX, and some sets differ by up to 5 points. A manual dispatch can test any PR's builds (pr) and set require and max_drift.

Evaluation set (.github/script/assets/asr_eval/, 6.8 MB, CC BY 4.0, attributed in its README):

Set Files Audio
en-short 20 single window
en-long 3 ~83 s each, two window boundaries
en-longform 1 308 s
de, es, fr, ja, zh, ar 10 each

build_asr_eval.py rebuilds the set deterministically from the FLEURS Parquet files.

Triggers: pull requests and pushes to main that touch any of the following, plus a weekly run and manual dispatch:

  • the Whisper engine, its NPU kernels and prebuilt libraries;
  • FFmpeg, FFT (fftw, pocketfft) and audio_process_utils;
  • the chat-model audio front ends;
  • rest_handler.cpp, server.cpp and multipart.*;
  • the test's own files.

Linux and Windows are both tested, default and HRX builds alike.

Report. A summary job writes one report to the run's summary page. It starts with a verdict per model and platform. Then each model gets its own section with Linux, Linux HRX, Windows and Windows HRX side by side:

  • whisper-v3:turbo: accuracy per set against main; runaways, changed files and speed; checks by group; failures and warnings per platform.
  • each chat model: WER against main, plus its own failures.

Each job's own report, results JSON and raw responses are in its artifact. The script also refuses to start if its port is already taken, so it can't end up testing another server by mistake.

Validation of #510, #794 and #798 on hardware

I ran each PR's CI-built Linux portable against main (77bc6bdd) on a Strix NPU. Each run required the default groups plus the ones that PR adds.

main #510 #794 #798
Pooled error rate 18.55% 18.55% 7.14% 18.82% (+0.27)
en-short / en-long / longform WER 9.71 / 11.89 / 16.53 same 4.74 / 4.87 / 6.48 9.71 / 11.89 / 15.24
de / es / fr WER 38.1 / 15.4 / 27.7 same 7.0 / 2.9 / 7.0 38.6 / 15.4 / 29.3
ja / zh CER, ar WER 19.4 / 13.5 / 41.7 same 6.8 / 12.4 / 17.7 20.0 / 15.4 / 42.8
Runaways 3 3 0 3
Files changed vs main – 0 50/84 18/84
Gemma chat WER 5.05% 5.05% 5.05% 5.05%
Result pass fail: verbose_json fail: prompt pass

#510 (verbose_json). On main's decoder, segments are invalid in 28 of 84 files, 5 of them English short clips:

  • they end past the clip (for example 0.00–24.74 s on an 8.6 s clip) in 24 files;
  • they are zero-length or empty in 8 files;
  • they go backwards in 4 files.

Most of this comes from main's decoder, which #794 fixes. #510 could still clamp segment ends to the duration and drop empty segments.

#794 (decode loop). It reproduces the claimed gains on every set, removes all runaway loops, rejects unknown language codes, and stops logging transcripts. However, prompt looks broken. When a prompt word is spoken in the clip, the transcript drops everything up to it:

  • prompt="China's" returns " economic output." instead of the whole sentence;
  • the WER of the prompted clips goes from 6.1% to 41.5%.

When the prompt words aren't spoken before the end, it works as intended: "Fichte" is then spelled correctly. The timestamp rules can't be observed without verbose_json (#510).

#798 (pocketfft). No regression: pooled +0.27 points, no new runaways, and Gemma is unchanged. 18 files changed, nearly all by one word or character; one change is a fix (漏れ効いた → 漏れ聞いた). That's the expected effect of slightly different FFT rounding tipping a borderline greedy decode. en-short is byte-identical.

Test plan

  • main and the three PR builds run through asr_test.py on a Strix NPU (results above)
  • Chat transcription is repeatable at temperature 0 (two runs byte-identical)
  • Baseline lookup tested against a fake gh: pull request with an expired main run and a missing HRX baseline, dispatch with a PR number, and push without a baseline
  • actionlint is clean apart from the custom runner labels
  • Windows and Windows HRX jobs on CI (run 38853168399): main then this PR's build, about 18 min per job; 42/42 checks pass, 0 files changed (this PR changes no engine code)
  • main's Linux HRX build measured locally for expected.json: it loops on the long-form file ("the state of the state of …"), 36.8% WER against 16.5% on Linux
  • Linux and Linux HRX jobs on CI (run 38865384844): 42/42 and 41/41 checks pass, matching the local runs
  • Combined report rendered from the four CI results, plus a failing example (fix(whisper): make the ASR decode loop behave like Whisper #794's prompt checks)
  • gemma4-it:12b: first tested by this PR's next CI run. It could not run locally, because the test NPU was busy with another CI job.

🤖 Generated with Claude Code

tawei-amd and others added 6 commits October 15, 2026 20:16
Runs on real NPU hardware whenever ASR code changes: the Whisper engine
and its prebuilt NPU libraries, the audio front end and FFT, and the
server files that host /v1/audio/transcriptions. Like the model sweep,
it waits for the commit's Linux portable builds (default and HRX) and
tests those instead of rebuilding. Linux only for now.

asr_test.py goes beyond sweep_audio.py's non-empty check:
- json: word error rate against a reference transcript of the bundled
  110 s clip, which spans four 30 s Whisper windows
- default response_format: same {model, text} as json
- verbose_json: same text as json, duration and language of the clip,
  and segments that are ordered, non-empty, inside the clip and join
  back into the text

Builds without verbose_json support report those checks as skipped
with a warning; --require-verbose-json makes them fail instead.

Co-Authored-By: Claude <[email protected]>
Waits for the commit's "Build Windows Packages" run as well and runs
asr_test.py on the Windows default and HRX portables, on the
[self-hosted, windows, x64, npu, sweep] runners. Adds src/lib/libfftw3*
(Windows-only FFT DLLs) to the trigger paths.

Co-Authored-By: Claude <[email protected]>
- Trigger on Whisper's NPU kernels (src/xclbins/Whisper-V3-Turbo-NPU2),
  the multipart upload parser, and the FFmpeg DLLs Whisper decodes
  audio with.
- Run weekly against main's latest build, to catch what no commit
  changes: republished Whisper weights and runner driver or firmware
  updates. The event name joins the concurrency group so the weekly
  run and a push to main do not cancel each other.
- Save every response body to the results artifact before parsing it,
  and fail with "response is JSON" instead of a parser error when the
  body is not JSON (the server answers with an empty body when ASR is
  not loaded).

Co-Authored-By: Claude <[email protected]>
The test now measures what PRs #510, #794 and #798 need validated:

- assets/asr_eval: 84 FLEURS test files (24.5 min, 6.8 MB) with human
  transcripts. English clips inside one window, ~83 s and ~308 s files
  that cross window boundaries, and 10 clips each of de, es, fr, ja, zh
  and ar. build_asr_eval.py rebuilds it; README.md carries the CC BY 4.0
  attribution. Replaces the model-made test_audio reference.
- asr_test.py: WER/CER per set and pooled (each file capped at its
  reference length), runaway transcripts, speed, format agreement,
  Gemma 4 chat transcription, and feature groups (verbose_json,
  timestamp_rules, window_tail, language, prompt, quiet_log) that fail
  the run only when required. --baseline compares with another build's
  results, --expected with fixed rates.
- asr-test.yml: on a pull request, main's latest build runs first on
  the same runner as the baseline. Pushes and the weekly run compare
  with expected.json. A dispatch can test any PR's builds and set the
  required groups and drift limit. window_tail and prompt are required
  by default, since main passes them.

Co-Authored-By: Claude <[email protected]>
…ault

The first CI run measured main on Windows and Windows HRX: identical
sources decode differently per platform, by up to 5 points on a set
(de-fleurs 43.3% on Windows, 38.1% on Linux). One set of expected rates
would fail Windows pushes for no regression, so expected.json is now
keyed by --platform: Linux and Linux HRX from local runs of main's
portables, Windows and Windows HRX from that CI run's baselines.

main's Linux HRX build loops on the long-form file (120 repeated
phrases), so window_tail is no longer required by default: every PR's
Linux HRX job would have failed on main's behaviour. The baseline check
"no more repeated phrases than the baseline" still catches a PR that
adds repeats.

Co-Authored-By: Claude <[email protected]>
- Chat transcription also runs gemma4-it:12b, which has its own audio
  front end (modeling_gemma4_12b_audio.cpp); E2B's is shared by E4B and
  the flash variants. Both jobs pull it.
- asr_summary.py renders the results as one report: a verdict per model
  and platform, then a section per model (Whisper accuracy per set,
  behaviour, checks by group, failures and warnings; each chat model's
  WER) with Linux, Linux HRX, Windows and Windows HRX side by side. A new
  summary job writes it to the run summary; asr_test.py uses the same
  renderer for each job's own report, which stays in its artifact.
- asr_test.py refuses to start when its port is already taken. flm exits
  when it cannot bind, but another server answers /v1/version, so the
  run would have carried on against it.
- State the tolerances: 1 point pooled and 5 per set against main; 2
  points pooled and the same 5 per set against expected.json.

Co-Authored-By: Claude <[email protected]>
@tawei-amd
tawei-amd merged commit 64f6a86 into main Oct 16, 2026
34 checks passed
quine00 pushed a commit to quine00/FastFlowLM that referenced this pull request Oct 16, 2026
The ASR test failed PR ROCm#798 on Windows HRX with "no more runaways than
the baseline: 4 vs 3". The new runaway, de-fleurs-02, loops on its
first half in both builds; ROCm#798's build also transcribed the second
half that main's dropped. Same 12 errors, more of the real speech, but
the extra words pushed it past 1.5 times the reference length.

- A file now counts against a build only if it became a runaway, or
  repeats more phrases, and also has more errors than in the baseline.
- The per-set limit applies to sets of ten or more files. A single
  long-form file moves by many points whenever one decode flips, which
  HRX builds do between identical runs (main's own Linux HRX build
  scored 18.5% and 36.8% on it); those files are left to the pooled
  rate and the two checks above.

Replayed against every CI result so far (ROCm#798 and ROCm#799, four platforms
each) and local runs of ROCm#510, ROCm#794 and ROCm#798: all pass, including ROCm#798
on Windows HRX. A synthetic new loop with more errors still fails.

Co-Authored-By: Claude <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant