Skip to content

fix(whisper): fall back to whisper-server builds for processors without AVX2 - #2396

Draft
greatcoat wants to merge 8 commits into
mainfrom
fix/2356-no-avx2-cpu-fallback
Draft

greatcoat wants to merge 8 commits into
mainfrom
fix/2356-no-avx2-cpu-fallback

Conversation

@greatcoat

Copy link
Copy Markdown
Contributor

In short: on PCs whose processor has AVX but not AVX2 (Intel 2nd/3rd-gen Core, AMD FX, older AMD A-series/Athlon, AMD Jaguar), local Whisper never starts. The bundled engine stops on its first AVX2-era instruction, and the user sees "whisper-server process died during startup: exit code: 3221225501". With this PR the app switches to a build made for those processors and keeps using it, so transcription works. It depends on new builds from OpenWhispr/whisper.cpp#8, so it stays a draft until that release is pinned (see Remaining).

Refs #2356. Also reported in #1418, #1613 and #793.

Problem

Root cause

The CPU whisper-server comes from OpenWhispr/whisper.cpp. It is statically linked and configured with -DGGML_NATIVE=OFF, which in ggml turns AVX2, FMA, F16C and BMI2 on. The 0.0.10 CI log prints /arch:AVX2 GGML_AVX2;GGML_FMA;GGML_F16C;__BMI2__;GGML_BMI2.

A static build has no run-time CPU dispatch. It dies in ggml_cpu_init() on a BMI2 shlx, before the port binds.

The ggml-cpu-ivybridge.dll that reporters found in resources/bin belongs to llama-server's llama.cpp bundle. whisper-server imports no ggml DLL, so it never uses it.

Fix

