Fix Windows CUDA context parallelism

This commit is contained in:
Tobi Lütke 2026-05-09 17:56:28 +00:00
parent dff6513693
commit e8229d8bfb
No known key found for this signature in database
4 changed files with 83 additions and 4 deletions

View File

@ -12,6 +12,7 @@
- Hybrid search: weight RRF lists by query type so original FTS and original vector evidence get the intended 2x boost, instead of accidentally boosting the first lexical expansion. #591
- MCP: seed llama.cpp/GGML quiet env vars before launching `qmd mcp` so native logs cannot pollute stdio JSON-RPC framing. #593
- CLI: remove CommonJS `require()` calls from ESM index path normalization so `qmd --index <path>` no longer crashes with `ERR_AMBIGUOUS_MODULE_SYNTAX` on Node 22+. #634
- Windows CUDA: serialize llama.cpp embedding/reranking contexts by default to avoid intermittent `ggml-cuda.cu:98` crashes in `qmd query`; set `QMD_EMBED_PARALLELISM` to opt back into parallel contexts if your driver is stable. #519
- GPU: respect explicit `QMD_LLAMA_GPU=metal|vulkan|cuda` backend overrides instead of always using auto GPU selection. #529
- Fix: preserve original filename case in `handelize()`. The previous
`.toLowerCase()` call made indexed paths unreachable on case-sensitive

View File

@ -797,6 +797,8 @@ llm_cache -- Cached LLM responses (query expansion, rerank scores)
| Variable | Default | Description |
|----------|---------|-------------|
| `XDG_CACHE_HOME` | `~/.cache` | Cache directory location |
| `QMD_LLAMA_GPU` | `auto` | Force llama.cpp GPU backend (`metal`, `vulkan`, `cuda`) or disable GPU with `false` |
| `QMD_EMBED_PARALLELISM` | automatic | Override embedding/reranking context parallelism (1-8). Windows CUDA defaults to `1` because parallel CUDA contexts can crash with `ggml-cuda.cu:98`; use Vulkan or raise this only if your driver is stable. |
## How It Works

View File

@ -451,7 +451,41 @@ export type LlamaCppConfig = {
const DEFAULT_INACTIVITY_TIMEOUT_MS = 5 * 60 * 1000;
const DEFAULT_EXPAND_CONTEXT_SIZE = 2048;
type LlamaGpuMode = "auto" | "metal" | "vulkan" | "cuda" | false;
export type LlamaGpuMode = "auto" | "metal" | "vulkan" | "cuda" | false;
type ParallelismOptions = {
gpu: string | false;
platform?: NodeJS.Platform;
computed: number;
envValue?: string;
};
export function resolveParallelismOverride(envValue = process.env.QMD_EMBED_PARALLELISM): number | undefined {
const normalized = envValue?.trim() ?? "";
if (!normalized) return undefined;
const parsed = Number(normalized);
if (!Number.isInteger(parsed) || parsed < 1) {
process.stderr.write(`QMD Warning: invalid QMD_EMBED_PARALLELISM="${envValue}", using automatic parallelism.\n`);
return undefined;
}
return Math.min(8, parsed);
}
export function resolveSafeParallelism(options: ParallelismOptions): number {
const override = resolveParallelismOverride(options.envValue);
if (override !== undefined) return override;
// node-llama-cpp/llama.cpp CUDA on Windows is unstable with multiple
// simultaneous contexts (ggml-cuda.cu:98 in #519). Vulkan and CPU do not
// show the same failure mode, so only serialize Windows CUDA by default.
if ((options.platform ?? process.platform) === "win32" && options.gpu === "cuda") {
return 1;
}
return Math.max(1, options.computed);
}
export function resolveLlamaGpuMode(envValue = process.env.QMD_LLAMA_GPU): LlamaGpuMode {
const normalized = envValue?.trim().toLowerCase() ?? "";
@ -726,16 +760,18 @@ export class LlamaCpp implements LLM {
const vram = await llama.getVramState();
const freeMB = vram.free / (1024 * 1024);
const maxByVram = Math.floor((freeMB * 0.25) / perContextMB);
return Math.max(1, Math.min(8, maxByVram));
const computed = Math.max(1, Math.min(8, maxByVram));
return resolveSafeParallelism({ gpu: llama.gpu, computed });
} catch {
return 2;
return resolveSafeParallelism({ gpu: llama.gpu, computed: 2 });
}
}
// CPU: split cores across contexts. At least 4 threads per context.
const cores = llama.cpuMathCores || 4;
const maxContexts = Math.floor(cores / 4);
return Math.max(1, Math.min(4, maxContexts));
const computed = Math.max(1, Math.min(4, maxContexts));
return resolveSafeParallelism({ gpu: false, computed });
}
/**

View File

@ -13,6 +13,8 @@ import {
getDefaultLlamaCpp,
disposeDefaultLlamaCpp,
resolveLlamaGpuMode,
resolveParallelismOverride,
resolveSafeParallelism,
withLLMSession,
canUnloadLLM,
SessionReleasedError,
@ -88,6 +90,44 @@ describe("QMD_LLAMA_GPU resolution", () => {
});
});
describe("LLM context parallelism safety", () => {
test("defaults Windows CUDA to one context to avoid ggml-cuda.cu:98 crashes", () => {
expect(resolveSafeParallelism({
gpu: "cuda",
platform: "win32",
computed: 8,
envValue: undefined,
})).toBe(1);
});
test("keeps non-Windows and non-CUDA backends on computed parallelism", () => {
expect(resolveSafeParallelism({ gpu: "cuda", platform: "linux", computed: 8 })).toBe(8);
expect(resolveSafeParallelism({ gpu: "vulkan", platform: "win32", computed: 8 })).toBe(8);
expect(resolveSafeParallelism({ gpu: false, platform: "win32", computed: 4 })).toBe(4);
});
test("QMD_EMBED_PARALLELISM overrides the Windows CUDA safety default", () => {
expect(resolveSafeParallelism({
gpu: "cuda",
platform: "win32",
computed: 8,
envValue: "2",
})).toBe(2);
});
test("QMD_EMBED_PARALLELISM clamps invalid values and warns", () => {
const stderrSpy = vi.spyOn(process.stderr, "write").mockReturnValue(true);
try {
expect(resolveParallelismOverride("0")).toBeUndefined();
expect(resolveParallelismOverride("bad")).toBeUndefined();
expect(stderrSpy).toHaveBeenCalledTimes(2);
expect(String(stderrSpy.mock.calls[0]?.[0] || "")).toContain("QMD_EMBED_PARALLELISM");
} finally {
stderrSpy.mockRestore();
}
});
});
describe("LlamaCpp expand context size config", () => {
const defaultExpandContextSize = 2048;