Repository navigation
Enforce explicit VRAM limit at load - #717
Conversation
Derive native CUDA weight offload from explicit VRAM limits and fail non-offload-capable device loads when weights exceed the limit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624
Size derived CUDA weight offload from the explicit VRAM limit after reserving native device KV and recurrent state, and fail at load when the remaining weight budget cannot satisfy the largest lazy-weight working set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624
|
Follow-up after real CUDA E2E correction:
Validation rerun: cargo fmt --all -- --check
cargo test -p onnx-genai-engine pool_sizing_tests --lib --features native-backend --no-default-features
cargo clippy --locked --all-targets -p onnx-genai-engine --features native-backend -- -D warnings
cargo clippy --all-targets -p onnx-genai-engine --features cuda,native-backend -- -D warnings
cargo clippy -p onnx-runtime-ep-cuda --lib --features cuda -- -D warnings
cargo build --release -p onnx-genai-cli --bin onnx-genai --features cuda,native-cuda |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #717 +/- ##
==========================================
- Coverage 80.48% 80.12% -0.37%
==========================================
Files 328 331 +3
Lines 125517 132533 +7016
Branches 125517 132533 +7016
==========================================
+ Hits 101024 106187 +5163
- Misses 20318 22107 +1789
- Partials 4175 4239 +64
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
🔴 Benchmark Regression DetectedComparison of criterion micro-benchmarks: PR head vs merge-base, measured on the same runner in the same job (base first → PR second).
Visual flags: Host infoWhat this cannot catch
|
|
Verified end to end myself on the 14b before merging, on a binary rebuilt from the committed source. Arm A — 4,389,387,264 = 6,000,000,000 − 1,610,612,736. The arithmetic is right and the regression is gone. Generation is correct: prompt Arm B — One honest gap, carried not closed: Merging. |
Consolidating today's findings into the document rather than leaving them in issues. ## The new section: a third failure mode `MEMORY_ARCHITECTURE.md` already recorded two ways this area fails — code that is right, tested and never reached; and measurements that are reproducible but support a conclusion they do not license. Today produced a third, and it cost more than either. **An instrument that runs, reports a plausible number, and measures the wrong thing.** `h2d_enqueue_copy_ms` put host-to-device transfer at **1.7%** of decode step time. That figure retired an entire line of work — prefetch depth, pinned versus pageable staging, transfer overlap — and was published on #705 and #718. The arithmetic gives it away: the same step stages **7.33 GiB**, and 7.33 GiB in 30 ms would be ~250 GB/s, roughly ten times this machine's PCIe link. The counter bracketed an asynchronous enqueue *returning*. The first repair renamed it `copy_wait_ms`, pointing it at `compute_wait_fence` — whose own doc comment reads *"a stream-ordered, **non host-blocking** cross-stream wait."* The same non-measurement survived under a name that invited the wrong reading **more** strongly than before. Measured properly with CUDA events, transfer is **18.8%**. Two habits, both cheap: - **Divide the bytes by the time and ask whether the answer is a real bandwidth.** A number near link speed, near memcpy speed, or ten times either tells you what the counter actually bracketed. This is what caught it, and it takes seconds. - **State on every timing counter exactly what lies between start and stop, and whether anything there blocks the host.** Enforced in `weight_paging.rs` since #715. ## The pattern underneath all three Telemetry that never reaches the operator, three times in one subsystem: documented env vars nothing reads (#688), counters computed and never printed (#719), counters printed with nothing writing them (#715). The #719 case is the sharpest. A residency cache paging at a **0% hit rate** printed a weight-offload section in which **every visible row read `0.00`** — and my first reading was that the cache was inert. The opposite was true and provably so: the section printing at all required one of the three *hidden* counters to be non-zero. The instrument reported the exact inverse of what it measured. After a third instance the response should be a guard, not more vigilance. ## Status table rows where today changed the facts - **`--vram-limit`** — was "does not limit VRAM"; now enforced at load, auto-enabling offload with the weight budget derived as `limit − device state`. Caveat carried rather than closed: activations and runtime overhead are still `unknown` and unsubtracted (#514). PR #717 - **Weight residency cache (CUDA)** — new row. It had a **0% hit rate for its entire life**: 6,936 page-ins, 0 hits, at both 3 GB and 6 GB, because `evict_to_fit` is LRU against a cyclic sequential scan. Fixed to **74.18%** against a `B/W ≈ 76%` ceiling, evictions 6,286 → **0**. #720, PR #723 - **Weight offload vs the OS** — the 27x gap now has a named mechanism instead of a mystery, with ~39% of step time placed downstream of that one defect. - **KV page storage** — new row. `onnx-genai-kv` never touched device memory; the contract is now open (`Box<dyn KvPageStore>`), host-only pending stages 2–5. PR #726 - **Captured graphs across a VMM remap** — new row, verified on hardware and sentinel-proven. PR #727 Docs only. Co-authored-by: justinchuby <223556219+Copilot@users.noreply.github.com> Copilot-Session: d60eb808-7cc6-4abc-b48d-2a6dd3841624
## Summary - Adds `serve --vram-limit` / `ONNX_GENAI_VRAM_LIMIT` and applies it to `EngineConfig.limits.vram_limit` before any eager model load. - Keeps a single knob and single ledger: no server-private budget; the value flows through the existing engine/resource-governor config that #717 already uses for CUDA auto-offload. - Exposes only process-wide CUDA weight-offload activity metrics (`page_ins_total`, `hits_total`, `evictions_total`, and derived process-wide `hit_rate`). I removed the residency budget/peak gauges because they are not process truth for multi-model servers; canonical VRAM used/limit/headroom remains the engine ledger metrics. I chose the smaller server-side change rather than moving the full CLI `EngineArgs`: `EngineArgs` currently lives in `onnx-genai-cli`, while `ServeArgs` is shared by the standalone server and unified CLI. Moving the full type would be a larger cross-crate refactor; #734's actual divergence is the missing launch-time VRAM limit that feeds load-time offload, and this fixes that without adding another budget or offload-specific flag. Refs #706 Closes #734 ## Acceptance check Built binary verified: ```text C:\Users\justinchu\dev\onnx-genai-706-memory-pressure\target\debug\onnx-genai-server.exe LastWriteTimeUtc 2026-08-07 13:39:19Z ``` Launch: ```powershell .\target\debug\onnx-genai-server.exe --model C:\Users\justinchu\dev\models\qwen14b-zp --addr 127.0.0.1:8134 --native-device cuda --vram-limit 6000000000 ``` Observed expected load-time log: ```text enabled CUDA weight offload because model weights exceed the VRAM limit model_weight_bytes=16653143582 resolved_vram_bytes=6000000000 required_device_non_weight_bytes=1610612736 native_kv_bytes=1610612736 recurrent_state_bytes=0 minimum_useful_weight_budget_bytes=437954560 offload_device_budget_bytes=4389387264 ``` After one request, `/metrics` showed non-zero process-wide offload activity counters: ```text onnx_genai_cuda_weight_offload_page_ins_total 867 onnx_genai_cuda_weight_offload_hits_total 0 onnx_genai_cuda_weight_offload_evictions_total 0 onnx_genai_cuda_weight_offload_hit_rate 0 ``` The non-zero activity counters plus the load log are the offload-engagement proof. The removed residency budget/peak names are intentionally not exported. ## Interleaved sweep Same binary/session, qwen14b-zp, native CUDA, `max_tokens=24`, n=5 per arm, interleaved no-limit vs `--vram-limit 6000000000`. | concurrency | arm | individual total seconds | median | range | offload counters | |---:|---|---:|---:|---:|---| | 1 | no limit | 6.705, 4.841, 4.907, 4.880, 4.906 | 4.906 | 4.841-6.705 | page_ins=0 hits=0 evictions=0 | | 1 | 6GB limit | 39.513, 23.838, 17.515, 18.638, 19.530 | 19.530 | 17.515-39.513 | page_ins=1785 hits=1683 evictions=0 hit_rate=48.53% | | 4 | no limit | 7.824, 6.589, 8.871, 8.607, 10.957 | 8.607 | 6.589-10.957 | page_ins=0 hits=0 evictions=0 | | 4 | 6GB limit | 54.000, 52.079, 71.010, 78.856, 57.951 | 57.951 | 52.079-78.856 | page_ins=5457 hits=8415 evictions=0 hit_rate=60.66% | | 8 | no limit | 11.151, 11.376, 12.463, 73.339, 87.021 | 12.463 | 11.151-87.021 | page_ins=0 hits=0 evictions=0 | | 8 | 6GB limit | 96.150, 96.370, 302.130, 472.220, 471.698 | 302.130 | 96.150-472.220 | page_ins=10353 hits=17391 evictions=0 hit_rate=62.68% | | 16 | no limit | 165.442, 140.933, 138.055, 138.154, 33.480 | 138.154 | 33.480-165.442 | page_ins=0 hits=0 evictions=0 | | 16 | 6GB limit | 805.475, 875.814, 861.425, 916.566, 191.329 | 861.425 | 191.329-916.566 | page_ins=20145 hits=35343 evictions=0 hit_rate=63.69% | The limited/governed arm is slower on this machine. I am not claiming a throughput win; this PR makes the server governed and measurable instead of relying on WDDM demand paging. Ranges remain noisy, and `rejections_total` stayed 0 in completed scrapes, so #706's admission/refusal question remains open. ## Validation - `cargo fmt --all -- --check` - `cargo test --locked -p onnx-genai-server prometheus_does_not_export_non_aggregate_weight_offload_gauges -- --nocapture` - `cargo test --locked -p onnx-genai-server --features "cuda,native-backend" cuda_weight_offload_metrics_are_process_activity_only -- --nocapture` - `cargo test --locked -p onnx-genai-server serve_vram_limit -- --nocapture` - `cargo clippy --locked --all-targets -p onnx-genai-server -- -D warnings` - `cargo clippy --locked --all-targets -p onnx-genai-server --features "cuda,native-backend" -- -D warnings` - `cargo clippy --locked --all-targets -p onnx-genai-engine --features native-backend -- -D warnings` - `cargo build --locked -p onnx-genai-server --features "cuda,native-backend"` Note: `cargo test --locked -p onnx-genai-server --features cuda ...` without `native-backend` still hits the existing engine cfg issue where CUDA load helpers reference native types behind `native-backend`; the CUDA metrics test passes with the server's actual native CUDA feature set. --------- Co-authored-by: justinchuby <223556219+Copilot@users.noreply.github.com>
## Summary - authority-owned weak `ReclaimableMappedHolder` registration and transactional RAII `MappedGrowthGrant` - unused-allowance-first transfer, external reclaim callbacks, grant-bound capacity tokens, rollback, FIFO serialization, and atomic reserved-to-mapped attribution - batched native CUDA KV/workspace growth with exact global granule 0→1 charging and 1→0 refund - one canonical mapped allowance per suballocating VMM arena; content lifetime remains separate from physical zone ownership - explicit byte `serve --vram-limit` selects managed no-spill VMM/pool mode; `ONNX_GENAI_DYNAMIC_KV_WEIGHT_LENDING=0` restores compatibility fallback - typed capacity refusals map to pre-header HTTP 429 with `Retry-After` Refs #736 Refs #717 Refs #745 Refs #747 ## Review fixes - managed VMM initialization failure is fatal before model allocation; compatibility mode alone may fall back - managed physical-pool ownership avoids double-charging resident weights when offload is unnecessary - allocator transactions charge only exact global granule transitions, including packed/shared workspace allocations - CUDA provider centrally refunds the canonical arena allowance on full deallocation and partial range decommit, including when an ordinary buffer drops the final shared reference - specialized workspace/session cleanup cannot omit or double-refund attribution - state-only native models load without KV bytes/token when paged geometry is not required ## Validation - `cargo fmt --all -- --check` - memory governor: 36 passed - session: 135 passed - CUDA EP library: 295 passed, 4 ignored - engine CUDA/native library: 434 passed, 1 ignored - hardware VMM GPU: 16 passed on RTX 4060 Laptop GPU - provider canonical full/partial decommit GPU regression: passed - changed-library clippy with `-D warnings`: passed - changed-package all-target clippy with `-D warnings`: passed ## 14B / 6 GiB E2E Model: `C:\Users\justinchu\dev\models\qwen14b-zp`; release native-CUDA binary. - managed mode logged VMM/pool strategy and registered the weight holder - short `max_tokens=24`: HTTP 200, correct `Paris`; weight allowance 4,831,838,208 bytes; owned 5,534,384,128 <= 6 GiB - first physical KV growth transferred/attributed 201,326,592 bytes - 8K refused pre-header with HTTP 429, never late 500/OOM - four queued short requests completed FIFO; server did not demonstrate simultaneous live KV accumulation - opt-out did not auto-enable VMM/pool and retained compatibility behavior Latest commit: `440a1df4`. PR remains open and unmerged. --------- Co-authored-by: justinchuby <223556219+Copilot@users.noreply.github.com>
Summary
Closes #712. Context: #704.
DeviceOffloadPolicyfrom--vram-limitwhen weights exceed the limit, passes it into the CUDA EP, and disables graph capture through the same resolved policy.ONNX_GENAI_WEIGHT_OFFLOAD_DEVICE_BYTESis preserved as an override for the derived residency budget.Design decisions
Where enforcement lives: engine load. The scheduler governor only knows how to derive KV budget; the allocator sees allocations too late, especially on WDDM spill. Engine load is the first point that knows model package bytes, selected backend, offload capability, and the engine ledger.
Unset
--vram-limit: unchanged. The default remains the existing resource-limit default (0.90of the current provisional VRAM capacity). I did not turn unset into a new hard real-device-capacity refusal because that would silently change WDDM behavior for users who did not ask for a limit, and this tree still uses provisional capacity providers.Auto-offload announcement: yes. Native CUDA emits an info line when explicit VRAM causes automatic offload, with model bytes, resolved VRAM bytes, and offload device budget bytes.
Verification
Passed:
Manual checks with the 14B artifact using the debug native-backend build:
Sabotage check: I broke
device_weight_reservation_forso the offload startup claim did not leave one KV page, then reran the targeted test. It failed as expected:Not completed: the requested CUDA release E2E could not be built on this machine because cudarc generated duplicate cuBLASLt bindings before reaching this change:
Also note:
cargo clippy --all-targets -p onnx-runtime-ep-cuda --features cuda -- -D warningsis blocked by pre-existing CUDA test lint errors (unusual byte grouping, clone-on-copy, too-many-arguments, etc.). The touched CUDA EP library target passes clippy.