Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 threwFailed to create any embedding contextoutright, 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 withgpuLayers: 0and retry context creation once against the CPU-loaded model, mirroringensureLlama()'s existing fallback pattern. A newembedContextGpuFailedflag 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.computeParallelismandthreadsPerContextboth take an optionalforceCpuparameter (defaultfalse, 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: cleannpx oxlint src/llm.ts test/llm.test.ts: cleannode ./node_modules/vitest/vitest.mjs run test/): 50 files, 1250 passed, 79 skipped (same skip count asmain, all require real GPU/models), 0 failedtest/llm.test.tscovering: the happy-path retry, the CPU retry also failing (throws, doesn't loop forever), no retry whenQMD_FORCE_CPUis already set, and the latch preventing a repeated GPU attempt on a later callqmd embed --forcewent from failing outright to succeeding via the new fallback path, matching the behavior of the existing--no-gpuworkaround but without needing it setNotes
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 withgpuLayers: 0, not passing an unsupported option to context creation, so it's not the same path. I leftensureRerankContextsandensureRerankContexts's soft-degrade-to-[]behavior alone since reranking is already optional (--no-rerank) in a way embeddings aren't.Changelog entry added under
## [Unreleased].