Skip to content

[Bug]: llm-llamacpp win32-x64 has no CPU fallback below AVX2 β€” 0xC000001D on pre-Haswell (linux-x64 ships 14 variants)Β #4151

Description

@bogidotcom

πŸ› Bug Description

@qvac/llm-llamacpp ships linux-x64 as a thin loader plus 14 dlopen-able per-microarch CPU backends, selected at runtime by ggml backend score β€” including ivybridge and sandybridge. It ships win32-x64 as a single statically-linked module with no CPU variant libraries and no runtime dispatch, built at an AVX2/FMA baseline.

Every Windows host older than Haswell therefore dies at first model load with STATUS_ILLEGAL_INSTRUCTION (0xC000001D). The same CPU works fine on Linux.

This is not #3222 / #3364. Those were an unrelated eagerly-loaded plugin (translation-nmtcpp) taking down the worker. We already use the recommended escape hatch β€” a custom bundle via qvac bundle sdk whose worker registers only llmPlugin, no other addon is ever loaded. The module crashing here is the LLM engine itself.

πŸ”„ Steps to Reproduce

Runtime repro (needs pre-Haswell x86 hardware):

  1. Any Windows x64 host with AVX but no AVX2/FMA (Sandy/Ivy Bridge, pre-Zen AMD, Goldmont/Tremont Celeron)
  2. Custom worker bundle registering only llmPlugin
  3. sdk.loadModel({ modelSrc: QWEN3_1_7B_INST_Q4, modelType: 'llm', modelConfig: { ctx_size: 8192 } })
  4. Worker dies with 0xC000001D before the RPC handshake

Static repro (no special hardware β€” runs on the published artifacts):

npm pack @qvac/llm-llamacpp@0.47.0 && tar xzf qvac-llm-llamacpp-0.47.0.tgz

# Their own AVX-only reference build: 486 xmm, 0 ymm
objdump -d package/prebuilds/linux-x64/qvac__llm-llamacpp/libqvac-ggml-cpu-ivybridge.so \
  | grep -oE 'vpmaddubsw\s+.*' | grep -oE '%(y|x)mm' | sort | uniq -c

# Their own AVX2 build: 2234 ymm, 0 xmm
objdump -d package/prebuilds/linux-x64/qvac__llm-llamacpp/libqvac-ggml-cpu-haswell.so \
  | grep -oE 'vpmaddubsw\s+.*' | grep -oE '%(y|x)mm' | sort | uniq -c

# win32 monolith: 1951 ymm, 0 xmm  <-- the AVX-only path is absent, not outnumbered
objdump -d package/prebuilds/win32-x64/qvac__llm-llamacpp.bare \
  | grep -oE 'vpmaddubsw\s+.*' | grep -oE '%(y|x)mm' | sort | uniq -c

ls package/prebuilds/win32-x64/    # no CPU variant libraries exist

βœ… Expected Behavior

win32-x64 selects a CPU backend matching the host, as linux-x64 already does β€” falling back to the AVX-only or SSE4.2 path on pre-AVX2 hardware, or at minimum failing with a clear "unsupported CPU" error instead of an illegal instruction.

❌ Actual Behavior