whisper.cpp (OpenWhispr/whisper.cpp#8):

  • Two more static CPU builds per Windows and Linux x64, at upstream ggml's ivybridge (SSE4.2 + AVX + F16C) and sandybridge (SSE4.2 + AVX) levels.
  • A startup check, so each of those builds refuses to start on a processor below its level.
  • CI that runs every build on an emulated processor of its level.

This PR:

  1. whisperServer.js names the signal when the server dies at startup. A SIGILL used to leave no detail at all. It uses the same names as the exit-signal capture in fix(whisper): save and show why GPU acceleration fell back to CPU #2317.
  2. Fallback on an illegal instruction. When a CPU build dies that way at startup (exit code 3221225501 in either sign, or SIGILL), _doStart retries on the next build in CPU_FALLBACK_LEVELS (ivybridge, then sandybridge) installed next to the build that died.
    • Each build tried becomes cachedServerBinaryPath, so later starts in the session (restarts, model or VAD changes, GPU→CPU fallbacks) never re-run a build that crashed.
    • This check runs after the GPU→CPU branch and before the thread-count retry, which used to relaunch the same crashing build.
    • With no build left, startup fails with whisper-server can't run on this processor: … (exit code: 3221225501) ….
    • Any other startup failure behaves exactly as before.
  3. download-whisper-cpp.js installs every build a platform lists. Entries are keyed per build with a platformArch, like download-llama-server.js, and each build has its own install marker. A release missing any listed build fails --current. Today's four builds, their markers and their output are unchanged.
  4. afterPack fails packaging when a listed fallback build is missing from resources/bin. The primary build is left to the download step.

Why two levels: without F16C, ggml converts fp16 weights one at a time through a lookup table. In local x86 runs (Rosetta, ratios only), the sandybridge build was about 7× slower than ivybridge, and every processor named in the reports has F16C.

Why a startup check in the fork: the first emulated run showed the ivybridge build starting and serving on a Sandy Bridge. ggml's startup code never touches F16C, so the build would only have crashed mid-transcription, where no fallback can see it. The check makes "dies at startup" a guarantee rather than an accident of ggml's init code. It is enabled only for the new builds, because Rosetta 2 (and some hypervisors) hide AVX in CPUID yet run it, and the existing build must behave exactly as before.

Deliberately untouched:

  • The primary CPU build, and the GPU packs.
    • CUDA and Vulkan also need AVX2. On these CPUs they fall back to CPU, which then walks down the levels (tested).
  • macOS: there is no level build. A darwin-x64 build that dies this way now gets the clearer message.
  • No persistence to .env. The primary's crash costs about 0.1–0.5 s once per launch, during pre-warm.
  • No new UI or translations. fix(whisper): clear message when local Whisper can't run (no AVX) #963's localized "processor not supported" toast can key on this message.

Tests

test/helpers/whisperServerCpuFallback.test.js has 11 tests. They spawn fake builds (shell scripts that die by SIGILL or SIGSEGV, or serve HTTP) through the real start path:

  • the Ivy Bridge chain and the Sandy Bridge chain;
  • a missing level;
  • no level left;
  • every level dying;
  • the switch outliving stop();
  • a CUDA pack on such a CPU;
  • a non-SIGILL crash;
  • racing starts;
  • the Windows exit code in both signs.

Results:

  • On the untouched base: 11/11 fail. Every scenario ends in the bare whisper-server process died during startup.
  • On this branch: 11/11 pass. 133/133 across the whisper, GPU, download and afterPack suites (Node 24).
  • New test/scripts/afterPackWhisperServerBuilds.test.js and the new downloadWhisperCpp tests cover the required-builds rule.
  • A real --current download of 0.0.10 installs exactly what it did before.

The full npm test locally fails only in editor, markdown and chat suites, which need happy-dom / remark-gfm absent from that machine's node_modules. CI's clean install is the authoritative run.

Independent review: a read-only deep review covered the whole branch. It found no correctness issue; its doc, backstop and timeout findings are fixed in the last two commits.

Merging with #2317

There is one conflict, in whisperServer.js. Both PRs add the identical exitSignal and getProcessInfo lines, and #2317 also adds this._lastProcessInfo = getProcessInfo;. Take #2317's side of that hunk; keeping both declares exitSignal twice. Everything else merges cleanly in either order.

Remaining (why this is a draft)

  1. Review and merge build: whisper-server for processors without AVX2 (ivybridge, sandybridge) whisper.cpp#8, then publish 0.0.11 from master. Its verify-cpu-levels job must pass first.

  2. The final commit on this branch:

    • pin WHISPER_CPP_TAG to 0.0.11;
    • list the four fallback builds;
    • record the 0.0.11 CUDA and Vulkan digests;
    • add the TROUBLESHOOTING line and the CLAUDE.md platform note. They are held back until then, so no commit documents builds it doesn't ship.

    Until then this branch finds no fallback build, and such a CPU gets the clearer error.

  3. After the app release ships, issues whisper-server crashes with Illegal Instruction (0xC000001D) on Ivy Bridge CPU (no AVX2) #2356, whisper-server-win32-x64.exe crashes with STATUS_ILLEGAL_INSTRUCTION (0xC000001D) on Intel Ivy Bridge (i5-3570) #1418, whisper-server-win32-x64.exe crashes with STATUS_ILLEGAL_INSTRUCTION (0xc000001d) on non-AVX2 CPUs, blocking GPU fallback path #1613 and OpenWhispr local whisper-server crashes with SIGILL on AMD FX-8350 #793 can be closed by hand.

Not verified

  • Real Sandy Bridge, Ivy Bridge, FX or Jaguar hardware. Every run-time proof is emulated (SDE and QEMU).
  • A packaged Windows or Linux install on such a machine.
  • Speed on those processors.

Follow-ups:

  • The GPU card offers "Retry GPU acceleration", which cannot succeed on these CPUs.
  • parakeetWsServer.js and qdrantManager.js also drop the signal on a signal death.
  • A build for processors without AVX (SSE4.2) if reports arrive.
  • Crash-report noise: one crashed launch per app start on these CPUs, two on Sandy Bridge.

Plan: docs/superpowers/plans/2026-09-28-no-avx2-cpu-fallback.md

🤖 Generated with Claude Code

greatcoat and others added 6 commits September 28, 2026 17:18
A Unix signal death has no exit code, so a whisper-server killed by SIGILL at
startup failed with "whisper-server process died during startup" and nothing
else. The close handler now records the signal, and the startup error names it
when the server printed nothing, as it already names a Windows exit code.
Same names as the exit-signal capture in #2317, so the two merge cleanly.

Refs #2356

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ut AVX2

The CPU whisper-server needs AVX2, FMA, F16C and BMI2, so on Intel processors
before Haswell and on AMD FX it dies at startup with an illegal instruction
(Windows exit code 3221225501, Unix SIGILL) and local Whisper never starts.

When a CPU build dies that way, retry on the next build for older processors
installed beside it (ivybridge: SSE4.2 + AVX + F16C, then sandybridge: SSE4.2
+ AVX) and keep the one that starts for the rest of the session. The thread
count retry no longer relaunches a build that crashed this way. With no build
left, startup fails with a message saying the processor can't run it. Any
other startup failure behaves as before.

Refs #2356

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
BINARIES is now keyed by build, and each entry names the platform it belongs
to, like download-llama-server.js. `--current` installs every build listed for
the platform and fails the build when any of them is missing from the release,
so a release without one of the builds for older processors can never ship
without it. Each build keeps its own install marker; the existing builds keep
their keys and marker names. Only the existing four builds are listed until the
whisper.cpp release that adds the ivybridge and sandybridge builds is pinned.

Refs #2356

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The two-repository plan behind this branch: the diagnosis, the whisper.cpp
fork's ivybridge and sandybridge builds and their emulated checks, the desktop
fallback and download changes, and what waits for the 0.0.11 release.

Refs #2356

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s missing

The builds for processors without AVX2 are what whisperServer.js falls back to
when the primary dies of an illegal instruction, and nothing else notices one
missing: a download step that installed only the primary would still exit 0.
afterPack now fails the build when a fallback build that download-whisper-cpp.js
lists for the platform is not in resources/bin. The primary is left to the
download step, so local cross-arch packaging is unchanged, and the check is a
no-op until the pinned release lists fallback builds.

Refs #2356

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Until the whisper.cpp release with the builds for processors without AVX2 is
pinned, none is installed, so the TROUBLESHOOTING line and the CLAUDE.md and
code-comment claims about which platforms ship them were false at this commit.
They move to the commit that pins the release; the CLAUDE.md entry now says
each fallback tried is cached. The tests that spawn fake builds get a timeout,
so a relaunch loop fails instead of hanging CI. The plan records the fork's
startup level check and these changes.

Refs #2356

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
greatcoat and others added 2 commits September 28, 2026 18:39
Refs #2356

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ot state

From the test-quality review:
- Each fake server gets an OS-assigned port. A fake that dies never binds its
  port, so with the manager's fixed range a second run of this file (another
  worktree) could answer its health check; two concurrent runs now pass.
- The CUDA test proves the fallback session keeps its CPU server behaviourally:
  with a Vulkan pack installed, a later Vulkan request spawns nothing. It no
  longer reads gpuFallbackActive, which an extra stop() would have cleared.
- The fallback builds' file names are pinned literally, so a Windows naming
  mistake fails a test before the release that ships the builds is pinned.

Refs #2356

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

This branch has not been deployed

No deployments
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