test: VRAM precondition gate for integration suites with opt-in Ollama auto-evacuate - #726
Open
brettdavies wants to merge 3 commits into
Open
brettdavies wants to merge 3 commits into
brettdavies wants to merge 3 commits into
Conversation
Adds `test/_helpers/vram-precondition.ts` and wires it into the ten `describe.skipIf` gates across `test/llm.test.ts`, `test/mcp.test.ts`, `test/store.test.ts`, `test/eval.test.ts`, and `test/sdk.test.ts`. Each integration suite was previously gated only on `CI` (a few also `QMD_REMOTE_URL`). On a dev box where Ollama or another workload is parking most of the GPU, the suites ran against contested VRAM and produced eighteen `Failed to create any rerank context` failures that did not represent code regressions. The new gate probes free VRAM once per process and skips the suites when the headroom is insufficient. The helper: - Probes `getLlama().getVramState()` once and caches the result for the process lifetime. - Threshold: 4 GB free covers a meaningful run of all three models (embed + rerank + generate ≈ 3.5 GB peak plus margin). - `QMD_SKIP_GPU_INTEGRATION=1` env override forces a skip without probing, useful for fast unit-loop iteration on a healthy box. Only the exact string `"1"` triggers; any other value (including truthy strings like `"true"`) is treated as unset. - CPU mode (`gpu === false`) returns true without probing. Per-token VRAM cost model does not apply. - Probe failure fails open (returns true) so a genuine code failure surfaces in the test rather than being silently masked by a skip. - Loud one-shot warning when a skip is recorded, with the actual free-GB value and the override hint. Helps the operator diagnose why integration suites were silent in a CI tail. - Dependency injection seam (`options.getLlama`, `options.minFreeGB`, `options.log`) so the unit tests of the helper run without loading the real node-llama-cpp runtime. Test coverage (`test/vram-precondition.test.ts`, 12 cases): - CPU mode returns true without probing. - Plentiful VRAM, exactly at threshold, and just below threshold (boundary behavior). - Ollama-hogged scenario (~1.4 GB free) returns false and emits the warning. - `QMD_SKIP_GPU_INTEGRATION=1` forces a skip without probing. - `getLlama` and `getVramState` throwing both fail open. - Result is cached so repeated calls do not re-probe. - Skip warning logs at most once per process. - Red-team: env values other than the exact string `"1"` (e.g. `"0"`, `"false"`, `"true"`, `"yes"`, `""`) do not force a skip. - Concurrent first-call behavior documented (current implementation does not coalesce racing probes; cached value wins after first resolve). Wider-suite impact under the actual Ollama-hogged GPU state where this was developed (2.8 GB free of 24 GB): - Before the gate: 898 pass / 18 fail (all `LlamaCpp Integration > rerank ...`-style failures from contested VRAM). - After the gate: 855 pass / 82 skip / 0 fail. The integration suites skip with a single-line warning instead of producing a wall of unrelated red. `CI=true` and `QMD_REMOTE_URL=...` continue to short-circuit before the probe, so CI and remote-server runs are unchanged. - `test/_helpers/vram-precondition.ts` helper that probes free GPU VRAM and gates the GPU-dependent integration test suites when headroom is insufficient. - `QMD_SKIP_GPU_INTEGRATION=1` env var to force-skip the integration suites without probing the GPU. - Integration suite gates in `test/llm.test.ts`, `test/mcp.test.ts`, `test/store.test.ts`, `test/eval.test.ts`, and `test/sdk.test.ts` now include the VRAM precondition alongside the existing `CI` and `QMD_REMOTE_URL` checks.
… suites Extends the VRAM precondition helper with an opt-in eviction step. When the initial probe shows insufficient free VRAM AND `QMD_TEST_EVACUATE_VRAM=1` is set, the helper asks Ollama to unload its currently resident models, re-probes once, and runs the integration suites if the eviction freed enough headroom. Off by default. Test infrastructure silently terminating another process's loaded models would be astonishing without explicit consent, even on a contested dev box. Operators who want the integration suites to power through a stuck-Ollama state opt into the behavior with a single env-var. Scope of eviction: - Ollama only. The helper hits `GET /api/ps` to list loaded models, then issues `POST /api/generate` with `keep_alive: 0` for each one. The `OLLAMA_HOST` env var overrides the default `http://127.0.0.1:11434` endpoint. - qmd-serve and gbrain are deliberately out of scope. Killing a user-facing daemon is too astonishing even behind the opt-in flag; if a stale qmd-serve is holding VRAM, the operator stops it manually. All steps run best-effort with explicit timeouts (2s for `/api/ps`, 5s per unload). Connection failures, non-OK responses, and per-model unload errors are logged through the same `log` channel as the rest of the helper. A throwing eviction step still triggers the re-probe so a partial recovery (some models unloaded before the failure) still helps. The dependency on the network call is injected through `options.evacuate` so the unit tests of the helper drive the env-gating, sequencing, and best-effort semantics without making real HTTP requests. One test exercises the real default eviction path by pointing `OLLAMA_HOST` at a port nothing listens on, asserting that the connection failure is swallowed and the re-probe still runs. Resolution order in the helper now: 1. Cached result if present. 2. `QMD_SKIP_GPU_INTEGRATION=1` short-circuits to false without probing or evicting. This env var wins over `QMD_TEST_EVACUATE_VRAM=1` so an operator can override a stale opt-in. 3. First VRAM probe. If sufficient, return true. 4. `QMD_TEST_EVACUATE_VRAM=1` → run eviction, re-probe once, return the second probe's result. 5. Otherwise return false with the loud skip warning. The skip message now mentions both env vars so the operator can pick the response that matches the situation. Test coverage added to `test/vram-precondition.test.ts` (eight new cases on top of the existing twelve): - Eviction does not run when `QMD_TEST_EVACUATE_VRAM` is unset, regardless of probe result. - Eviction runs and the re-probe succeeds → returns true, both probes counted. - Eviction runs but the re-probe is still insufficient → returns false, post-eviction message logged. - Eviction throws → re-probe still runs (best-effort), the throw is logged. - First probe already sufficient → eviction is not called even when the env var is set. - `QMD_SKIP_GPU_INTEGRATION=1` precedence wins over `QMD_TEST_EVACUATE_VRAM=1`. - Red-team: env values other than the exact string `"1"` (`"0"`, `"false"`, `"true"`, `"yes"`, `""`) do not trigger eviction. - Default eviction path against an unreachable Ollama logs the connection failure and the re-probe still runs. Twenty tests pass on the file. The existing skip-gate wiring across `test/llm.test.ts`, `test/mcp.test.ts`, `test/store.test.ts`, `test/eval.test.ts`, and `test/sdk.test.ts` is unchanged: the helper's public surface is the same `hasSufficientFreeVramForIntegration()` returning a boolean. Suites that already gated on it now transparently benefit from the eviction option. ## Changelog ### Added - `QMD_TEST_EVACUATE_VRAM=1` opts the VRAM-precondition helper into asking Ollama to unload its resident models when the initial free-VRAM probe shows insufficient headroom, then re-probing. Off by default to avoid side effects on user-facing daemons. - `OLLAMA_HOST` env var overrides the default `http://127.0.0.1:11434` endpoint used by the eviction step. ### Changed - The skip warning emitted by the VRAM precondition helper now lists both `QMD_TEST_EVACUATE_VRAM=1` and `QMD_SKIP_GPU_INTEGRATION=1` as available operator responses.
…AMA_HOST env The first `describe` block of `test/vram-precondition.test.ts` cleared `QMD_SKIP_GPU_INTEGRATION` in its `beforeEach` but not `QMD_TEST_EVACUATE_VRAM` or `OLLAMA_HOST`. When the surrounding process set `QMD_TEST_EVACUATE_VRAM=1` (which is how an operator would exercise the eviction path against a real contested GPU), the helper's default `defaultEvacuate` fired against the actual Ollama daemon inside tests that expected to assert the skip-only path: - `returns false on the Ollama-hogged scenario (~1.4 GB free)` injected a low-VRAM probe and expected the result to be `false`. With the leaked env var, the post-probe code path triggered the real eviction, freed VRAM at Ollama, then re-probed against the test's injected `getLlama`. The same injected probe still returned 1.4 GB free on the second call, so the helper surfaced eviction-related warnings the test did not expect to count. - `only logs the skip message once across calls` asserted exactly one log message; the leaked env path emits additional eviction-attempt log lines, breaking the count. Same risk applies to `OLLAMA_HOST`: a leaked override would point the default eviction step at the wrong endpoint and silently change what the helper observed. The fix is symmetric isolation. Both `describe` blocks now snapshot and clear `QMD_SKIP_GPU_INTEGRATION`, `QMD_TEST_EVACUATE_VRAM`, and `OLLAMA_HOST` in `beforeEach`, then restore in `afterEach`. The second block already cleared the first two; this adds `OLLAMA_HOST` symmetry. The first block adds all three. The helper itself is unchanged. The bug was purely in how the test harness handled inherited environment. Verified by running the helper suite under three env states: - No env vars set: 20 pass / 0 fail. - `QMD_TEST_EVACUATE_VRAM=1`: 20 pass / 0 fail (previously 18 pass / 2 fail). - `QMD_SKIP_GPU_INTEGRATION=1`: 20 pass / 0 fail. ## Changelog (none: test-only fix, internal isolation)
brettdavies
force-pushed
the
feat/test-vram-precondition
branch
from
June 24, 2026 16:01
95b6e6c to
8f558b9
Compare
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.
VRAM precondition gate for GPU-dependent integration suites
Why
The GPU-dependent integration sub-suites in
llm.test.ts,mcp.test.ts,store.test.ts,eval.test.ts, andsdk.test.tsload real GGUF models and exercise embed, rerank, and expandQuery against a realnode-llama-cppruntime. When another workload is holding most of the GPU (Ollama parking a large model, a sibling daemon mid-job, a staleqmd serve), those allocations fail and the suites produce a cascade ofFailed to create any rerank contextfailures that look like code regressions but are not.The existing
describe.skipIf(!!process.env.CI)gates handle CI correctly but leave a dev box exposed: the suites run, fail loudly, and obscure the genuine regressions the suite is supposed to catch. Asking developers to chase ghost failures every time a co-resident model is loaded is the wrong default.This PR adds a one-shot VRAM probe at module load and gates ten integration sub-suites that perform real allocations on whether the GPU has enough headroom. Suites that do not need a real LLM (BM25 search, MCP HTTP transport, path utilities, store creation, chunking metadata) continue to run unconditionally.
What this PR adds
A helper at
test/_helpers/vram-precondition.ts, wired into the existingdescribe.skipIfcascades:test/llm.test.tsLlamaCpp Integrationtest/llm.test.tsLLM Session Managementtest/mcp.test.tssearchVec (vector similarity)test/mcp.test.tshybridQuery (expansion + reranking)expandQueryandreranktest/mcp.test.tsMCP HTTP Transporttest/store.test.tsToken-based Chunkingtest/store.test.tsLlamaCpp Integrationtest/eval.test.tsVector Searchtest/eval.test.tsHybrid Search (RRF)test/sdk.test.tswith LLM query expansionexpandQueryThe helper decides whether to skip:
QMD_SKIP_GPU_INTEGRATION=1short-circuits to false without probing. Useful for fast unit-loop iteration on a healthy box when you just want the non-GPU tests.getLlama().getVramState(). Threshold is 4 GB free, which covers embed (~0.7 GB) + rerank (~1.5 GB) + generate (~1.5 GB) + slack. CPU mode (no GPU) returns true without probing.QMD_TEST_EVACUATE_VRAM=1is set, the helper asks Ollama to unload its resident models viaPOST /api/generate {keep_alive: 0}for each model listed byGET /api/ps, then re-probes once. Off by default: test infrastructure silently terminating another process's loaded models would be astonishing without explicit consent.OLLAMA_HOSToverrides the defaulthttp://127.0.0.1:11434endpoint.The eviction step is Ollama-only. Other VRAM consumers (qmd serve, sibling daemons) are deliberately out of scope: killing a user-facing daemon is too astonishing even behind the opt-in flag. All steps run best-effort with explicit timeouts (2 s for
/api/ps, 5 s per unload). Connection failures, non-OK responses, and per-model unload errors are logged but do not abort the re-probe.Dependencies on
node-llama-cppand the eviction HTTP calls are injected throughoptions.getLlamaandoptions.evacuateso the unit tests attest/vram-precondition.test.tsdrive the gate without making real GPU calls or HTTP requests.Behavior on the merged result
CI=1)node-llama-cppnot builtQMD_SKIP_GPU_INTEGRATION=1QMD_TEST_EVACUATE_VRAM=1, eviction frees enoughQMD_TEST_EVACUATE_VRAM=1, eviction insufficientMeasured locally
Same contested GPU for both runs (RTX 3090 Ti, Ollama holding ~20 GB of gemma4:26b, ~3.1 GB free).
Default (skip path, no opt-in)
The probe sees 2.81 GB free, prints the operator-readable warning, and skips the gated suites.
The eight integration failures previously surfaced as
Failed to create any rerank contextcascades are now clean skips with a single warning line:Evacuate path (
QMD_TEST_EVACUATE_VRAM=1)The probe sees 2.81 GB free, hits the eviction branch, logs the eviction attempt, and asks Ollama to unload its loaded model. Re-probe shows headroom restored and the gated suites then run.
Whole suite (29 files):
876 pass / 80 skip / 0 failagainst the gated suites.Two caveats worth knowing:
rerank contextallocation failures on a baretobi:maincheckout because the vanilla single-pass reclaim path is the weak link, not the headroom this gate restores. This is the same failure mode that feat(llm): --low-vram mode with on-demand reclaim on embed VRAM pressure #662 (lowVram two-pass reclaim) is designed to fix. Stacking feat(llm): --low-vram mode with on-demand reclaim on embed VRAM pressure #662 on top of this PR turns the post-eviction integration suite green; this PR alone restores the headroom but does not change reclaim behavior.ollama run, a residualkeep_alivefrom a recent request) re-requests the same model during the suite run. The gate is a probe-once mechanism; it does not pin Ollama into a quiet state for the duration of the suite.The eviction path is most useful on developer machines where the operator opts in deliberately and expects the test session to take priority over background Ollama state for a few minutes.
Test coverage
19 unit tests on the helper (
test/vram-precondition.test.ts), all driven through injectedgetLlamaandevacuateso no real GPU or HTTP request runs in this suite:QMD_SKIP_GPU_INTEGRATION=1forces a skip without probing.getLlamaandgetVramStatethrowing both fail open.QMD_TEST_EVACUATE_VRAMis unset.QMD_TEST_EVACUATE_VRAM=1and first probe insufficient, then re-probes.QMD_SKIP_GPU_INTEGRATION=1precedence wins overQMD_TEST_EVACUATE_VRAM=1."1"("0","false","true","yes","") do not trigger skip or eviction.The helper test harness explicitly snapshots and clears
QMD_SKIP_GPU_INTEGRATION,QMD_TEST_EVACUATE_VRAM, andOLLAMA_HOSTinbeforeEach/afterEach, so the helper suite passes identically under each of(no env vars),QMD_TEST_EVACUATE_VRAM=1, andQMD_SKIP_GPU_INTEGRATION=1. Without that isolation the tests would inherit the operator's chosen env at the surrounding process level and false-fail.tsc --noEmitclean on the helper and its tests.Backwards compatibility
describe.skipIf(!!process.env.CI)keep theCIshort-circuit and add the VRAM precondition via boolean-OR.CI=1short-circuits before the probe matters.QMD_SKIP_GPU_INTEGRATION,QMD_TEST_EVACUATE_VRAM) are opt-in and read precisely (only the string"1"triggers).OLLAMA_HOSTis read only when eviction is opted into.Changelog
Added
test/_helpers/vram-precondition.tsthat probes free GPU VRAM once per process and reports whether the integration suites can run meaningfully. Gates ten GPU-dependent sub-suites acrosstest/llm.test.ts,test/mcp.test.ts,test/store.test.ts,test/eval.test.ts, andtest/sdk.test.ts.QMD_SKIP_GPU_INTEGRATION=1env var forces the integration suites to skip without probing the GPU.QMD_TEST_EVACUATE_VRAM=1env var opts into asking Ollama to unload its resident models when the initial VRAM probe shows insufficient headroom, then re-probing. Off by default.OLLAMA_HOSTenv var overrides the defaulthttp://127.0.0.1:11434endpoint used by the eviction step.Type of Change
test: Adding or updating testsfeat: New featureBREAKING CHANGE: Breaking API changeRelated Issues/Stories
Testing
test/vram-precondition.test.ts, 19 cases)Files Modified
Modified:
test/llm.test.ts: VRAM precondition import + probe + skipIf extension on two integration sub-suites.test/mcp.test.ts: same on three integration sub-suites.test/store.test.ts: same on two integration sub-suites.test/eval.test.ts: same on two integration sub-suites.test/sdk.test.ts: same on one integration sub-suite.Created:
test/_helpers/vram-precondition.ts: the helper itself.test/vram-precondition.test.ts: unit tests for the helper (19 cases including red-team).Renamed:
Deleted:
Breaking Changes
Deployment Notes