The worker process is killed by the OS with 0xC000001D before the RPC handshake. The SDK surfaces this as a generic init timeout, so the user sees a network-looking error with no hint that the CPU is the cause (error-surfacing side is tracked in #1821).

πŸ“œ Stack Trace / Error Output

Error: RPCInitTimeoutError: RPC initialization timed out after 30000ms
Caused by: Error: Worker process exited with code 3221225501, signal null before IPC connection was established

# 3221225501 = 0xC000001D = STATUS_ILLEGAL_INSTRUCTION

πŸ’» Platform / OS

Windows (x64)

πŸ–₯️ OS Version

Windows 10 x64 (reporter). CPU: Intel Xeon E5-2696 v2 (Ivy Bridge-EP β€” AVX, no AVX2, no FMA). GPU: AMD Radeon RX 580 (Vulkan).

βš™οΈ Runtime Environment

Node.js

πŸ“¦ Runtime Version

Electron 31.7.7 (Node 20.x) on the client side; worker runs bare-runtime 1.29.5

🏷️ SDK Version

0.16.0

πŸ“‹ Relevant Dependencies

{
  "@qvac/sdk": "0.16.0",
  "@qvac/llm-llamacpp": "0.38.2",
  "bare-runtime": "1.29.5",
  "electron": "31.7.7"
}

πŸ” Frequency

Always (100%)

πŸ”₯ Severity

Critical - Complete blocker, no workaround

🩹 Workaround

None on Windows. The only escape is a different OS β€” the Linux build runs on the exact same hardware, because linux-x64 ships the ivybridge backend and Vulkan works on the RX 580 there.

Downstream we now decode the exit status ourselves and latch our circuit breaker so we stop respawning a worker that can never start, but that is error reporting, not a fix.

πŸ“Ž Additional Context

Packaging asymmetry

prebuilds/linux-x64/ β€” loader + 14 variants:

qvac__llm-llamacpp.bare (12.7 MB loader)   FMA/AVX2/AVX512: 0
libqvac-ggml-cpu-sandybridge.so            vpmaddubsw:  486 x xmm
libqvac-ggml-cpu-ivybridge.so              vpmaddubsw:  486 x xmm
libqvac-ggml-cpu-haswell.so                vpmaddubsw: 2234 x ymm
+ sse42, x64, piledriver, skylakex, icelake, cannonlake, cascadelake,
  cooperlake, sapphirerapids, alderlake, zen4

prebuilds/win32-x64/ β€” one module, no variants:

qvac__llm-llamacpp.bare (95 MB)            vpmaddubsw: 1951 x ymm, 0 x xmm
                                           vfmadd231ps 209, vfmadd213ps 66,
                                           vpermd 67, vpbroadcastd 31, vpsravd 6

On the reliability of this measurement β€” #3364 correctly established that raw mnemonic counting false-positives on runtime-dispatched code, and that caveat applies here too: this binary also contains AVX-512 mnemonics (52 vmovdqu8) that are certainly dispatched (llamafile/tinyBLAS sgemm, CRT memcpy), since otherwise it would fault on nearly every consumer CPU.

That is why the argument above is not a raw count. It is the register width of the quantized dot-product kernels measured against your own reference builds: your AVX-only variants are 486 xmm / 0 ymm, your AVX2 variant is 2234 ymm / 0 xmm, and win32 is 1951 ymm / 0 xmm. The AVX-only code path is not outnumbered by a dispatched AVX2 path β€” it is absent from the binary. Those ggml quant kernels are compile-time selected, not runtime-dispatched.

Independently: GGML_CPU_ALL_VARIANTS requires GGML_BACKEND_DL, which emits variant backends as separate shared libraries. prebuilds/win32-x64/ contains none, so the CPU backend is necessarily statically linked at one baseline.

Vulkan does not rescue it. Vulkan is already compiled into the Windows module (ggml-vulkan, GGML_DISABLE_VULKAN, Vulkan 1.2 required are all present in the binary). It never gets a chance: ggml registers and initializes the CPU backend unconditionally at registry construction, before any GPU is consulted. The reporter has a working Vulkan GPU and is blocked by a code path they would never execute.

Not a regression β€” it has never shipped. Published file listings across the package's history:

version published win32-x64 linux-x64
0.1.0 2025-08-26 1 module, no variants β€”
0.31.2 2026-07-06 1 module, no variants 14 variants
0.38.2 2026-07-25 1 module, no variants 14 variants
0.47.0 2026-08-24 1 module, no variants 14 variants

So downstreams cannot fix this by upgrading β€” I verified 0.47.0 before filing.

Build provenance leaked in the binary:
C:\Users\actions-runner\vcpkg\buildtrees\qvac-fabric\src\v9840.0.0-f78517b95a.clean\ggml\src\ggml-cpu\repack.cpp

Ask: build win32-x64 with GGML_BACKEND_DL + GGML_CPU_ALL_VARIANTS, the same way linux-x64 already is. This is the x86 analogue of the BUILD_ARCH pinning that resolved #3364, and it fixes the whole pre-Haswell Windows tier at once β€” Xeon E5 v1/v2 (very common in homelab servers), pre-Zen AMD, and the Atom-line Celeron/Pentium range, where Goldmont and Tremont have no AVX at all.

Disclosure: I do not have the affected hardware. The 0xC000001D crash is from a downstream user report; the binary analysis above is mine, against the published artifacts, and is reproducible with the commands in the repro section.

βœ… Checklist

  • I have searched existing issues for duplicates
  • I have included a minimal reproduction
  • I am using a supported SDK version

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions