Skip to content

[Bug]: Default worker entry crashes with SIGILL on Raspberry Pi 5 (Cortex-A76) due to translation-nmtcpp addon #3222

Description

@Bobby1kenoby

🐛 Bug Description

Minimal reproduction: https://gist.github.com/Bobby1kenoby/9b6f2a09c2d737717ff6460a2886fa77

Environment

  • Device: Raspberry Pi 5 (Cortex-A76, CPU part 0xd0b)
  • OS: Ubuntu Server 24.04 (arm64), running inside Docker (ubuntu:24.04 base image)
  • Node.js: 22.23.1
  • @qvac/sdk: 0.14.1
  • Mesa: 25.2.8 (V3DV Vulkan driver working correctly, confirmed via vulkaninfo)

Summary
The default worker entry point (@qvac/sdk/dist/server/worker.js) unconditionally
registers all bundled plugins on startup, including nmtPlugin
(@qvac/translation-nmtcpp). On this device, loading that addon's native binary
crashes the entire worker process with SIGILL before any plugin — including the
one actually configured for use (llm-llamacpp) — becomes available.

This manifests to the end user only as a generic, unhelpful error:
RPC initialization timed out after 30000ms — the worker process may have failed to start
with no indication that an unrelated, unrequested plugin is the actual cause.

Steps to reproduce

  1. On a Raspberry Pi 5 running Ubuntu Server 24.04 (arm64)
  2. Configure qvac.config.json to serve only an LLM model via qvac serve openai
  3. Start the server — preload fails after 30s with the generic RPC timeout error

Root cause (confirmed via manual reproduction)
Spawning the default worker directly and attaching gdb shows:

The crash happens during dlopen() of the translation addon's native binary —
almost certainly an illegal instruction for Cortex-A76 (no i8mm/SVE/ARMv8.6+
support; ARMv8.2 with dotprod only), unrelated to Vulkan, GPU passthrough, or
the LLM plugin actually being used.

Workaround
Building a custom worker entry (via QVAC_WORKER_PATH) that only registers
llmPlugin (imported from @qvac/sdk/llamacpp-completion/plugin) avoids
loading the translation addon entirely, and the LLM model then loads and
serves inference correctly.

Suggested fix

  • Only register plugins for model types actually referenced in the resolved
    qvac.config.json, rather than unconditionally loading every bundled addon
    in the default worker.
  • At minimum, surface the underlying worker crash reason/signal in the
    RPC timeout error message instead of a generic "may have failed to start" —
    this would have saved significant debugging time, since the actual SIGILL
    was silently swallowed by the parent process.

Independent confirmation

This same root cause (SVE instructions in the linux-arm64 translation-nmtcpp
prebuild causing SIGILL on non-SVE ARM CPUs, including all Raspberry Pi models)
is independently documented as a known issue in another project's build notes:
https://github.com/savewithstash/stash — see their Docker build script comments
regarding an "ARM SVE workaround."
Related/independent confirmation: https://github.com/savewithstash/stash (documents the same SVE-on-ARM SIGILL as a known workaround in their build)

🔄 Steps to Reproduce

  1. On a Raspberry Pi 5 (Cortex-A76) running Ubuntu Server 24.04 arm64, in Docker
  2. Configure qvac.config.json to serve an LLM model via qvac serve openai
  3. Start the server
  4. Preload fails after 30s with a generic RPC timeout

Minimal reproduction (Dockerfile + config + workaround): [your gist link here]

Manual repro without the CLI wrapper (bypasses the timeout to show the real crash):

```bash
docker exec qvac node /path/to/manual-spawn-test.js

→ Worker exited: null SIGILL

```

Attaching gdb to the spawned worker confirms the crash is in the
translation-nmtcpp addon's native binary, not the LLM plugin being used:

```
Thread 1 "bare" received signal SIGILL, Illegal instruction.
0x0000ffff55b74610 in _GLOBAL__sub_I_fastopt.cpp ()
from .../node_modules/@qvac/translation-nmtcpp/prebuilds/linux-arm64/qvac__translation-nmtcpp.bare
```

✅ Expected Behavior

The LLM model configured in qvac.config.json (my-llm) should preload and become available within the RPC init timeout.

❌ Actual Behavior

Preload fails after 30s with a generic timeout:
RPC initialization timed out after 30000ms — the worker process may have failed to start

No indication is given that an unrelated, unconfigured plugin (translation-nmtcpp) is the actual cause of the crash.

📜 Stack Trace / Error Output

Thread 1 "bare" received signal SIGILL, Illegal instruction.
0x0000ffff55b74610 in _GLOBAL__sub_I_fastopt.cpp ()
   from .../node_modules/@qvac/translation-nmtcpp/prebuilds/linux-arm64/qvac__translation-nmtcpp.bare
#0  0x0000ffff55b74610 in _GLOBAL__sub_I_fastopt.cpp ()
   from .../node_modules/@qvac/translation-nmtcpp/prebuilds/linux-arm64/qvac__translation-nmtcpp.bare
#1  0x0000ffffa15b4b14 in call_init (...) at ./elf/dl-init.c:74
...
#14 0x0000aaaaad0d10bc in bare_runtime.load_dynamic_addon ()

💻 Platform / OS

Linux (ARM64)

🖥️ OS Version

Ubuntu Server 24.04 (arm64), running inside Docker on the host (ubuntu:24.04 container base image)

⚙️ Runtime Environment

Other (specify in additional context)

📦 Runtime Version

Node.js 22.23.1 bare-runtime-linux-arm64 1.30.3

🏷️ SDK Version

@qvac/sdk 0.14.1 @qvac/translation-nmtcpp (bundled dependency, version installed via @qvac/sdk 0.14.1) Note: 0.14.1 appears ahead of the current npm "latest" tag (0.11.0 as of this writing) — installed via npm install @qvac/sdk@0.14.1 directly, possibly from a next/beta channel rather than the stable dist-tag.

📋 Relevant Dependencies

Mesa 25.2.8 (V3DV Vulkan driver, confirmed working via vulkaninfo)
CPU: Cortex-A76 (CPU part 0xd0b), ARMv8.2-A with dotprod, no SVE/i8mm/ARMv8.6+ extensions

🔁 Frequency

Always (100%)

🔥 Severity

High - Major functionality broken

🩹 Workaround

Building a custom worker entry (via QVAC_WORKER_PATH) that only registers llmPlugin (imported from @qvac/sdk/llamacpp-completion/plugin) avoids loading the translation addon entirely, and the LLM model then loads and serves inference correctly. See gist for full example.

📎 Additional Context

Node.js (via Bare runtime for native addon loading), Docker container, Raspberry Pi 5 hardware

severity - blocks all use of the SDK's default worker/CLI serve command on Raspberry Pi (and any other non-SVE ARM64 device) unless the translation plugin is manually excluded

Independent confirmation: https://github.com/savewithstash/stash documents the same SVE-on-ARM SIGILL as a known workaround in their own build (they replace the native binding with a JS stub since they don't use translation).

Root cause: the default worker entry (@qvac/sdk/dist/server/worker.js) unconditionally registers ALL bundled plugins on startup regardless of what's in qvac.config.json. The linux-arm64 prebuild of @qvac/translation-nmtcpp contains SVE instructions, which SIGILL on any CPU without SVE support (all Raspberry Pi models, Apple Silicon under emulation, etc.).

Suggested fix:

  • Only register plugins for model types actually referenced in the resolved qvac.config.json, instead of unconditionally loading every bundled addon.
  • At minimum, surface the underlying worker crash signal/reason in the RPC timeout error instead of a generic "may have failed to start" message — this would save significant debugging time for anyone hitting this.

✅ Checklist

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions