🐛 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
- On a Raspberry Pi 5 running Ubuntu Server 24.04 (arm64)
- Configure
qvac.config.json to serve only an LLM model via qvac serve openai
- 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
- On a Raspberry Pi 5 (Cortex-A76) running Ubuntu Server 24.04 arm64, in Docker
- Configure
qvac.config.json to serve an LLM model via qvac serve openai
- Start the server
- 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
🐛 Bug Description
Minimal reproduction: https://gist.github.com/Bobby1kenoby/9b6f2a09c2d737717ff6460a2886fa77
Environment
Summary
The default worker entry point (
@qvac/sdk/dist/server/worker.js) unconditionallyregisters all bundled plugins on startup, including
nmtPlugin(
@qvac/translation-nmtcpp). On this device, loading that addon's native binarycrashes 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 startwith no indication that an unrelated, unrequested plugin is the actual cause.
Steps to reproduce
qvac.config.jsonto serve only an LLM model viaqvac serve openaiRoot 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 registersllmPlugin(imported from@qvac/sdk/llamacpp-completion/plugin) avoidsloading the translation addon entirely, and the LLM model then loads and
serves inference correctly.
Suggested fix
qvac.config.json, rather than unconditionally loading every bundled addonin the default worker.
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-nmtcppprebuild 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
qvac.config.jsonto serve an LLM model viaqvac serve openaiMinimal 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
💻 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.1directly, possibly from a next/beta channel rather than the stable dist-tag.📋 Relevant Dependencies
🔁 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:
✅ Checklist