Repository navigation
[docs] Policy-based, route-aware workspace estimation design - #32944
Chi Lo (chilo-ms) wants to merge 9 commits into
Conversation
Documents the design for making Level-1 partition-time workspace estimation route-aware and policy-bound, so CPU-offloading placement decisions on VRAM-constrained machines are not driven by a worst-case route (e.g. GQA's quadratic unfused fallback) the runtime almost never executes. Adds docs/annotated_partitioning/policy_based_workspace_estimation.md and cross-references it from future_directions_constrained_env.md (Runtime Temp Buffers gap). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fa98d07-aec3-41b7-92b8-b0ec317a95bf
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The soundness model relies on an unenforced shape hint and contains factual inconsistencies with current GQA behavior and configuration.
Review effort: Balanced
Findings: 1
Open (5)
Max shape override does not enforce the runtime workload envelope · New Absolute sequence length is conflated with resident cache capacity · New Route enforcement prerequisite contradicts documented case handling · New L1 estimate is not equal to concrete runtime allocation · New Unimplemented workspace option is presented as an existing setting · New
What changed in this PR
Documents a route-aware workspace-estimation design for safer, less conservative GPU partitioning.
Changes:
- Defines enforceable dispatch policies and workload envelopes.
- Covers GQA routing, configuration, validation, and rollout.
- Cross-references the design from constrained-environment guidance.
| File | Description |
|---|---|
policy_based_workspace_estimation.md |
Adds the workspace-policy design. |
future_directions_constrained_env.md |
Links the new design from existing guidance. |
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| | **`S_q` (query)** | tokens fed this Run (`N` prefill, `1` decode, chunk size) | `session.max_shape_override` on `input_ids` | | ||
| | **`S_kv` / capacity** | accumulated KV context / provisioned ceiling (`seqlen_present_kv_cache`) | overridden `past_key` seq dim, or the GQA capacity knob (see below) | |
| The invariant is `S_q ≤ total_sequence_length ≤ kv_cache_capacity`; the two axes coincide only in | ||
| the first full prefill. |
| - **Single hard prerequisite for all of it:** the dispatch-ladder enforcement change — redirecting | ||
| the unfused terminal (`data.use_unfused = true` in `group_query_attention.cc`) under a committed | ||
| contract — landed as its own small, well-tested PR *before* any smaller estimate is trusted. |
| one-directional — **migrate the runtime allocation to consume the same recipe builders the | ||
| estimator already uses** — after which estimate == allocation by construction and profiling | ||
| becomes *validation*, not a *requirement*. (The recipe layouts are marginally larger than the |
| The literal `π`, for benchmarking / power users. EP-scoped because routes are hardware-specific — | ||
| consistent with the existing `ep.cuda.gqa_workspace_max_total_sequence_length`. Today's scattered | ||
| per-route toggles become the **compile target** this policy lowers to: |
| has_memory_efficient_attention(sm, fp16, bf16, head_size, head_size) // no seq args | ||
| ``` | ||
|
|
||
| By the priority ladder, on any Run either a higher-priority route claims it (and runs, linear), or |
There was a problem hiding this comment.
So we basically add "dispatch order" to set up the correct priority as opposed to choosing the fallback (conservative) choice. The only risk is that we don't know for sure whether it will be taken during runtime? We only know it's accessible.
There was a problem hiding this comment.
Exactly right. L1 never predicts the taken route — it enumerates the reachable set and takes the max over it. "Reachable but maybe not taken" is fine for an upper bound: a seq-free-ineligible route is excluded; a reachable one is included in the max even if a given Run picks something cheaper. Dispatch order matters only to prove the unfused terminal unreachable (its guard requires every higher flag false). The corollary — which your next comment correctly presses — is that every reachable route must be sized, not just Flash/XQA.
Ti-Tai Wang (titaiwangms)
left a comment
There was a problem hiding this comment.
Design review (documentation only): I agree with the central distinction: we need not predict a single route, but must prove an upper bound for every route and input the runtime may execute. The Case A Flash/MEA argument can rule out unfused when its complete static eligibility predicate holds for every admitted Run; that alone does not make the proposed lower L1 bound sound. Two additional, actionable gaps are noted inline. I also agree with the existing comments that session.max_shape_override does not enforce a runtime envelope and that absolute total length differs from windowed-cache capacity. Please gate Stage 2's smaller estimate on a hard, cache-mode-aware envelope and complete sizing of every reachable route; otherwise preserve the conservative bound or return estimate unavailable. Profiling in Stage 4 can validate an upper bound but should not replace it with a sampled peak.
| By the priority ladder, on any Run either a higher-priority route claims it (and runs, linear), or | ||
| — if none does — Flash/MEA runs *because it is statically eligible*. Either way one of | ||
| `use_flash || use_mea || use_xqa || use_cudnn` is true, so the unfused guard is false. **Unfused | ||
| never executes in the current source, with no code change.** `max(prefill, decode, chunk)` is sound |
There was a problem hiding this comment.
Case A proves at most that unfused is unreachable; it does not prove every higher-priority route has a workspace upper bound. group_query_attention.cc:741-771 may choose cuDNN before Flash (including auto-selection on SM >= 90), while group_query_attention_workspace_bounds.cc:241-244 explicitly declines to bound a reachable cuDNN route without a graph-free workspace oracle. Flash being statically eligible is therefore insufficient to budget only Flash/XQA. Please require a sound cuDNN bound or prohibit cuDNN at runtime before enabling a smaller Stage-2 estimate; otherwise retain the conservative/unavailable result.
There was a problem hiding this comment.
Agreed that Case A proves only unfused unreachable, not that Flash/XQA bound the higher routes. This is already enforced in the estimator: BuildBounds flags a reachable cuDNN route (group_query_attention_workspace_estimate.cc:383-390, gate UseCudnnFlashAttention() || (AllowCudnnFlashAttentionAuto() && device_prop.major >= 9)) and GetGQAWorkspaceAggregateForBounds returns Unavailable for it (group_query_attention_workspace_bounds.cc:241-244) → the estimate is nullopt and the conservative reservation is retained. The smaller Stage-2 estimate is therefore never emitted when cuDNN is reachable. The real gap is the doc: the Case A paragraph's "max(prefill, decode, chunk) is sound as-is" silently assumes cuDNN is unreachable. I''ll add the cuDNN-reachability caveat so the prose matches the guard. Thanks for catching it.
| W_L1 = max(...) = flash prefill term ← linear, ~38x below the unfused reservation | ||
| ``` | ||
|
|
||
| Using `(S_q=P, S_kv=C)` as a single safe corner slightly over-approximates any individual Run |
There was a problem hiding this comment.
This corner need not bound Flash workspace even when the route does not switch. flash_attention/flash_api.cc:199-269 chooses num_splits by an occupancy heuristic: a large S_q=P can use one split (no accumulators), while a shorter admitted chunk at the same KV extent can use multiple splits and allocate substantial lse_accum and out_accum buffers. group_query_attention_workspace_bounds.cc:104-148 already bounds split counts rather than evaluating only the endpoint. Please bound all heuristic regimes (including preparation buffers); a single Stage-4 profile at the envelope corner cannot replace that proof.
There was a problem hiding this comment.
Agreed the worked-example corner (S_q=P, S_kv=C) labelled "[linear]" over-simplifies the Flash term. The estimator does not evaluate the heuristic at the corner: FlashBackendEnvelope (the group_query_attention_workspace_bounds.cc:104-148 you cite) bounds the split count by its structural maximum max_splits = min(128, multi_processor_count, ceil((kv + block_n − 1) / block_n)) and sizes lse_accum/out_accum with it — the code comment notes it "remains sound across its discontinuities," i.e. the one-split and many-split regimes are both covered; preparation/QKV buffers come from GQABufferRequirements. Stage-4 profiling validates this bound rather than replacing it with a sampled peak. I''ll rewrite the worked example to reference the max_splits bound instead of implying a single endpoint is evaluated.
|
Thanks Ti-Tai Wang (@titaiwangms) — this is the right frame: prove an upper bound for every reachable route, don't predict one. Three doc fixes incoming (behavior-level checks already exist in the estimator; these align the prose with the code):
I''ll push these as a follow-up commit on this branch for your re-review. |
… regimes, proposed-option labels Responds to Ti-Tai's design review and the endorsed bot findings on #32944 (prose-only edits; the estimator already behaves as described): - Case A: clarify it eliminates only the unfused terminal; a reachable cuDNN route has no graph-free oracle, so the reduced max(prefill,decode,chunk) is sound only when cuDNN is unreachable. The estimator already enforces this via GetGQAWorkspaceAggregateForBounds returning Unavailable for cuDNN. - Envelope: mark ep.cuda.gqa_workspace_max_total_sequence_length as proposed; add two caveats - max_shape_override is an estimation hint (needs a hard runtime envelope check for no-OOM), and the KV axis is cache-mode-aware (windowed uses the effective/staged extent, not absolute total length). - Single-source sizing: allocation is bounded by W_L1 (max over states/routes), not byte-equal; the shared recipe builders make the per-(state,route) formula identical. - Tier 2: label the knob proposed rather than existing. - Worked example: the Flash term is bounded across occupancy regimes by the structural max_splits ceiling in FlashBackendEnvelope, not read off a single envelope corner; Stage-4 profiling validates (not replaces) the bound. - Roadmap: the dispatch-ladder enforcement change is a prerequisite for the Case B reductions only, not for Case A. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fa98d07-aec3-41b7-92b8-b0ec317a95bf
…fe over latency) Clarifies that latency is the zero-config default for the common case (interactive, fits in VRAM) and documents why users pick the other intents: the knob changes per-node workspace reservation, hence how many nodes stay resident vs. offload to CPU, and CPU offload is asymmetrically expensive. - memory: near the VRAM cliff (offload cost can exceed per-kernel savings), multi-tenant/high-concurrency, and large-batch/long-context serving. - safe: hard no-OOM guarantee (bounded graph-free envelope; unbounded/unknown max seq len), correctness/reproducibility/debugging, and portability. Notes the three named intents are the manual approximation of the Tier-0 auto-optimizer that consumes (W_L1, cost) per route. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fa98d07-aec3-41b7-92b8-b0ec317a95bf
…e levers) Captures the broader offloading scope the workspace estimate plugs into: - Distinguishes the two independent VRAM levers - persistent weight residency (where a node's parameters live) vs. transient workspace/activation (what W_L1 bounds) - and notes they are separate accounting. - States the four properties that make a node a good CPU-offload candidate (large weight, low arithmetic intensity, low frequency, small activation transfer) with a per-node-type table; embeddings and MoE/QMoE experts qualify, attention and dense FFN stay resident. - Documents the practical first-cut knob: a node count per VRAM tier (not a byte budget), targeting the fused com.microsoft.MoE/QMoE node's packed expert-weight initializer inputs, with llama.cpp --n-cpu-moe / -ot and other precedents. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fa98d07-aec3-41b7-92b8-b0ec317a95bf
…3001) ## Description Factors the **sequence-length-independent** parts of the CUDA `GroupQueryAttention` backend-selection logic out of `GroupQueryAttention::ComputeInternal` into a new header-only module, `group_query_attention_eligibility.h`. It provides four pure "seq-free" route-eligibility predicates: | Helper | Route | |---|---| | `IsGQAXqaEligibleSeqFree<U>` | XQA (Extreme Query Attention) decode kernel | | `IsGQACudnnSdpaCoreEligibleSeqFree<T, U>` | cuDNN SDPA | | `IsGQAFlashEligibleSeqFree<T>` | FlashAttention | | `IsGQAMemoryEfficientEligibleSeqFree<T>` | CUTLASS memory-efficient attention | A **seq-free** predicate depends only on partition-time facts (geometry, device capability, quantization, feature flags) and *not* on the per-step sequence length or phase (prompt vs decode). The runtime keeps the sequence-length-dependent gates and dispatch priorities at the call sites: - **XQA** — phase gates (`is_first_prompt` / `sequence_length == 1` / `kv_sequence_length`) and the per-node shared-memory capability probe. - **cuDNN** — `cudnn_sdpa::is_stable()` and the seq-dependent `is_supported(...)` probe. - **Flash / MEA** — the `!use_xqa` / `!use_cudnn_sdpa` / `!use_flash_attention` dispatch priority. ## No functional change The eligibility booleans are computed from the same expressions in the same short-circuit-equivalent order. Every moved term is a side-effect-free read, so the selected backend is identical for every input. The GQA operator tests exercise all four routes and validate this (a wrong predicate would dispatch the wrong kernel and fail the tests). ## Why header-only All four helpers are templated on the Q/KV element types (`T`/`U`), so the module is header-only rather than the usual `.h`/`.cc` split. ## Motivation This is a small NFC step toward **policy-based, capacity-aware CPU offloading** (see the design doc in #32944). The shared seq-free core is what the Level-1 workspace estimator will later reuse, so runtime route selection and workspace estimation cannot drift. ## Relationship to sibling PRs - #32944 — design doc (policy-based workspace estimation). - #32945 — consolidates GQA **scratch sizing** into recipe helpers. This PR is the sibling that consolidates **route eligibility**. Both are independent NFC refactors; whichever merges second will need a trivial merge in the XQA/Flash region of `group_query_attention.cc`. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fa98d07-aec3-41b7-92b8-b0ec317a95bf Copilot-Session: 37ad7066-fcd3-45df-891e-44bee9e1cf04
… shared workspace recipes (#32945) ## Description Consolidates the GroupQueryAttention (CUDA) runtime scratch **sizing** for the memory-efficient (MEA), unfused fallback, XQA, and Flash paths into the shared workspace recipe builders that already back the Level-1 workspace estimator. `ComputeInternal` previously sized these buffers with inline math that duplicated the estimator''s recipe logic, so the allocation and the estimate could drift. This routes the runtime allocations through `GetGQAMemoryEfficientWorkspaceRecipe`, `GetGQAUnfusedWorkspaceRecipe`, `GetGQAXqaWorkspaceRecipe`, and `GetGQAFlashWorkspaceRecipe`, giving a single source of truth. ### No functional change (byte-identical) The recipe byte counts are identical to the previous inline sizing: - **MEA** expanded-K/V = `sizeof(T) * batch * num_heads * seqlen_present_kv_cache * head_size`; FP32 accumulator = `sizeof(float) * batch * sequence_length * num_heads * head_size`, gated exactly as `MemoryEfficientAttentionParams::need_workspace(head_size, /*is_float=*/false)` (i.e. `head_size > 128`). - **Unfused** reproduces the 256-byte-aligned Q and Y regions plus the two `GetUnfusedAttentionWorkspaceSize`-equivalent QK/softmax regions (same 256 alignment → total bytes and all sub-buffer offsets unchanged). - **XQA** reproduces the internal scratch (`GetXQAScratchSize`) plus the optional 256-aligned rotary-Q/K and dynamic head-sink regions, with the same pointer wiring. The prepacked head sink (`xqa_head_sink_`) stays out of scratch. - **Flash** reproduces `get_num_splits_and_buffer_sizes` (including the fast-decode accumulator correction) and preserves the `parameters.num_splits` side effect and the two accumulator `cudaMemsetAsync` guards. - GQA is only instantiated for `MLFloat16` / `BFloat16` (`qkv_element_size == 2`); the MEA/unfused fallbacks are only reached with an unquantized KV cache, satisfying the recipe validators. ### Scope - One file: `onnxruntime/contrib_ops/cuda/bert/group_query_attention.cc`. - All four backend scratch sizings now source from recipes. - The **preparation** allocations (windowed staging, compaction, seq-lens vectors, and the `GQABufferRequirements` QKV preprocess buffer) are intentionally **deferred to a follow-up PR**. Unlike the backend scratch, those allocations are interleaved with route selection and mutate `seqlen_present_kv_cache`, so a byte-identical migration needs a dedicated runtime-equivalence test (mirroring the XQA/Flash equivalence tests) first. - Part of the workspace-estimation series; see the design doc PR #32944. ### Validation Relying on CI''s CUDA build + GQA/workspace tests (a local CUDA build was disk-constrained). Because sizes and offsets are byte-identical, existing `group_query_attention_workspace*_test` and GQA op tests pass unchanged. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fa98d07-aec3-41b7-92b8-b0ec317a95bf Copilot-Session: 37ad7066-fcd3-45df-891e-44bee9e1cf04
…3001) ## Description Factors the **sequence-length-independent** parts of the CUDA `GroupQueryAttention` backend-selection logic out of `GroupQueryAttention::ComputeInternal` into a new header-only module, `group_query_attention_eligibility.h`. It provides four pure "seq-free" route-eligibility predicates: | Helper | Route | |---|---| | `IsGQAXqaEligibleSeqFree<U>` | XQA (Extreme Query Attention) decode kernel | | `IsGQACudnnSdpaCoreEligibleSeqFree<T, U>` | cuDNN SDPA | | `IsGQAFlashEligibleSeqFree<T>` | FlashAttention | | `IsGQAMemoryEfficientEligibleSeqFree<T>` | CUTLASS memory-efficient attention | A **seq-free** predicate depends only on partition-time facts (geometry, device capability, quantization, feature flags) and *not* on the per-step sequence length or phase (prompt vs decode). The runtime keeps the sequence-length-dependent gates and dispatch priorities at the call sites: - **XQA** — phase gates (`is_first_prompt` / `sequence_length == 1` / `kv_sequence_length`) and the per-node shared-memory capability probe. - **cuDNN** — `cudnn_sdpa::is_stable()` and the seq-dependent `is_supported(...)` probe. - **Flash / MEA** — the `!use_xqa` / `!use_cudnn_sdpa` / `!use_flash_attention` dispatch priority. ## No functional change The eligibility booleans are computed from the same expressions in the same short-circuit-equivalent order. Every moved term is a side-effect-free read, so the selected backend is identical for every input. The GQA operator tests exercise all four routes and validate this (a wrong predicate would dispatch the wrong kernel and fail the tests). ## Why header-only All four helpers are templated on the Q/KV element types (`T`/`U`), so the module is header-only rather than the usual `.h`/`.cc` split. ## Motivation This is a small NFC step toward **policy-based, capacity-aware CPU offloading** (see the design doc in #32944). The shared seq-free core is what the Level-1 workspace estimator will later reuse, so runtime route selection and workspace estimation cannot drift. ## Relationship to sibling PRs - #32944 — design doc (policy-based workspace estimation). - #32945 — consolidates GQA **scratch sizing** into recipe helpers. This PR is the sibling that consolidates **route eligibility**. Both are independent NFC refactors; whichever merges second will need a trivial merge in the XQA/Flash region of `group_query_attention.cc`. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fa98d07-aec3-41b7-92b8-b0ec317a95bf Copilot-Session: 37ad7066-fcd3-45df-891e-44bee9e1cf04
… shared workspace recipes (#32945) ## Description Consolidates the GroupQueryAttention (CUDA) runtime scratch **sizing** for the memory-efficient (MEA), unfused fallback, XQA, and Flash paths into the shared workspace recipe builders that already back the Level-1 workspace estimator. `ComputeInternal` previously sized these buffers with inline math that duplicated the estimator''s recipe logic, so the allocation and the estimate could drift. This routes the runtime allocations through `GetGQAMemoryEfficientWorkspaceRecipe`, `GetGQAUnfusedWorkspaceRecipe`, `GetGQAXqaWorkspaceRecipe`, and `GetGQAFlashWorkspaceRecipe`, giving a single source of truth. ### No functional change (byte-identical) The recipe byte counts are identical to the previous inline sizing: - **MEA** expanded-K/V = `sizeof(T) * batch * num_heads * seqlen_present_kv_cache * head_size`; FP32 accumulator = `sizeof(float) * batch * sequence_length * num_heads * head_size`, gated exactly as `MemoryEfficientAttentionParams::need_workspace(head_size, /*is_float=*/false)` (i.e. `head_size > 128`). - **Unfused** reproduces the 256-byte-aligned Q and Y regions plus the two `GetUnfusedAttentionWorkspaceSize`-equivalent QK/softmax regions (same 256 alignment → total bytes and all sub-buffer offsets unchanged). - **XQA** reproduces the internal scratch (`GetXQAScratchSize`) plus the optional 256-aligned rotary-Q/K and dynamic head-sink regions, with the same pointer wiring. The prepacked head sink (`xqa_head_sink_`) stays out of scratch. - **Flash** reproduces `get_num_splits_and_buffer_sizes` (including the fast-decode accumulator correction) and preserves the `parameters.num_splits` side effect and the two accumulator `cudaMemsetAsync` guards. - GQA is only instantiated for `MLFloat16` / `BFloat16` (`qkv_element_size == 2`); the MEA/unfused fallbacks are only reached with an unquantized KV cache, satisfying the recipe validators. ### Scope - One file: `onnxruntime/contrib_ops/cuda/bert/group_query_attention.cc`. - All four backend scratch sizings now source from recipes. - The **preparation** allocations (windowed staging, compaction, seq-lens vectors, and the `GQABufferRequirements` QKV preprocess buffer) are intentionally **deferred to a follow-up PR**. Unlike the backend scratch, those allocations are interleaved with route selection and mutate `seqlen_present_kv_cache`, so a byte-identical migration needs a dedicated runtime-equivalence test (mirroring the XQA/Flash equivalence tests) first. - Part of the workspace-estimation series; see the design doc PR #32944. ### Validation Relying on CI''s CUDA build + GQA/workspace tests (a local CUDA build was disk-constrained). Because sizes and offsets are byte-identical, existing `group_query_attention_workspace*_test` and GQA op tests pass unchanged. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fa98d07-aec3-41b7-92b8-b0ec317a95bf Copilot-Session: 37ad7066-fcd3-45df-891e-44bee9e1cf04
Distinguish analytical bounds from profiles and heuristics, document fail-closed GQA placement, and separate per-node route bounds from whole-graph lifetime accounting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Updated in |
Ti-Tai Wang (titaiwangms)
left a comment
There was a problem hiding this comment.
Follow-up design review at 324f7c46c77f919f8d56d0fd9d3ade2812cf75a3, informed by #29775 and the related implementation PRs.
The main earlier concerns are addressed: profiling now validates rather than replaces the analytical proof; declared estimates are distinguished from runtime-enforced envelopes; and a whole-session no-OOM claim depends on global lifetime accounting and controlled allocator capacity/reserve, not just a node's scratch bound.
I have four remaining documentation corrections, detailed inline:
- Label fail-closed placement on an unavailable GQA estimate as proposed, rather than current behavior.
- Separate embedding gather from the dense vocabulary projection performed by
lm_head. - Specify the execution/copy mechanism for MoE offload; redirecting an initializer to a CPU allocator alone is not CPU execution or GPU weight streaming.
- Separate measured concrete-allocation savings from analytical admission-bound savings in the 38x worked example.
These are documentation/design corrections, not claims that this docs-only PR changes runtime behavior. The current two failed CI jobs do not establish a documentation regression: the lint job failed resolving a Docker Hub image with HTTP429, and the ASan job received a runner shutdown during Python setup before build/test. No local build, performance measurement, or external-framework offload experiment was run.
| (`include/onnxruntime/core/framework/resource_accountant.h`) records provenance, not proof: | ||
| `kFallback` is the generic 50% heuristic, `kEstimator` may be a proven analytical route bound, and | ||
| `kProfile` is an empirical observation. `kProfileAndEstimator` retains the maximum of the observed | ||
| and analytical values. Capacity-aware GQA placement accepts only an available route-aware |
There was a problem hiding this comment.
Clarify current behavior versus the proposed fail-closed contract. At this pinned revision, CUDAExecutionProvider::GetCapability still calls ComputeResourceCount after a GQA estimate returns unavailable; the accountant can use generic 50% fallback or profile data. There is no GQA-specific unavailable-to-decline branch. Please label analytical-bound-only placement here and in the earlier cuDNN discussion as a future requirement, and explicitly state today's fallback behavior. Otherwise the descriptive section suggests that enforcement is already implemented.
|
|
||
| | Node | Weight size | Compute / byte | Frequency | Offload? | | ||
| |---|---|---|---|---| | ||
| | Embedding / `lm_head` | very large (`vocab × hidden`) | ~0 (gather) | once / token | ✅ best | |
There was a problem hiding this comment.
Separate embedding from lm_head in this cost model. Embedding is a gather, but lm_head normally computes a dense hidden-to-vocabulary projection. Its arithmetic and result-transfer costs are not the same as transferring a few embedding rows. Please split the row and evaluate the projection's offload tradeoff separately rather than assigning both near-zero gather compute and the same recommendation.
| resident and offload the cheap-to-move weight-heavy nodes. In ORT the target is the fused | ||
| `com.microsoft.MoE` / `QMoE` node's packed expert-weight **initializer inputs** | ||
| (`fc1/fc2/fc3_experts_weights`) — one node per layer — so "offload `N` MoE layers" is literally | ||
| redirecting the weight inputs of `N` `QMoE` nodes to a CPU allocator. The structure mirrors the |
There was a problem hiding this comment.
Initializer placement alone does not implement the proposed offload. The current CUDA MoE/QMoE registrations expect device inputs for expert weights; placing the source initializer on CPU does not make the fused node execute on CPU or make its CUDA kernel stream host-resident weights. Please distinguish whole-node CPU assignment, with supported dtype/feature checks and activation-copy boundaries, from a new GPU weight-staging/streaming mechanism. State which mechanism the proposed option selects and account for any retained device/prepacked copies before claiming the residency savings.
| ``` | ||
| prefill/chunk : CompleteWorkspace(S_q=P, S_kv=C, Flash) = lse + lse_accum + out_accum | ||
| decode : CompleteWorkspace(S_q=1, S_kv=C, XQA) = GetXQAScratchSize(C) [small] | ||
| W_L1 = max(...) = flash prefill term ← ~38x below the unfused reservation |
There was a problem hiding this comment.
The structural envelope does not establish a 38x admission reduction. The earlier 123 MB versus approximately 3.2 MB comparison is a concrete allocation observation. The structural split ceiling below can combine query and split extrema that do not occur together, so its analytical bound can be much larger than that concrete Flash allocation. Please label the measured comparison separately and avoid carrying its ratio into W_L1 without a bound calculation under the same geometry and assumptions. The displayed CompleteWorkspace expression also needs to include preparation/QKV terms, or be explicitly labeled backend-only.
…oute-aware L1 estimate) Make the GQA Level-1 workspace estimator produce a sound bound for the non-windowed KV-cache case instead of declining. Previously BuildBounds returned nullopt for every non-windowed node because total_sequence_length is a runtime scalar not recoverable from shapes. It now uses the declared KV-length envelope (config.max_total_sequence_length, from session option ep.cuda.gqa_workspace_max_total_sequence_length, plumbed in the prior PR) as present_kv_cache_capacity_bound and evaluates the existing route-aware, phase-cornered aggregate over it. Opt-in and default-off: when the envelope is unset (== 0) the non-windowed path still returns nullopt, so behavior is byte-identical to today. The windowed path is untouched -- every change is guarded by the windowed flag. Soundness: - Backend workspace is non-decreasing in KV length and the runtime sizes non-windowed scratch from total_sequence_length <= envelope (GetGQAEffectiveWorkspaceKvLength), so capacity_bound = envelope is an upper bound. - Non-windowed partial aliasing (exactly one past/present K/V pair shared) copies the full past cache into preservation scratch that coexists with the backend workspace. This is now charged on top of the per-route maximum via GQAWorkspaceBounds::account_partial_alias_preservation, bounded by the envelope and head_size_bound. Windowed GQA rejects non-shared buffers, so it never incurs this copy. - cuDNN (when reachable) and attention bias conservatively decline (nullopt), keeping the estimate an upper bound. The small seq_lens buffer remains an unmodeled pre-existing gap shared with the windowed path. Tests (onnxruntime_provider_test): non-windowed without an envelope still declines; with an envelope it is bounded and monotonic in the envelope; the preservation copy is additive on top of the selected route. Part of the policy-based workspace estimation effort; see docs/annotated_partitioning/policy_based_workspace_estimation.md and the PR chain (#32944, #32945, #33001, #33014). Base retargets to main after #33014 merges. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fa98d07-aec3-41b7-92b8-b0ec317a95bf
## Summary Complete the preparation-sizing half of Stage 1 in the policy-based, route-aware workspace-estimation design (#32944). This combines the agreed QKV-preprocessing refactor and the remaining cache/sequence preparation sizing into one PR. Builds on #32945, now merged into `main`, which consolidates MEA, unfused, XQA and Flash backend scratch sizing. - Share checked cache-row, past-preservation, staging, compaction, sequence-vector and QKV-preprocessing calculations between `ComputeInternal` and `GetGQAPreparationRecipe`. - Remove the duplicate `GQABufferRequirements` implementation. - Keep the existing separate allocations, ownership, pointer wiring, dispatch priorities and lifetimes. - Preserve the preparation recipe's aligned layout and full validation; the live kernel calls narrow sizing helpers rather than inheriting unrelated recipe-admission restrictions. This is sizing-code consolidation, not a missing-buffer fix. It does not activate dispatch policies, exclude routes from L1, coalesce allocations, or change static-preallocation strategy. ## Compatibility details - Cache sizing runs before backend selection and retains the original present-cache capacity. Windowed staging still changes the runtime extent from `C` to `C + S`. - Past preservation uses the aliased tensor's physical row width; staging uses the physical output row width; compaction retains its packed-row interpretation. INT4 rows are not packed a second time. - QKV sizing runs after the existing backend selection and receives the effective post-staging capacity. cuDNN retains the existing generic Q-materialization behavior. - Flash fast decode still reuses the device sequence-length input instead of allocating sequence vectors, and still suppresses the QKV-preprocessing buffer. - Existing healthy-input byte counts are retained. Arithmetic overflow and invalid sizing inputs produce explicit sizing statuses at the runtime boundary. ## Coverage and validation - Fresh standalone build of the existing preparation Google Test source: **50 tests passed**, including the legacy QKV feature matrix, dense/byte/INT4 cache geometry, physical packed rows, sequence-vector suppression, invalid dimensions and overflow. - MSVC `/W4 /WX` compilation of the actual CUDA GQA runtime translation unit in both shared-provider and plugin-adapter configurations, with Flash, MEA, FP8 and INT4 enabled. - MSVC `/W4 /WX` compilation of the graph-time estimator and XQA/Flash sizing-test translation units. - MSVC `/W4 /WX` compilation of the CUDA GQA operator-test translation unit. - Four new CUDA regressions compare packed and separate QKV on unfused/Flash paths, including windowed staging. They reuse the aliasing helper, which disables CPU fallback and verifies actual backend dispatch. - `clang-format --dry-run --Werror` and `git diff --check` passed. **Full CUDA inference execution is not validated locally against a freshly linked provider.** The new operator regressions and existing GQA tests need full CUDA build/test CI. ## Stack Base: `main`. The conflict-resolution merge retains the early preparation-sizing initialization, the upstream seq-free eligibility helpers, and the INT8/FP8 storage-width normalization. The envelope and policy-reader PRs (#33014, #33016 and #33042) remain independent of this refactor. Committed-policy resolution, coverage/envelope enforcement, bounded fallback and policy-bound estimates remain follow-up work. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fa98d07-aec3-41b7-92b8-b0ec317a95bf
…oute-aware L1 estimate) Make the GQA Level-1 workspace estimator produce a sound bound for the non-windowed KV-cache case instead of declining. Previously BuildBounds returned nullopt for every non-windowed node because total_sequence_length is a runtime scalar not recoverable from shapes. It now uses the declared KV-length envelope (config.max_total_sequence_length, from session option ep.cuda.gqa_workspace_max_total_sequence_length, plumbed in the prior PR) as present_kv_cache_capacity_bound and evaluates the existing route-aware, phase-cornered aggregate over it. Opt-in and default-off: when the envelope is unset (== 0) the non-windowed path still returns nullopt, so behavior is byte-identical to today. The windowed path is untouched -- every change is guarded by the windowed flag. Soundness: - Backend workspace is non-decreasing in KV length and the runtime sizes non-windowed scratch from total_sequence_length <= envelope (GetGQAEffectiveWorkspaceKvLength), so capacity_bound = envelope is an upper bound. - Non-windowed partial aliasing (exactly one past/present K/V pair shared) copies the full past cache into preservation scratch that coexists with the backend workspace. This is now charged on top of the per-route maximum via GQAWorkspaceBounds::account_partial_alias_preservation, bounded by the envelope and head_size_bound. Windowed GQA rejects non-shared buffers, so it never incurs this copy. - cuDNN (when reachable) and attention bias conservatively decline (nullopt), keeping the estimate an upper bound. The small seq_lens buffer remains an unmodeled pre-existing gap shared with the windowed path. Tests (onnxruntime_provider_test): non-windowed without an envelope still declines; with an envelope it is bounded and monotonic in the envelope; the preservation copy is additive on top of the selected route. Part of the policy-based workspace estimation effort; see docs/annotated_partitioning/policy_based_workspace_estimation.md and the PR chain (#32944, #32945, #33001, #33014). Base retargets to main after #33014 merges. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fa98d07-aec3-41b7-92b8-b0ec317a95bf
Define route descriptors as workload-dependent functions and specify the GQA-first auto, latency, memory, and safe policy behavior across L1, L2, and runtime enforcement. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c132130a-b0c7-422c-9ff7-75826b6ea3fc
Describe per-phase route masks, decode fallback closure, runtime classification, and maximum-over-phases Level-1 accounting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c132130a-b0c7-422c-9ff7-75826b6ea3fc
…acts Design whole-session GPU lifetime and physical backing accounting, pre-allocation budget planning, and deterministic contract/placement selection. Defer workload-envelope enforcement and additional bounded fallback coverage without weakening conditional guarantees. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c132130a-b0c7-422c-9ff7-75826b6ea3fc
…oute-aware L1 estimate) (#33016) ## Summary Adds opt-in, route-aware Level-1 workspace estimation for non-windowed CUDA GroupQueryAttention using the declared total-KV envelope from `ep.cuda.gqa_workspace_max_total_sequence_length`. The present-cache capacity bound is **`max(allocated past-cache capacity, declared total-KV envelope)`**. Static caches may be larger than the active sequence: GQA permits `total_sequence_length <= past_sequence_length` and retains that allocated capacity. Bounding only the envelope would underestimate MEA expansion and partial-alias preservation. The existing graph-free aggregate sizes reachable routes and admitted prompt/decode phase corners; this PR does not introduce a second sizing engine. ## Envelope validation and caller contract - Explicit values are parsed once during session initialization and must be decimal integers in **`[0, INT32_MAX]`**, matching the runtime ABI. Malformed, negative, overflowing, and greater-than-`INT32_MAX` values fail initialization. - Zero remains the unspecified value. With the envelope unset or zero, non-windowed estimation is unavailable; windowed estimation continues to use its cache-shape bound. - The envelope must cover the query-length bound, and the resulting cache-capacity bound must fit the runtime int32 ABI. - The query/max-shape and total-KV declarations are estimation inputs, **not runtime-enforced admission limits or a no-OOM guarantee**. The proof applies only within the caller-declared workload envelope; runtime enforcement remains follow-up work. ## Workspace accounting and proof policy - Capacity-sensitive preparation and MEA expansion cover the full static-cache extent, even when it exceeds the active total-KV envelope. - Partial aliasing copies the full past tensor. Its preservation scratch coexists with the chosen route and is added above the per-route maximum, bounded by the full cache-capacity bound. - Preparation recipes already account for sequence-length vectors; there is no new missing-vector gap. - Reachable cuDNN without a workspace oracle, unsupported attention-bias cases, and otherwise unprovable route domains conservatively make the estimator unavailable. - For capacity-aware CUDA placement, every GQA candidate must have a successful analytical estimate. If proof is unavailable, CUDA assignment for that node is declined. - The generic accountant fallback is an ad hoc safety margin, not a proven GQA bound. Profile data is empirical and likewise cannot substitute for the missing analytical proof. Unrelated operators retain their existing generic fallback behavior. ## Regression coverage - Accepted boundary value `INT32_MAX`; rejected `INT32_MAX + 1`, `INT64_MAX`, decimal overflow, malformed text, and negative values. - Unset-envelope behavior and monotonicity when the envelope increases. - Static past capacity **1024** with total-KV envelope **512**, checking full preservation and MEA expansion against complete concrete runtime recipes. - An envelope below the query-length bound still declines, even when the static cache is larger. - Static capacity outside the runtime int32 ABI declines. - Parsed-envelope plumbing and unchanged windowed estimates. - Additive full-past preservation above the per-route maximum. - Capacity-aware CUDA placement declines GQA when the analytical estimator is unavailable, including when profile data exists; unrelated operators retain generic fallback. ## Dependencies Targets `main`, which now includes #33118's shared preparation-sizing implementation. The duplicated sizing commits were dropped during the post-merge rebase; this PR contains only its envelope-estimation, hardening, test, and documentation changes. Part of the policy-based workspace-estimation series described in #32944. Committed per-`(node, phase)` route policy and runtime workload-envelope enforcement remain follow-up work. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Chi Lo <lochi@microsoft.com> Copilot-Session: 7fa98d07-aec3-41b7-92b8-b0ec317a95bf Copilot-Session: c132130a-b0c7-422c-9ff7-75826b6ea3fc Copilot-Session: b95559c4-5cbe-4787-8dd7-ffa45a2157f9
… estimator (reader-only) Adds the hardware-neutral session option session.attention_dispatch_policy and threads its value through to the CUDA GroupQueryAttention Level-1 workspace estimator. This is the Tier-1 intent knob from the policy-based workspace estimation design: "latency" favors high-workspace fast routes, "memory" favors low-workspace routes (so more nodes fit on GPU), and "safe" assumes the bounded fallback everywhere. Unset (default) or any unrecognized value means "auto" and keeps the current route-aware behavior. This is a pure no-functional-change reader. The raw string is parsed (WorkspaceEstimatorConfig::attention_dispatch_policy -> AttentionDispatchPolicy enum via ParseAttentionDispatchPolicy) and stored on GQAWorkspaceEstimateConfig::dispatch_policy, but the estimator does not yet consume it, so every policy value produces the identical estimate. A follow-up PR biases route selection on the committed policy (coverage validation and the bounded fallback land in their own PRs). Plumbing mirrors the ep.cuda.gqa_workspace_max_total_sequence_length reader. The enum/parser live in the CUDA GQA estimator header because it is the only consumer today; hoist to a neutral header when a second op consumes it. Tests: extends the FactoryRetainsNarrowWorkspaceEstimatorConfig framework test (config string -> WorkspaceEstimatorConfig field, CPU CI) and adds a provider-test asserting the token parsing (exact-match, unknown -> Auto) and the reader-only invariant (no policy value changes the estimate). Part of the policy-based workspace estimation effort; see docs/annotated_partitioning/policy_based_workspace_estimation.md and the PR chain (#32944, #32945, #33001, #33014, #33016). Base retargets toward main as the stack merges. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: b95559c4-5cbe-4787-8dd7-ffa45a2157f9
Tianlei Wu (tianleiwu)
left a comment
There was a problem hiding this comment.
Well-structured design document covering policy-based, route-aware L1/L2 workspace estimation and whole-session GPU memory planning. Left two inline comments on GPU-resident total_sequence_length phase classification and on how current post-partition reservation callbacks / empty L2 declarations interact with the proposed verification invariants.
| `total_sequence_length` resides on the GPU and the helper cannot distinguish the first prompt from | ||
| a later chunk, the existing runtime behavior reports the multi-token step as non-first, so policy | ||
| enforcement conservatively uses the chunked-prefill mask. L1 has already admitted and sized both | ||
| general phases. | ||
|
|
||
| Everything L1 needs is static: | ||
|
|
||
| | Ingredient | Source | | ||
| |---|---| |
There was a problem hiding this comment.
Clarify GPU-resident total_sequence_length phase classification for single-token first prompts and the no past input ⇒ prefill-only table row.
- In
group_query_attention_helper.h:412-427, whentotal_sequence_lengthresides on the GPU (!is_total_seqlen_on_cpu),CheckInputsskips the prompt-type block entirely and leavesis_first_prompt = falseandis_subsequent_prompt = falsefor all steps:- A single-token first prompt (
sequence_length == 1,total_sequence_length == 1on GPU) has!is_first_prompt && sequence_length == 1, so runtime classifies it asdecoderather thanprefill. - The
batch_size == 1guard forsequence_length > 1with past context (group_query_attention_helper.h:417-420, cited on line 313 as the guard protectingmaxacross phases) is also bypassed whentotal_sequence_lengthis on GPU.
- A single-token first prompt (
- Line 302's table states
no past input ⇒ prefill-only, whereasBuildBounds(group_query_attention_workspace_estimate.cc:244) currently requirespast_key/past_value(if (!Present(shapes, kPastKey)) return std::nullopt;). Moreover, if a future prefill-only mode omitteddecodeandchunkfromAdmittedPhases, any run with GPU-residenttotal_sequence_lengthwould setis_first_prompt = falseand classifysequence_length == 1asdecodeandsequence_length > 1aschunk, escaping aprefill-only phase mask.
There was a problem hiding this comment.
Thanks, agreed. Updated the document in 1934035 to make both GPU-resident cases explicit: a single-token first prompt uses the decode mask, and a multi-token first prompt uses the chunked-prefill mask. These are runtime dispatch classes, not necessarily the semantic prompt stage.
The document now states that BuildBounds currently requires both past K/V shapes and declines no-past configurations; absent past inputs do not establish prefill-only coverage. It also removes the CPU-only batch guard as a universal justification for max across phases: alternatives are mutually exclusive within one GQA invocation, but that does not prove phase lockstep across an entire Run or exclude overlapping allocations.
I also opened #33420, stacked on #33316, to tie phase admission explicitly to the shared runtime classifier and query-length bound, with regression coverage for CPU/GPU total-length metadata, missing past inputs, and phase-mask coverage. #33316 already admits the conservative phase set; this follow-up protects against future narrowing rather than claiming an existing escape. It introduces no device-to-host scalar read, runtime routing change, or new GPU-path batch restriction.
| verification. A fused region requires its own contract, not the sum of constituent scratch peaks. | ||
| Use bounded deterministic planning/refinement passes rather than allocating and trying to recover | ||
| from OOM. | ||
|
|
||
| Store selections with the finalized `SessionState`. Kernels must consume the selected contract ID | ||
| and compatible descriptor version, rather than independently re-resolving the session intent to a | ||
| different route set after placement. A change to route permissions, lifetime, capture state, or pool | ||
| strategy invalidates the plan even when one node's workspace scalar is unchanged. | ||
|
|
||
| L2 may tighten a requirement without broadening permissions. A larger requirement needs a verified | ||
| new plan before allocation/use or an explicit Initialize error; a warning followed by dynamic growth |
There was a problem hiding this comment.
Document how current post-partition callbacks and empty L2 declarations differ from the target verification invariants.
- Lines 815–818 state that
orphaned bindings fail verificationandA fused region requires its own contract, not the sum of constituent scratch peaks.Today,InferenceSession::TransformGraph(inference_session.cc:1882-1907) registersSetNodeReplacementCallback→ConsolidateWorkspaceReservations(resource_accountant.cc:26-46), which sumsreservation.bytesacrosssource_node_indices, andSetNodeRemovalCallback, which erases removednode_indexentries fromworkspace_reservationsbeforeFinalizeSessionStateImpl'sorphaned_reservationscheck (session_state.cc:2227-2236) runs. - Lines 823–825 state that a warning followed by dynamic growth cannot satisfy hard-budget/strict verification. In
SessionState::FinalizeSessionStateImpl(session_state.cc:2191-2196), ifkernel->DeclareWorkspaceRequirements(...)returnsStatus::OK()withrequirements.empty()for a node that has a nonzero L1reservation, it incrementsmissing_declarationandcontinues without settingstrict_verification_failed.
Calling out both gaps in the integration table below (lines 860–869) would make the required Stage 4/5 framework changes explicit.
There was a problem hiding this comment.
Thanks, agreed. Addressed in 1934035 with a new "Current reconciliation gaps, not implemented guarantees" discussion, corresponding integration-table entries, and explicit Stage 4/5 follow-ups:
- Replacement callbacks currently aggregate source reservations; that is bookkeeping, not a target implementation estimate or fused-region contract. The target design requires the replacement's own requirements and binding.
- Removal callbacks erase entries before orphan checking, so the final reservation map cannot reveal an obligation already lost. The proposed verification must track explicit retirement, validated transfer, or invalidation/re-estimation provenance.
- A successful empty L2 declaration with a nonzero L1 reservation currently increments
missing_declarationwithout settingstrict_verification_failed. The proposed opt-in strict mode must reject unresolved obligations or require evidence of a valid zero-workspace replacement; an empty declaration alone is not that evidence.
These are now clearly identified as required framework work, not guarantees provided by the current callbacks/finalization path. The update also calls for focused coverage of each gap and preserving legacy behavior outside the proposed opt-in contract/budget mode. This PR remains documentation-only; it does not claim to implement those framework fixes.
Address Tianlei Wu review observations on #32944: preserve GPU-length classifier coverage and document replacement, removal, and empty-declaration gaps as Stage 4/5 work rather than current guarantees. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c132130a-b0c7-422c-9ff7-75826b6ea3fc
…ations (#33240) ### Description <!-- Describe your changes. --> Extend this PR from GQA envelope plumbing to the GQA, MatMulNBits, and shared-framework L1/L2 reconciliation follow-up for #32944. Stacked on #33316, reusing its shared GQA envelope and committed phase-policy implementation rather than duplicating that work. - Preserve the original non-windowed GQA cross-level and strict-initialization regressions, disable CPU fallback in those integration tests, and distinguish a successful zero-workspace estimate from an unavailable estimate. - Declare the constructed MatMulNBits legacy dequantization or fpA-intB route using the existing shared sizing/chunking helpers. Cover smaller unchunked inputs below the fpA size gate, unknown M with a launch cap, and graph-proven zero versus an unproven zero-shaped hint. Keep the legacy override available when fpA is compiled out. - In strict mode, reject nonzero reservations without an available kernel declaration. A replacement must provide its own declaration against its transferred budget; reuse the existing transfer/removal callbacks instead of introducing another reservation ledger. Preserve default-mode dynamic allocation and diagnostics. - Document the empty-list/unavailable versus nonempty-zero declaration contract and add focused framework/kernel regressions. **Out of scope:** planned backing allocation, session-wide lifetime planning, hard session-budget enforcement, automatic placement, and plugin C-ABI forwarding. ### Motivation and Context <!-- - Why is this change required? What problem does it solve? - If it fixes an open issue, please link to the issue here. --> A partition-time reservation is a budget, not proof that the final kernel fits it. Strict reconciliation must not silently accept an unavailable declaration, including after replacement/fusion. MatMulNBits must also reconcile the actual selected route and the entire hinted chunking envelope, not only its maximum-M endpoint. **Validation** Fresh Windows Release builds completed for the CPU framework target and the in-tree CUDA provider/internal-test target. Commands below ran from their respective build output directories. CPU framework/accounting/envelope selection: **42 passed, exit 0**. ```powershell .\onnxruntime_test_all.exe '--gtest_filter=SessionStateTest.*Workspace*:OpKernelTest.DefaultDeclareWorkspaceRequirementsClearsOutput:ResourceAccountantTest.*:RealAccountantTest.*:CudaGqaWorkspaceEnvelopeTest.*' ``` CUDA selection: **47 inner tests passed, no skips; outer runner passed, exit 0**. Includes all new kernel regressions plus the existing MatMulNBits end-to-end and chunking regressions. ```powershell $env:GTEST_FILTER='MatMulNBitsWorkspace.*:GroupQueryAttentionWorkspaceEstimateTest.DeclaresOneAlignedSlotAndOnlyWorkspaceAtLevel1:GroupQueryAttentionWorkspaceEstimateTest.KernelDeclarationMatchesBoundedNonWindowedLevel1:GroupQueryAttentionWorkspaceEstimateTest.StrictVerificationAcceptsBoundedNonWindowedAgreement' .\onnxruntime_provider_test.exe '--gtest_filter=CUDA_EP_Unittest.All' ``` Additional native CUDA MatMulNBits source compilation with `USE_FPA_INTB_GEMM=0`, into an independent object/PDB without reusing the fpA-on PCH: **passed**. This is a compile check, not a full fpA-off runtime build. ```text lintrunner -r origin/chilo/gqa-committed-policy --take CLANGFORMAT --output oneline git diff --check origin/chilo/gqa-committed-policy ``` Both checks **passed**. **Local validation limits/workarounds** - CUDA coverage is CUDA 12.8 / T1000 SM75, compact fpA, in-tree EP; Flash/MEA/TRT-fused attention are off. No SM80+ backend coverage is claimed. - Windows heap checking was disabled with `onnxruntime_ENABLE_MEMLEAK_CHECKER=OFF`. With checking enabled, all 42 assertions passed but shutdown exited unsuccessfully; running only the unchanged `ResourceAccountantTest.AccountForAllNodes_CorrectlyUsesPreStoredCosts` also passed its assertions and failed the shutdown check. The leak cause remains unresolved. - Local CUDA compilation used `-allow-unsupported-compiler`, `_CL_=/wd4310 /wd4702`, and `NVCC_APPEND_FLAGS=--ptxas-options=-w` for warnings in unmodified CUTLASS/Abseil and SM75 FP8 code. GoogleTest's Abseil integration was disabled through a build-only CMake project hook after the host/module copies aborted on duplicate Windows `SymInitialize` (error 87). These workarounds are not repository changes. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Chi Lo <lochi@microsoft.com> Copilot-Session: 7fa98d07-aec3-41b7-92b8-b0ec317a95bf Copilot-Session: 6d876416-81ec-4982-a9b0-38b8b28b5e4a
### Description <!-- Describe your changes. --> Adds the **S0 internal capability-planning protocol** for future region-aware CPU/GPU placement: - An owned query snapshot with graph/revision, workload bounds, normalized EP/policy constraints, device/configuration, and execution assumptions. - Optional `ComputeCapability::planning` metadata describing candidate identity, alternative views, compatibility, prepared recipes, execution contracts, and lifetime-aware allocation requirements. - Result validation and immutable selected bindings that retain the original query and only the selected recipe/contract state. Failed binding attempts leave an existing binding unchanged. - Nine host-only protocol tests, including illustrative GQA and fusion fixtures, plus consolidated [interface/protocol documentation](https://github.com/microsoft/onnxruntime/blob/6853315594683f76fbaf6ab157ac0d35dd3514e4/docs/annotated_partitioning/capability_planning_interfaces.md). **Scope:** the planning-aware `GetCapability` overload, host dispatcher, shared-provider bridge, and versioned plugin C transport are documented proposals, not implemented entry points in this PR. Existing legacy discovery signatures, resource-accountant behavior, optimization hooks, placement, kernel dispatch, and published C API tables remain unchanged. A locally validated bounded binding is **not** whole-plan feasibility or memory-budget admission. The fixtures do not execute CUDA or prove GQA estimator coverage; their workspace values are illustrative inputs. ### Motivation and Context <!-- - Why is this change required? What problem does it solve? - If it fixes an open issue, please link to the issue here. --> Establishes the shared S0 vocabulary for #33372 and supports the allocation vocabulary in #32944, without depending on KV-cache ownership or a GQA policy implementation. Follow-up work is separated by the roadmap: **C1** wires planning-safe discovery while preserving the legacy workflow; **C2** adapts L1 sizing; **C3** carries a selected GQA contract into initialization/L2/runtime. **I1/I2** later add whole-plan evaluation and pre-allocation admission/commitment. Automatic cut selection and optional fusion application are not part of this PR. #### Validation At reviewed head `6853315594`, lintrunner, Python format, the completed minimal-build checks, and the Linux exception-disabled MinSizeRel check passed. Other CI checks are still running or queued; none have reported failure at the readiness check. No local compilation or test execution was rerun for this review. No full local ORT rebuild, CUDA integration, or real-model execution is claimed. --------- Co-authored-by: Chi Lo <lochi@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: c132130a-b0c7-422c-9ff7-75826b6ea3fc Copilot-Session: 5170d5a1-509e-4b08-b4f8-241666469dd0



Description
Adds a design document for policy-based, route-aware Level-1 (L1) workspace estimation, the foundation for capacity-aware CPU offloading on VRAM-constrained machines.
Problem
ORT's partition-time workspace estimate can over-reserve VRAM by including routes the committed runtime policy will not execute, such as GQA's quadratic unfused path. This needlessly pushes heavy nodes off GPU. An estimate is also unsound if runtime remains free to escape to an omitted route.
Proposal
Bind a committed dispatch policy:
(admitted workload envelope, phase-specific permitted routes, workspace bound). Runtime is contractually constrained to that policy, and L1 budgets every permitted route for each admitted phase, then takes the maximum across prefill, decode, and chunked prefill while phases remain mutually exclusive per Run.The document covers:
auto/latency/memory/safeintent semantics;Implementation alignment
#33316 implements the GQA-first committed-policy slice: L1 and L2 resolve the same phase-specific masks, runtime dispatch is gated by the current phase's mask, and
memoryretains selected general routes as bounded decode fallbacks. Whole-session lifetime/physical-backing accounting and budget-constrained contract selection are the next framework follow-ups. Optional ORT-side workload-envelope enforcement and additional bounded fallback coverage are explicit deferred TODOs.Scope
Files
docs/annotated_partitioning/policy_based_workspace_estimation.md— the design document.docs/annotated_partitioning/future_directions_constrained_env.md— cross-reference paragraph.