Skip to content

fix(llm): fall back to CPU when a GPU embedding context fails to allocate - #958

Open
e-Garcia wants to merge 1 commit into
tobi:mainfrom
e-Garcia:fix/embed-context-gpu-cpu-fallback
Open

e-Garcia wants to merge 1 commit into
tobi:mainfrom
e-Garcia:fix/embed-context-gpu-cpu-fallback

Conversation

@e-Garcia

Copy link
Copy Markdown

Fixes #957.

Problem

ensureLlama() already falls back to CPU when the GPU backend itself fails to initialize (headless machine, no driver). ensureEmbedContexts() had no equivalent one layer deeper: the backend and model can both load onto GPU successfully, and the actual context allocation still fails, for example because another process is holding the VRAM at that exact moment. Before this change that threw Failed to create any embedding context outright, even though CPU-mode embedding worked fine with the identical model and config (confirmed on a host running Ollama on the same GPUs, ~1.3GB free of 28.2GB VRAM at the time).

qmd doctor's device probe only checks backend presence, not live free VRAM, so it reports GPU as available right up until the failure.

Fix

In ensureEmbedContexts(), when the first context creation attempt fails and GPU offload was not already forced, reload the embedding model with gpuLayers: 0 and retry context creation once against the CPU-loaded model, mirroring ensureLlama()'s existing fallback pattern. A new embedContextGpuFailed flag latches once this happens, so later calls in the same process skip straight to CPU instead of repeating a known-doomed GPU attempt each time.

computeParallelism and threadsPerContext both take an optional forceCpu parameter (default false, so every existing call site is unaffected) so the CPU-mode context count and thread split are computed correctly for the reloaded model, instead of the GPU-sized math being reused for a model now running on CPU.

If both the GPU attempt and the CPU retry fail, it still throws the original error, so a genuinely broken environment (no CPU-compatible llama.cpp binding either) fails the same way as before.

Testing

  • npm run test:types: clean
  • npx oxlint src/llm.ts test/llm.test.ts: clean
  • Full suite (node ./node_modules/vitest/vitest.mjs run test/): 50 files, 1250 passed, 79 skipped (same skip count as main, all require real GPU/models), 0 failed
  • Added 4 unit tests in test/llm.test.ts covering: the happy-path retry, the CPU retry also failing (throws, doesn't loop forever), no retry when QMD_FORCE_CPU is already set, and the latch preventing a repeated GPU attempt on a later call
  • Manually reproduced the original failure and fix on the host from createEmbeddingContext failure doesn't fall back to CPU the way GPU backend init does #957 (AMD RX 6900 XT + NVIDIA RTX 3060, Ollama holding VRAM): qmd embed --force went from failing outright to succeeding via the new fallback path, matching the behavior of the existing --no-gpu workaround but without needing it set

Notes

I looked at ensureRerankContexts' history first (the comment there mentions a previous "retry without flash attention" path that was removed as dead code, since ranking contexts never accepted that option) to make sure I wasn't reintroducing something already rejected. This is a different fallback: reloading the model with gpuLayers: 0, not passing an unsupported option to context creation, so it's not the same path. I left ensureRerankContexts and ensureRerankContexts's soft-degrade-to-[] behavior alone since reranking is already optional (--no-rerank) in a way embeddings aren't.

Changelog entry added under ## [Unreleased].

…cate

ensureLlama() already falls back to CPU when the GPU backend itself fails
to initialize, but ensureEmbedContexts() had no equivalent one layer
deeper: the backend and model can both load onto GPU successfully while
the actual context allocation still fails, e.g. because another process
(a co-located LLM server in my case) is holding the VRAM at that moment.
Previously this threw "Failed to create any embedding context" outright,
even though CPU-mode embedding worked fine with the identical model.

Reload the embedding model with gpuLayers: 0 and retry context creation
once on the first failure, mirroring ensureLlama()'s existing pattern.
Latches (embedContextGpuFailed) so later calls in the same process skip
straight to CPU instead of repeating a known-doomed GPU attempt.

Fixes tobi#957
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.

createEmbeddingContext failure doesn't fall back to CPU the way GPU backend init does

1 participant