Skip to content

[docs] Policy-based, route-aware workspace estimation design - #32944

Open
Chi Lo (chilo-ms) wants to merge 9 commits into
mainfrom
chilo/policy-based-workspace-estimation-doc
Open

Chi Lo (chilo-ms) wants to merge 9 commits into
mainfrom
chilo/policy-based-workspace-estimation-doc

Conversation

@chilo-ms

@chilo-ms Chi Lo (chilo-ms) commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

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:

  • the policy contract and soundness invariant;
  • route descriptors as workload/device-dependent functions;
  • query-length and KV-capacity envelopes;
  • per-node/per-phase commitment for prefill, decode, and chunked prefill;
  • maximum-over-phases L1 accounting and Level-2 lifetime implications;
  • explicit runtime enforcement and fallback closure;
  • portable auto / latency / memory / safe intent semantics;
  • empirical profiling as validation rather than proof;
  • llama.cpp, vLLM, and SGLang offloading comparisons;
  • a worked GQA policy and staged implementation roadmap;
  • framework-owned allocation identity/lifetime accounting for Initialize and Run;
  • physical backing, allocator retention, external KV/I/O ownership, and reserve composition;
  • pre-allocation budget planning, immutable contract bindings, and L2 reconciliation;
  • deterministic budget-constrained contract/CPU-region selection, with latency calibration later.

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 memory retains 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

  • Docs only. No code or behavior change in this PR.
  • Design foundation for the workspace-estimation and policy-enforcement PR series.

Files

  • docs/annotated_partitioning/policy_based_workspace_estimation.md — the design document.
  • docs/annotated_partitioning/future_directions_constrained_env.md — cross-reference paragraph.

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
@chilo-ms
Chi Lo (chilo-ms) marked this pull request as ready for review September 30, 2026 17:32
Copilot AI balanced review requested due to automatic review settings September 30, 2026 17:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 High severity · 2 Medium severity · 2 Low severity

Open (5)
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.

Comment on lines +196 to +197
| **`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) |
Comment on lines +202 to +203
The invariant is `S_q ≤ total_sequence_length ≤ kv_cache_capacity`; the two axes coincide only in
the first full prefill.
Comment on lines +501 to +503
- **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.
Comment on lines +270 to +272
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
Comment on lines +331 to +333
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@titaiwangms Ti-Tai Wang (titaiwangms) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@chilo-ms

Copy link
Copy Markdown
Contributor Author

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):

  1. Gate Stage 2's smaller estimate explicitly on a hard, cache-mode-aware envelope and cuDNN-unreachability, else return Unavailable. The estimator already enforces the cuDNN case — BuildBounds marks a reachable cuDNN route and GetGQAWorkspaceAggregateForBounds returns Unavailable (group_query_attention_workspace_bounds.cc:241-244) — so the smaller estimate is never emitted when cuDNN is reachable; the Case A prose will say so.
  2. Correct the Flash worked example to cite FlashBackendEnvelope's structural max_splits = min(128, SM_count, ceil(kv/block_n)) bound (bounds.cc:104-148) rather than implying a single envelope corner is evaluated. Stage-4 profiling validates this bound, it does not replace it with a sampled peak.
  3. Relabel the knobs: session.max_shape_override as an estimation hint (not a runtime envelope enforcer) and ep.cuda.gqa_workspace_max_total_sequence_length as a proposed option — per the bot comments I'm also addressing — and note a hard runtime envelope check (query + KV) is required for the no-OOM claim.

I''ll push these as a follow-up commit on this branch for your re-review.

Chi Lo (chilo-ms) and others added 3 commits October 1, 2026 10:24
… 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
Chi Lo (chilo-ms) added a commit that referenced this pull request Oct 7, 2026
…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
Chi Lo (chilo-ms) added a commit that referenced this pull request Oct 7, 2026
… 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
eserscor pushed a commit that referenced this pull request Oct 7, 2026
…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
eserscor pushed a commit that referenced this pull request Oct 7, 2026
… 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>
@chilo-ms

Copy link
Copy Markdown
Contributor Author

Updated in 324f7c46c7 to make the proof boundary explicit: analytical upper bounds, empirical peaks, and the generic 50% heuristic are distinct; unavailable GQA proof bounds fail closed for capacity-aware CUDA placement; profiling validates/calibrates rather than replaces an upper bound; and whole-graph peak/lifetime accounting is separate from per-node route bounds. The caller-declared query/KV envelope remains explicitly non-enforced and is not presented as a no-OOM guarantee.

@titaiwangms Ti-Tai Wang (titaiwangms) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Label fail-closed placement on an unavailable GQA estimate as proposed, rather than current behavior.
  2. Separate embedding gather from the dense vocabulary projection performed by lm_head.
  3. Specify the execution/copy mechanism for MoE offload; redirecting an initializer to a CPU allocator alone is not CPU execution or GPU weight streaming.
  4. 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Chi Lo (chilo-ms) added a commit that referenced this pull request Oct 9, 2026
…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
Chi Lo (chilo-ms) added a commit that referenced this pull request Oct 9, 2026
## 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
Chi Lo (chilo-ms) added a commit that referenced this pull request Oct 9, 2026
…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
Chi Lo and others added 2 commits October 12, 2026 16:15
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
Chi Lo (chilo-ms) added a commit that referenced this pull request Oct 15, 2026
…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
Chi Lo (chilo-ms) added a commit that referenced this pull request Oct 16, 2026
… 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

@tianleiwu Tianlei Wu (tianleiwu) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +294 to +302
`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 |
|---|---|

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarify GPU-resident total_sequence_length phase classification for single-token first prompts and the no past input ⇒ prefill-only table row.

  1. In group_query_attention_helper.h:412-427, when total_sequence_length resides on the GPU (!is_total_seqlen_on_cpu), CheckInputs skips the prompt-type block entirely and leaves is_first_prompt = false and is_subsequent_prompt = false for all steps:
    • A single-token first prompt (sequence_length == 1, total_sequence_length == 1 on GPU) has !is_first_prompt && sequence_length == 1, so runtime classifies it as decode rather than prefill.
    • The batch_size == 1 guard for sequence_length > 1 with past context (group_query_attention_helper.h:417-420, cited on line 313 as the guard protecting max across phases) is also bypassed when total_sequence_length is on GPU.
  2. Line 302's table states no past input ⇒ prefill-only, whereas BuildBounds (group_query_attention_workspace_estimate.cc:244) currently requires past_key/past_value (if (!Present(shapes, kPastKey)) return std::nullopt;). Moreover, if a future prefill-only mode omitted decode and chunk from AdmittedPhases, any run with GPU-resident total_sequence_length would set is_first_prompt = false and classify sequence_length == 1 as decode and sequence_length > 1 as chunk, escaping a prefill-only phase mask.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +815 to +825
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Document how current post-partition callbacks and empty L2 declarations differ from the target verification invariants.

  1. Lines 815–818 state that orphaned bindings fail verification and A fused region requires its own contract, not the sum of constituent scratch peaks. Today, InferenceSession::TransformGraph (inference_session.cc:1882-1907) registers SetNodeReplacementCallback → ConsolidateWorkspaceReservations (resource_accountant.cc:26-46), which sums reservation.bytes across source_node_indices, and SetNodeRemovalCallback, which erases removed node_index entries from workspace_reservations before FinalizeSessionStateImpl's orphaned_reservations check (session_state.cc:2227-2236) runs.
  2. 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), if kernel->DeclareWorkspaceRequirements(...) returns Status::OK() with requirements.empty() for a node that has a nonzero L1 reservation, it increments missing_declaration and continues without setting strict_verification_failed.

Calling out both gaps in the integration table below (lines 860–869) would make the required Stage 4/5 framework changes explicit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_declaration without setting strict_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
Chi Lo (chilo-ms) added a commit that referenced this pull request Nov 6, 2026
…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
Chi Lo (chilo-ms) added a commit that referenced this pull request Nov 10, 2026
### 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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants