From 26e3d0c07789f99932d996bb78137dab1686d123 Mon Sep 17 00:00:00 2001 From: cocoon Date: Tue, 7 Apr 2026 23:18:58 +0800 Subject: [PATCH 1/4] fix(status): avoid build attempts during device probe --- src/cli/qmd.ts | 11 +++++++---- src/llm.ts | 9 +++++---- test/llm.test.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/cli/qmd.ts b/src/cli/qmd.ts index a09ffb3..b0057ac 100755 --- a/src/cli/qmd.ts +++ b/src/cli/qmd.ts @@ -461,10 +461,10 @@ async function showStatus(): Promise { } // Device / GPU info + console.log(`\n${c.bold}Device${c.reset}`); try { const llm = getDefaultLlamaCpp(); - const device = await llm.getDeviceInfo(); - console.log(`\n${c.bold}Device${c.reset}`); + const device = await llm.getDeviceInfo({ allowBuild: false }); if (device.gpu) { console.log(` GPU: ${c.green}${device.gpu}${c.reset} (offloading: ${device.gpuOffloading ? 'yes' : 'no'})`); if (device.gpuDevices.length > 0) { @@ -486,8 +486,11 @@ async function showStatus(): Promise { console.log(` ${c.dim}Tip: Install CUDA, Vulkan, or Metal support for GPU acceleration.${c.reset}`); } console.log(` CPU: ${device.cpuCores} math cores`); - } catch { - // Don't fail status if LLM init fails + } catch (error) { + console.log(` Status: ${c.dim}skipped${c.reset} (status probe does not build llama.cpp backends)`); + if (error instanceof Error && error.message) { + console.log(` ${c.dim}${error.message}${c.reset}`); + } } // Tips section diff --git a/src/llm.ts b/src/llm.ts index 485ca7b..d07b89a 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -550,7 +550,7 @@ export class LlamaCpp implements LLM { /** * Initialize the llama instance (lazy) */ - private async ensureLlama(): Promise { + private async ensureLlama(allowBuild = true): Promise { if (!this.llama) { // Allow override via QMD_LLAMA_GPU: "false" | "off" | "none" forces CPU const gpuOverride = (process.env.QMD_LLAMA_GPU ?? "").toLowerCase(); @@ -558,9 +558,10 @@ export class LlamaCpp implements LLM { const loadLlama = async (gpu: "auto" | false) => await getLlama({ - build: "autoAttempt", + build: allowBuild ? "autoAttempt" : "never", logLevel: LlamaLogLevel.error, gpu, + skipDownload: !allowBuild, }); let llama: Llama; @@ -1244,14 +1245,14 @@ export class LlamaCpp implements LLM { * Get device/GPU info for status display. * Initializes llama if not already done. */ - async getDeviceInfo(): Promise<{ + async getDeviceInfo(options: { allowBuild?: boolean } = {}): Promise<{ gpu: string | false; gpuOffloading: boolean; gpuDevices: string[]; vram?: { total: number; used: number; free: number }; cpuCores: number; }> { - const llama = await this.ensureLlama(); + const llama = await this.ensureLlama(options.allowBuild ?? true); const gpuDevices = await llama.getGpuDeviceNames(); let vram: { total: number; used: number; free: number } | undefined; if (llama.gpu) { diff --git a/test/llm.test.ts b/test/llm.test.ts index d336036..f5c39cc 100644 --- a/test/llm.test.ts +++ b/test/llm.test.ts @@ -193,6 +193,32 @@ describe("LlamaCpp rerank deduping", () => { }); }); +describe("LlamaCpp.getDeviceInfo", () => { + test("can skip build attempts for status probes", async () => { + const llm = new LlamaCpp({}) as any; + const fakeLlama = { + gpu: "metal", + supportsGpuOffloading: true, + cpuMathCores: 8, + getGpuDeviceNames: vi.fn().mockResolvedValue(["Apple GPU"]), + getVramState: vi.fn().mockResolvedValue({ total: 1024, used: 256, free: 768 }), + }; + + llm.ensureLlama = vi.fn().mockResolvedValue(fakeLlama); + + const device = await llm.getDeviceInfo({ allowBuild: false }); + + expect(llm.ensureLlama).toHaveBeenCalledWith(false); + expect(device).toEqual({ + gpu: "metal", + gpuOffloading: true, + gpuDevices: ["Apple GPU"], + vram: { total: 1024, used: 256, free: 768 }, + cpuCores: 8, + }); + }); +}); + // ============================================================================= // Integration Tests (require actual models) // ============================================================================= From 1ecb5c9f96f3b5efcc7f83d91997203750a97920 Mon Sep 17 00:00:00 2001 From: Jeff Gardner <202880+erskingardner@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:49:22 +0200 Subject: [PATCH 2/4] Fix QMD_LLAMA_GPU backend override handling --- CHANGELOG.md | 4 ++++ src/llm.ts | 24 +++++++++++++++++------- test/llm.test.ts | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8feb5d..1e50d26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixes + +- GPU: respect explicit `QMD_LLAMA_GPU=metal|vulkan|cuda` backend overrides instead of always using auto GPU selection. #529 + ## [2.1.0] - 2026-04-05 Code files now chunk at function and class boundaries via tree-sitter, diff --git a/src/llm.ts b/src/llm.ts index 485ca7b..6f9e982 100644 --- a/src/llm.ts +++ b/src/llm.ts @@ -385,6 +385,18 @@ 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 function resolveLlamaGpuMode(envValue = process.env.QMD_LLAMA_GPU): LlamaGpuMode { + const normalized = envValue?.trim().toLowerCase() ?? ""; + if (!normalized) return "auto"; + if (["false", "off", "none", "disable", "disabled", "0"].includes(normalized)) return false; + if (normalized === "metal" || normalized === "vulkan" || normalized === "cuda") return normalized; + + process.stderr.write(`QMD Warning: invalid QMD_LLAMA_GPU="${envValue}", using auto GPU selection.\n`); + return "auto"; +} + function resolveExpandContextSize(configValue?: number): number { if (configValue !== undefined) { if (!Number.isInteger(configValue) || configValue <= 0) { @@ -552,11 +564,9 @@ export class LlamaCpp implements LLM { */ private async ensureLlama(): Promise { if (!this.llama) { - // Allow override via QMD_LLAMA_GPU: "false" | "off" | "none" forces CPU - const gpuOverride = (process.env.QMD_LLAMA_GPU ?? "").toLowerCase(); - const forceCpu = ["false", "off", "none", "disable", "disabled", "0"].includes(gpuOverride); + const gpuMode = resolveLlamaGpuMode(); - const loadLlama = async (gpu: "auto" | false) => + const loadLlama = async (gpu: LlamaGpuMode) => await getLlama({ build: "autoAttempt", logLevel: LlamaLogLevel.error, @@ -564,16 +574,16 @@ export class LlamaCpp implements LLM { }); let llama: Llama; - if (forceCpu) { + if (gpuMode === false) { llama = await loadLlama(false); } else { try { - llama = await loadLlama("auto"); + llama = await loadLlama(gpuMode); } catch (err) { // GPU backend (e.g. Vulkan on headless/driverless machines) can throw at init. // Fall back to CPU so qmd still works. process.stderr.write( - `QMD Warning: GPU init failed (${err instanceof Error ? err.message : String(err)}), falling back to CPU.\n` + `QMD Warning: GPU init failed${gpuMode === "auto" ? "" : ` for QMD_LLAMA_GPU=${gpuMode}`} (${err instanceof Error ? err.message : String(err)}), falling back to CPU.\n` ); llama = await loadLlama(false); } diff --git a/test/llm.test.ts b/test/llm.test.ts index d336036..f3797f0 100644 --- a/test/llm.test.ts +++ b/test/llm.test.ts @@ -12,6 +12,7 @@ import { LlamaCpp, getDefaultLlamaCpp, disposeDefaultLlamaCpp, + resolveLlamaGpuMode, withLLMSession, canUnloadLLM, SessionReleasedError, @@ -55,6 +56,38 @@ describe("LlamaCpp.modelExists", () => { }); }); +describe("QMD_LLAMA_GPU resolution", () => { + test("uses auto when unset or blank", () => { + expect(resolveLlamaGpuMode(undefined)).toBe("auto"); + expect(resolveLlamaGpuMode(" ")).toBe("auto"); + }); + + test("maps CPU disable values to false", () => { + expect(resolveLlamaGpuMode("false")).toBe(false); + expect(resolveLlamaGpuMode("OFF")).toBe(false); + expect(resolveLlamaGpuMode(" none ")).toBe(false); + expect(resolveLlamaGpuMode("disabled")).toBe(false); + expect(resolveLlamaGpuMode("0")).toBe(false); + }); + + test("passes through supported GPU backends", () => { + expect(resolveLlamaGpuMode("metal")).toBe("metal"); + expect(resolveLlamaGpuMode("VULKAN")).toBe("vulkan"); + expect(resolveLlamaGpuMode(" cuda ")).toBe("cuda"); + }); + + test("warns and falls back to auto for unsupported values", () => { + const stderrSpy = vi.spyOn(process.stderr, "write").mockReturnValue(true); + try { + expect(resolveLlamaGpuMode("rocm")).toBe("auto"); + expect(stderrSpy).toHaveBeenCalled(); + expect(String(stderrSpy.mock.calls[0]?.[0] || "")).toContain("QMD_LLAMA_GPU"); + } finally { + stderrSpy.mockRestore(); + } + }); +}); + describe("LlamaCpp expand context size config", () => { const defaultExpandContextSize = 2048; From 17074eafa1eca7ef7dadcb7fed4c87352a66ebc6 Mon Sep 17 00:00:00 2001 From: Ryan Malia Date: Tue, 7 Apr 2026 11:53:09 -0700 Subject: [PATCH 3/4] fix: include line in CLI --json search output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit but that function has no callers — the CLI's outputResults() uses its own inline JSON formatting that destructured only .snippet from extractSnippet(), discarding .line. Extract the full SnippetResult and spread the line field into the JSON output object. Closes #505 --- src/cli/qmd.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cli/qmd.ts b/src/cli/qmd.ts index a09ffb3..c6a66eb 100755 --- a/src/cli/qmd.ts +++ b/src/cli/qmd.ts @@ -1932,7 +1932,8 @@ function outputResults(results: OutputRow[], query: string, opts: OutputOptions) const output = filtered.map(row => { const docid = row.docid || (row.hash ? row.hash.slice(0, 6) : undefined); let body = opts.full ? row.body : undefined; - let snippet = !opts.full ? extractSnippet(row.body, query, 300, row.chunkPos, undefined, opts.intent).snippet : undefined; + const snippetInfo = !opts.full ? extractSnippet(row.body, query, 300, row.chunkPos, undefined, opts.intent) : undefined; + let snippet = snippetInfo?.snippet; if (opts.lineNumbers) { if (body) body = addLineNumbers(body); if (snippet) snippet = addLineNumbers(snippet); @@ -1941,6 +1942,7 @@ function outputResults(results: OutputRow[], query: string, opts: OutputOptions) ...(docid && { docid: `#${docid}` }), score: Math.round(row.score * 100) / 100, file: toQmdPath(row.displayPath), + ...(snippetInfo && { line: snippetInfo.line }), title: row.title, ...(row.context && { context: row.context }), ...(body && { body }), From 9dd8a738f936363de911e674c4dde55cd0573033 Mon Sep 17 00:00:00 2001 From: jungholee Date: Wed, 8 Apr 2026 19:55:07 +0900 Subject: [PATCH 4/4] fix(mcp): call enableProductionMode before getDefaultDbPath Co-Authored-By: Claude Opus 4.6 --- src/mcp/server.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 8f29f9c..0cfb607 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -30,6 +30,9 @@ import { type IndexStatus, } from "../index.js"; import { getConfigPath } from "../collections.js"; +import { enableProductionMode } from "../store.js"; + +enableProductionMode(); // ============================================================================= // Types for structured content