feat(llm): --low-vram mode with on-demand reclaim on embed VRAM pressure - #662
brettdavies wants to merge 9 commits into
Conversation
c9d6c27 to
29073bc
Compare
--low-vram mode for memory-constrained GPUs--low-vram to share a GPU with another large model
|
Hi @tobi, friendly nudge on this one when you have a review cycle. Single opt-in flag with no behaviour change in the default path: The win is concrete and matches the case in the body: on a 24 GB GPU sharing space with a ~20 GB Ollama, Rebased onto current Happy to rename the flag (the body's open question on |
ee99ba4 to
837100e
Compare
dbf013a to
837100e
Compare
--low-vram to share a GPU with another large model13c9ed5 to
57edb82
Compare
57edb82 to
2331f38
Compare
Adds `lowVram` to `LlamaCppConfig` (also `--low-vram` CLI flag and `QMD_LOW_VRAM=1` env). When enabled, the heavy generate (~2 GB) and rerank (~2.3 GB) models are disposed immediately after each use, while the tiny embed model (~320 MB) stays resident. Peak VRAM drops from ~5.4 GB to ~2.6 GB at the cost of per-stage load latency (~3 s → ~5.6 s on a typical GPU). This makes qmd usable on GPUs where loading all three models at once exhausts free VRAM — for example, sharing a 24 GB GPU with a ~20 GB Ollama instance. Addresses the failure mode tracked in tobi#275 across all entry points that construct an LlamaCpp instance: `qmd query`, `qmd mcp`, and the upcoming `qmd serve`. The pipeline stages (expand → embed → search → rerank) are inherently sequential, so disposing between them only costs reload time, not correctness. Concurrency: when lowVram is on, expandQuery and rerank calls serialize through per-method promise chains so a dispose can never race with another caller's in-flight use of the same model. embed and embedBatch remain parallel. The two chains are independent — expand and rerank against their separate heavy models can run in parallel against each other. The flag is global (`--low-vram` works on any subcommand that constructs an LlamaCpp), so qmd query, qmd mcp, and other one-shot commands all benefit — not just long-lived daemons. Naming follows the existing engine-knob pattern (QMD_FORCE_CPU, QMD_LLAMA_GPU).
In lowVram mode, wrap LlamaCpp.embed and embedBatch with a catch-and-retry path: when the underlying ensureEmbedContext(s) fails because node-llama-cpp pre-allocation refused for lack of VRAM, drain the generate and rerank chains (so any in-flight expand/rerank completes its own finally-dispose), defensively dispose both heavy models, and retry the operation once. Outside lowVram mode this is a pass-through, so default-path callers see zero overhead. isInsufficientVramError matches node-llama-cpp's 'too large for the available VRAM' string, our own 'Failed to create any (embedding|rerank) context' wrapper messages, and the bare 'out of memory' / 'insufficient VRAM' variants. Conservative on purpose: too broad and we'd evict on unrelated failures, too narrow and we'd miss the case the feature targets. The chain-drain via Promise.allSettled is what makes the dispose race-free against in-flight expand/rerank callers: by the time their chain settles, each caller has already run its own finally-dispose, so our subsequent disposeGenerateModel / disposeRerankModel calls land on null models and are no-ops. The retry then succeeds because the embed context can allocate into the freed VRAM. 5 new tests in test/llm-low-vram.test.ts cover: embed and embedBatch recover from VRAM error and return correct results, non-VRAM errors do not trigger eviction, lowVram=false skips the reclaim path entirely, and reclaim awaits in-flight generate/rerank chains before disposing.
Generalize embedWithReclaim into a single withReclaim(needs, fn) helper, then wire it into the expandQuery and rerank lowVram chain handlers. The 'needs' argument names which model the operation requires (so it's never disposed) and identifies which OTHER chain to drain. expandQuery uses needs='generate' (drains rerankChain, disposes rerank); rerank uses needs='rerank' (drains generateChain, disposes generate); embed continues to use needs='embed' (drains both, disposes both). The needs discrimination also prevents a self-await deadlock, because a caller inside generateChain cannot await generateChain without blocking on its own current link. By restricting drains/disposes to the OTHER chains, the reclaim path is always safe. Three new tests in test/llm-low-vram.test.ts: expandQuery retries after VRAM error and disposes rerank (not generate); rerank retries after VRAM error and disposes generate (not rerank); expand reclaim awaits an in-flight rerank chain before disposing rerank. Existing 11 lowVram tests still pass. Practical impact: a qmd query that previously failed with 'A context size of 2048 is too large for the available VRAM' on the expand stage now recovers if the only thing standing between it and its budget is a resident rerank model (or vice versa). External VRAM contention (e.g., a co-resident Ollama eating the budget) is still unrecoverable: that's the next layer the maintainers may want.
…ror strings Adds a tripwire test suite for `isInsufficientVramError`, the function that gates the two-pass reclaim path in `withReclaim`. The regex catalog inside the function has to match the real error strings that node-llama-cpp emits under VRAM pressure or the reclaim never fires when it should, and the user just gets a raw `Failed to create any rerank context` instead of a recovered query. Exports the function (was previously module-local) and pins it against two curated lists of strings. Positives that must match: | Error string | Source | | --- | --- | | `A context size of N is too large for the available VRAM` | `node-llama-cpp/dist/evaluator/LlamaContext/LlamaContext.js:30` single-sequence `InsufficientMemoryError` template | | `A context size of N with M sequences is too large for the available VRAM` | Same template, multi-sequence form | | `Failed to create any (rerank\|embedding) context` | Synthetic strings this codebase throws when the inner allocation loops give up | | `CUDA error: out of memory` | Generic CUDA OOM | | `ggml_cuda_compute_forward: out of memory` | GGML OOM under load | | `ran out of memory while allocating buffer` | Lowercase variant | | `Insufficient VRAM to load the model` | Safety-check variant | Negatives that must NOT match. False positives would chew through retries on errors the reclaim cannot fix: | Error string | Why it must not match | | --- | --- | | `model file not found` | IO error, not VRAM | | `Computing rankings is not supported for this model.` | Wrong architecture for the call | | `fetch failed: connect ECONNREFUSED ...` | Network error during model download | | `Invalid GGUF magic bytes` | File corruption | | `Something else went wrong` | Generic runtime error with no memory-related keywords | | `Loaded model into memory successfully` | Mentions "memory" but is not OOM (red-team: substring greediness) | | `VRAM usage: 12.3 GB / 24 GB` | Mentions "VRAM" but is a status line (red-team) | Input-shape tests round out the suite. The function must handle null, undefined, empty string, a bare string with a positive message, an arbitrary object that string-coerces to a positive message, and case-insensitivity across the catalog. Adding a positive case here that fails before updating the regex is the preferred way to fix a "reclaim didn't fire when it should have" report. The failing test pins the behavior, the regex update makes it pass, and future regressions are caught. Twenty-one tests pass on the new file. ## Changelog ### Added - `isInsufficientVramError` is now exported from `src/llm.ts` so the reclaim trigger function can be unit-tested directly against curated error strings. - `test/llm-vram-error-pinning.test.ts` pins the reclaim trigger against a sourced list of real node-llama-cpp error messages plus codebase-internal synthetic strings, and against a red-team list of similar-looking errors that must not trigger.
Hardens lowVram mode's `withReclaim` for cards where shared workloads (Ollama, gbrain, etc.) leave qmd only a few GB of headroom. The previous single-pass reclaim only freed generate and rerank. Neither helped when the rerank context allocation itself was the failing call and the embed model was the next-largest qmd-side consumer. The new flow on a non-embed reclaim: 1. First pass: drain the other chains, dispose generate model, dispose rerank model, dispose embed *contexts* (~143 MB × N). Retry once. 2. Second pass: if the retry also hits a VRAM error, drop the embed *model* (~568 MB). Retry once more. 3. Embed reclaim short-circuits the second pass because it cannot make progress without the model. Each dispose runs best-effort: a failure is logged with the dispose label and the remaining disposes still run, so a single broken dispose does not poison the retry. The original VRAM error is more useful than a downstream dispose error, and a retry against partial reclaim is still more likely to succeed than no retry at all. Also removes a misleading "retry without flash attention" branch in `ensureRerankContexts`: both `createRankingContext` calls passed identical options, and `LlamaRankingContextOptions` does not accept `flashAttention` (the option is silently dropped at `LlamaRankingContext.js:160-168`). The synthetic `Failed to create any rerank context` message is preserved so `isInsufficientVramError` continues to recognize it. Worst-case attempt count is capped at 3 (initial + first-pass retry + second-pass retry). Test coverage (`test/llm-low-vram.test.ts`): - First-pass reclaim symmetry for `embed`, `expandQuery`, `rerank` (existing tests updated for new dispose:embed-contexts event). - Second-pass escalation for `rerank` and `expandQuery` (3-attempt success path). - Second-pass cap for `rerank` and `expandQuery` (3-attempt give-up path). - `embed` reclaim never disposes embed contexts or model under any retry pattern. - Reclaim continues retry when a dispose helper itself throws (best-effort, warning logged). - Reclaim ignores non-VRAM errors (no dispose, no retry). - Reclaim treats a post-first-pass non-VRAM error as terminal (no escalation). 23 tests pass on the file. Wider suite delta is unchanged (the remaining 19 wider failures are pre-existing GPU-contention integration tests; addressed separately). - Low-VRAM reclaim now escalates to disposing the embed model when an initial dispose pass plus retry still hit a VRAM error, with all dispose calls running best-effort so a single failing dispose does not abort the retry. - Removed a no-op "retry without flash attention" fallback in `ensureRerankContexts` whose retry passed identical options to the failing call and whose `flashAttention` flag is silently dropped by `node-llama-cpp`'s `createRankingContext`.
Picks the rerank context size based on free VRAM at allocation time and budgets `computeParallelism` against the actual chosen size. The previous code allocated a fixed 4096-token context (~1146 MB with flash attention) and budgeted parallelism at a hardcoded 1000 MB/context, which both over-allocated on contested cards and skewed the parallelism estimate. Resolution order in `resolveRerankContextSize`: 1. `QMD_RERANK_CONTEXT_SIZE` env override (read at call time, not class-load) always wins. Falls back to adaptive logic on missing / non-positive / non-numeric values. 2. CPU mode (offload forced or no GPU) keeps the 4096 default. CPU RAM is plentiful and the per-token cost model does not apply. 3. GPU probe via `getVramState`. Free VRAM below 1500 MB shrinks to 2048; at or above the threshold stays at 4096. A probe failure conservatively falls back to 4096, where the caller's existing retry path will catch a too-tight allocation. Threshold derivation: the 4096-token rerank context with flash attention costs roughly 1.15 GB. The 1500 MB threshold reserves the context plus ~350 MB margin for the rerank model and concurrent allocations. The per-token cost model (0.28 MB/token) was measured against real loads: 4096 ≈ 1146 MB, 2048 ≈ 568 MB. The chosen size is stored on the instance as `rerankContextSize` so `rerankImpl` truncates documents against the same size that was actually allocated, never a static guess that would under-truncate when the context was shrunk. The field resets to `null` in `disposeRerankModel`, `unloadIdleResources`, and `dispose`, so a re-allocation re-probes VRAM. `computeParallelism` now receives `Math.ceil(contextSize * 0.28)` as the per-context MB estimate. At the 4096 default this is roughly 1146 MB (was 1000 MB hardcoded); at the 2048 fallback it is roughly 573 MB. Parallelism scales accordingly. Test coverage (`test/llm-adaptive-rerank.test.ts`): - Env override returns user-supplied size regardless of VRAM, including when VRAM is plentiful. - Invalid env override (0, negative, non-numeric, empty) falls back to adaptive logic. - CPU mode and CPU-offload-forced skip the VRAM probe entirely. - Plentiful VRAM returns 4096; exactly at threshold returns 4096; just below returns 2048. - Ollama-hogged GPU scenario (~1.4 GB free) returns 2048. - `getVramState` throwing falls back to 4096. - `ensureRerankContexts` stores the resolved size on `rerankContextSize`. - `computeParallelism` budget reflects the actual chosen size, not the old 1000 MB hardcode. - `disposeRerankModel` and `dispose` clear `rerankContextSize` so the next call re-probes. 18 tests pass on the new file; pre-existing lowVram concurrency tests on this branch are unaffected. - Rerank context size now adapts to free VRAM at allocation time, picking 2048 instead of the 4096 default when free VRAM drops below ~1.5 GB. The `QMD_RERANK_CONTEXT_SIZE` env override still wins when set. - Parallelism for rerank context creation is now budgeted against the actual chosen context size instead of a fixed 1000 MB-per-context estimate that assumed the older 2048 default. - Long-document truncation in `rerankImpl` now uses the actually allocated context size, not a static constant, so documents are never under-truncated when the context was shrunk under VRAM pressure.
…path Adds an end-to-end test that drives the two-pass reclaim against a real `node-llama-cpp` runtime instead of monkey-patched fakes. The synthetic tests in `llm-low-vram.test.ts` prove the orchestration of `withReclaim`. This file fills the gap they leave: - The real dispose helpers actually run against a live model and contexts. - The actual `InsufficientMemoryError` thrown by `node-llama-cpp` exercises the live `isInsufficientVramError` regex (B6 pins it string-by-string, this throws it). - The three-attempt cap holds against a real failing allocation, not just a synthetic throw. The recovery path (reclaim disposes other models and the retry succeeds) needs precise VRAM ballast and tear-down that is hard to make deterministic on a shared GPU, so it stays covered by the synthetic suite. Gated behind `QMD_RECLAIM_INTEGRATION=1`. The tests are skipped by default because: - They download the rerank model on first run (one-off, can take minutes). - They exercise real GPU allocation, so CI and remote-server-routed runs cannot meaningfully execute them. - They pair naturally with B3's VRAM precondition: the suite does not care about free VRAM, it only needs enough headroom to load the rerank model (~400 MB). Two cases: 1. **Oversized rerank context → reclaim gives up cleanly after 3 attempts.** Reflectively swap the static `RERANK_CONTEXT_SIZE` to 1 048 576 tokens (~290 GB context). The static is captured at class load, so `process.env` mutation alone would have no effect; the reflective swap is the only honest way to force a real allocation that no GPU can satisfy. Spy on `rerankImpl` to count attempts (wrap, do not replace, so the real implementation runs). Verify the call rejects, the spy ran exactly three times, and the surfaced error is recognized by `isInsufficientVramError`. If that last assertion ever fails, the lib reworded its error and the regex needs a new alternative — add a positive case to `llm-vram-error-pinning.test.ts` first, then update the regex. 2. **Non-VRAM rerank failure does not retry (missing model file).** Build a `LlamaCpp` pointed at `/nonexistent/path/to/no-such-model.gguf`, attempt rerank, verify exactly one attempt was made and the error is NOT classified as VRAM. Catches the inverse failure: if `isInsufficientVramError` ever started matching non-VRAM errors, the reclaim would chew through retries for nothing and bury the real diagnostic. Each test owns a fresh `LlamaCpp` and disposes it in `finally` so a failure cannot leak GPU resources into the next test. Verified locally: ``` QMD_RECLAIM_INTEGRATION=1 bun test --preload ./src/test-preload.ts test/llm-reclaim-integration.test.ts 2 pass / 0 fail / 6 expect() calls in 794ms ``` (Models pre-cached on the dev box; first-run download would dominate the timing.) ## Changelog ### Added - `test/llm-reclaim-integration.test.ts` real-GPU tests for the two-pass reclaim path, gated behind `QMD_RECLAIM_INTEGRATION=1` so they do not run in CI or remote-LLM-routed environments. Covers the give-up branch (oversized context forces real allocation failure, asserts the 3-attempt cap and `isInsufficientVramError` classification) and the no-retry branch (missing model file must not be misclassified as VRAM).
The low-vram tests monkey-patch the *Impl methods to exercise the reclaim and serialization wrappers without loading real models. Two ambient env vars distort them: CI trips LlamaCpp's _ciMode guard, which throws before the wrapper runs, and a leaked QMD_LOW_VRAM=1 flips the lowVram=false cases to true. Neutralize both in beforeAll and restore in afterAll so the suite is deterministic regardless of environment.
2331f38 to
9af2ed0
Compare
|
Rebased onto current
Everything else rebased clean, including onto main's node-llama-cpp 3.20 bump and the async sequence-dispose changes. |
The oxlint fence on main (anti-slop/no-chained-type-assertions) rejects the `x as unknown as T` chains the low-vram suites used to reach private members. A widen() helper moves the unknown-widening to the test boundary, so each site narrows with one assertion and keeps its precise structural type.
--low-vram: share a GPU with another large modelWhy
qmd query(and any other path that runs the full pipeline) loads three GGUF models into a single process: embed (~320 MB), generate (~2 GB), rerank (~2.3 GB). Once all three are resident, peak VRAM sits around 5.4 GB. On a GPU that's already shared with another model (say a 24 GB card running a ~20 GB Ollama), there isn't enough free VRAM left, and rerank context creation fails withFailed to create any rerank context. That's the failure mode tracked in #275 (closed during the v2.5.1 backlog cleanup; reproducible on hardware ranging from a 2 GB GTX 960M to a 6 GB RTX 3060 per reporters there, plus the 24 GB / Ollama-coexistence case below).The three pipeline stages (expand → embed → search → rerank) are inherently sequential, so the win is straightforward: keep the tiny embed model resident, dispose the heavy generate and rerank models after each use, reload them on demand. Peak drops to ~2.6 GB. The cost is per-stage load latency.
Measured on an RTX 3090 Ti running alongside Ollama Gemma 4 26B (~20.3 GB VRAM):
qmd query(cold)qmd query(warm, default)qmd query --low-vram22K-file collection, hybrid query, full pipeline (expand + vec + rerank).
What this PR does
Adds
lowVramtoLlamaCppConfig, surfaced as a global--low-vramCLI flag (any subcommand) and aQMD_LOW_VRAM=1env var. Naming follows the existing engine-knob pattern (QMD_FORCE_CPU,QMD_LLAMA_GPU).When enabled,
LlamaCpp:finallyblock after eachexpandQuerycall.finallyblock after eachrerankcall.expandQuerycalls through an internal promise chain so adisposecall can never race with another caller's in-flight use of the same model. Same forrerank.embedandembedBatchrunning in parallel as before: the embed model stays resident.getVramState(). Free VRAM at or above 1.5 GB keeps the 4096-token default; below that shrinks to 2048 tokens (~568 MB allocation cost instead of ~1146 MB). TheQMD_RERANK_CONTEXT_SIZEenv override still wins when set. CPU mode keeps the 4096 default. Document truncation inrerankImpluses the actually allocated size, not a static constant, so chunks are never under-truncated when the context was shrunk under pressure.embed,expandQuery, orrerankfails with an insufficient-VRAM error fromnode-llama-cpp, the wrapper drains the unrelated chains, disposes everything that isn't needed for the failing operation (generate model, rerank model, embed contexts), and retries once. If the retry also fails for VRAM AND the failing operation is not embed, a second pass drops the embed model itself (~568 MB) and retries one more time. OutsidelowVrammode this is a pass-through, so default-path callers see zero overhead. Detail in the reclaim section below.The two chains are independent:
expandQueryandrerankagainst their separate heavy models can run in parallel against each other.Because the flag is global and the constructor reads
QMD_LOW_VRAM, it works everywhereLlamaCppis constructed (qmd query,qmd embed,qmd mcp --http --daemon,qmd vsearch, etc.) without per-subcommand plumbing.Two-pass reclaim
The reclaim path addresses the case where the catch-after-throw approach by itself does not free enough VRAM in one shot. The original observation that the lib's own pre-allocation VRAM check (
InsufficientMemoryError: "A context size of N is too large for the available VRAM") is the right signal to react to still applies — we do not race to predict it. What is new is what we do after catching it.First pass, on any insufficient-VRAM error:
finally-dispose has run.expandQuerycallers), rerank (for non-rerankcallers).If the retry also fails with an insufficient-VRAM error AND the failing operation is not embed, a second pass evicts the embed model itself (~568 MB) and retries one final time. Embed reclaim short-circuits the second pass — it cannot make progress without the model resident. Worst-case attempt count is capped at three (initial + first-pass retry + second-pass retry); no infinite retry loop is possible.
Each dispose runs best-effort: a failure is logged with the dispose label, but the remaining disposes still run. A retry against a partial reclaim is still more likely to succeed than no retry at all, and the original VRAM error is more useful than a downstream dispose error.
Per-context VRAM cost model in
computeParallelismnow derives from the actually chosen rerank context size (~0.28 MB/token, measured at both 2048 and 4096) instead of the previous 1000 MB hardcode that assumed 2048. Parallelism estimates scale correctly across both sizes.isInsufficientVramErrormatches node-llama-cpp's"too large for the available VRAM"template (single-sequence and multi-sequence variants), our own"Failed to create any (embedding|rerank) context"wrapper messages, generic"out of memory"(CUDA / GGML), and"insufficient VRAM"safety-check variants. Conservative on purpose — too broad and we'd evict on unrelated failures; too narrow and we'd miss the case the feature targets. Pinned against a curated list of real lib strings plus a red-team negative list (model-not-found, unsupported-architecture, network errors, substring-greediness cases) so a future lib message change is caught as a unit-test failure before it ships.Test coverage
test/llm-low-vram.test.tstest/llm-adaptive-rerank.test.tsrerankImpluses the chosen size,computeParallelismbudget reflects the chosen size, lifecycle reset on dispose.test/llm-vram-error-pinning.test.tsInsufficientMemoryErrortemplate variants, synthetic codebase strings, CUDA / GGML OOM), red-team negatives that must NOT match (model-not-found, wrong-architecture, network errors, substring greediness), input-shape handling.test/llm-reclaim-integration.test.tsQMD_RECLAIM_INTEGRATION=1). Oversized context (1 M tokens) forces a real allocation failure, asserts the 3-attempt cap holds andisInsufficientVramErrorclassifies the liveInsufficientMemoryError. Missing-model-file case asserts non-VRAM failures do not retry.The integration test is gated because it downloads the rerank model on first run and exercises real GPU allocation; it cannot meaningfully execute in CI. All synthetic tests run unconditionally.
Architecture
Changelog
Added
--low-vramglobal flag andQMD_LOW_VRAM=1env var. Disposes the generate and rerank models after each use while keeping the embed model resident. Peak VRAM drops from ~5.4 GB to ~2.6 GB at the cost of per-stage load latency. Intended for shared GPUs (Ollama coexistence) and small-VRAM cards where loading all three models at once would fail.embed,expandQuery, orrerankhits an insufficient-VRAM error fromnode-llama-cpp. First pass disposes the other models and the embed contexts; second pass evicts the embed model itself. Worst-case three attempts. Off outside--low-vram.QMD_RERANK_CONTEXT_SIZEenv override still wins.isInsufficientVramErrorexported fromsrc/llm.tsfor direct unit-testing.Changed
computeParallelismbudgets each rerank context against its actual chosen size (~0.28 MB/token) instead of a fixed 1000 MB-per-context estimate. Parallelism scales correctly across both 2048 and 4096 sizes.rerankImplnow uses the actually allocated context size, so chunks are never under-truncated when the context was shrunk under VRAM pressure.Fixed
--low-vrammode, a VRAM failure creating the first rerank context propagates towithReclaim(which evicts heavies and retries) instead of falling into the warn-and-skip path fix(llm): surface the real error when rerank context creation fails #866 added for the default mode. The original error is rethrown as-is;isInsufficientVramErrorrecognizes the underlying node-llama-cpp messages directly (pinned bytest/llm-vram-error-pinning.test.ts). Default-mode behavior is unchanged: reranking still degrades gracefully with a warning.Type of Change
feat: New feature (non-breaking change which adds functionality)fix: Bug fixBREAKING CHANGE: Breaking API changeRelated Issues/Stories
qmd serve): the two merge in either order, and the combined serve+low-vram shape lives in feat: low-vram engine + qmd serve combined (production shape of #662 + #663) #927.Testing
Test Summary:
test/llm-low-vram.test.ts: 23 cases (concurrency, two-pass reclaim, best-effort, non-VRAM termination).test/llm-adaptive-rerank.test.ts: 18 cases (env override, CPU mode, boundary, lifecycle).test/llm-vram-error-pinning.test.ts: 21 cases (positive catalog, red-team negatives, input shapes).test/llm-reclaim-integration.test.ts: 2 opt-in real-GPU cases gated byQMD_RECLAIM_INTEGRATION=1. Verified locally: 2 pass / 0 fail in 794 ms with the rerank model pre-cached.Files Modified
Modified:
src/llm.ts— adaptive context sizing, two-pass reclaim,disposeEmbedContexts/disposeEmbedModel/bestEfforthelpers, dead-flash-attention-retry removal,isInsufficientVramErrorexport,rerankContextSizelifecycle reset on the three dispose sites.Created:
test/llm-adaptive-rerank.test.tstest/llm-vram-error-pinning.test.tstest/llm-reclaim-integration.test.tsRenamed:
Deleted:
Breaking Changes
Deployment Notes