Add workspace estimation accounting and reporting - #31962
Conversation
Move workspace reporting out of GetCapabilityForEP and into the completed GraphPartitioner::Partition boundary. Track workspace estimates as pending during capability probing and commit them only for nodes that ORT actually assigns. Replace the generic safety-margin workspace with the MatMulNBits Level-1 estimate before CUDA's budget decision. Preserve workspace estimates across the layout-transformation deferred-commit path, and report non-workspace, workspace, and total estimated memory once per resource-accounted EP. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cdd38fe6-fcf3-45c2-acea-b8e6206a7839
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cdd38fe6-fcf3-45c2-acea-b8e6206a7839
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cdd38fe6-fcf3-45c2-acea-b8e6206a7839
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cdd38fe6-fcf3-45c2-acea-b8e6206a7839
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cdd38fe6-fcf3-45c2-acea-b8e6206a7839
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cdd38fe6-fcf3-45c2-acea-b8e6206a7839
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cdd38fe6-fcf3-45c2-acea-b8e6206a7839
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cdd38fe6-fcf3-45c2-acea-b8e6206a7839
There was a problem hiding this comment.
Pull request overview
Adds workspace-memory estimation, accounting, and reporting for resource-aware CUDA graph partitioning.
Changes:
- Combines profile, estimator, and fallback workspace costs.
- Preserves workspace accounting across two-pass NHWC partitioning.
- Adds Level-1/Level-2 reporting and accountant tests.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
onnxruntime/core/framework/graph_partitioner.cc |
Commits and reports accepted workspace estimates. |
onnxruntime/core/framework/resource_accountant.cc |
Implements workspace selection and accounting. |
onnxruntime/core/framework/session_state.cc |
Reports Level-2 workspace declarations. |
onnxruntime/core/providers/cuda/cuda_execution_provider.cc |
Applies MatMulNBits estimates to budgets. |
include/onnxruntime/core/framework/resource_accountant.h |
Defines workspace accounting interfaces. |
include/onnxruntime/core/graph/indexed_sub_graph.h |
Commits complete per-node resources. |
onnxruntime/test/framework/resource_accountant_test.cc |
Tests workspace accounting policies. |
onnxruntime/test/internal_testing_ep/internal_testing_partitioning_tests.cc |
Updates renamed accountant reference. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Review summary (multi-agent pass: readability, correctness, adversarial, spec/math, cross-module)Reviewed the full diff. Two findings look like real semantic risk worth resolving before merge; the rest are minor/nits. Major1. No-profile path drops the safety floor entirely instead of taking
Suggested fix: 2. Stats-miss nodes silently go from cost=0 to fully-charged, and double-count shared initializers across the two accounting models Second-order issue: the stats path doesn't touch Suggested fix: either keep Minor
What's solid
Design-doc drift (multiple stale references to renamed methods/signatures in |
Separate runtime workspace, persistent prepack buffers, and temporary prepack scratch in Level-1 estimates, and conservatively charge each component during CUDA partitioning. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Re-review of latest push (draft → ready, 8→17 files)Re-ran the same review pass against the updated diff. Good progress — one of the two Major findings from the previous review is substantially addressed; the other two remain. Resolved / substantially addressedPrevious Major #1 (no floor against unknown allocations) — now addressed via a structural fix, not just a floor. One residual nuance: Still open1. [Major] Stats-file-miss node still goes from cost=0 to fully-charged fallback, with an unreconciled double-count risk The double-count concern also still stands: the stats-hit path doesn't touch 2. [Major] Plugin EP host bridge still uses the 1-arg Minor (unchanged from before)
New surface added since last review (not previously covered) — looks solid
Overall: solid iteration, the prepack-memory gap is closed. The stats-miss double-count and plugin-bridge divergence are the two things I'd still want addressed or explicitly deferred-with-tracking-issue before merge. |
Keep nodes missing from a supplied stats file at zero cost so profile-based sessions do not fall through to ad-hoc initializer and workspace bookkeeping. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add estimator-aware NHWC and CUDA budget coverage, make resource arithmetic checked, clarify per-graph Level-2 reporting, and document deferred plugin estimator parity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1faf9517-5062-4655-a967-3834143334d0
Extract Level-1 runtime workspace into explicitly initialized scalar state so GCC 14 reduced builds do not diagnose nested optional storage as maybe uninitialized. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1faf9517-5062-4655-a967-3834143334d0
Tianlei Wu (tianleiwu)
left a comment
There was a problem hiding this comment.
One blocking accounting issue remains. The structured lifetime split, checked byte arithmetic, dynamic-shape fallback, accepted-node commit semantics, and diagnostics look sound, and the earlier test/reporting requests are addressed on this head. However, the offline-prepacked GPU path is charged for a duplicate packed-weight allocation that PrePack_B does not make.
Review with build + test verificationI reviewed this PR and, since the description notes "Tests were not run for this update", I built the branch and ran the new tests on real hardware. Environment: 4× A100-SXM4-80GB (SM80), CUDA 12.9, cuDNN 9.10.2, Release, Build and test status: green
Note for anyone reproducing: the CUDA test cases need Critical — the ad-hoc path double-counts prepacked weights, and the headline test encodes that bugIn the ad-hoc branch of Per if (is_packed) {
++number_of_prepacks_counter_;
if (constant_initializers_use_count.count(input_name) && --constant_initializers_use_count[input_name] == 0) {
st->initialized_tensors_.erase(ort_value_idx);
constant_initialized_tensors.erase(ort_value_idx);
}
}So the original copy does not exist in steady state, and charging both is a genuine double count — not merely conservative, as Using the exact constants from the new e2e test (
The part I'd most like you to look at: this directly determines the outcome of The test's 500/600 KiB straddle works because of the double count. Remove it (charge That means the budget thresholds in the test were effectively back-derived from the current behavior. When fixing the accounting, please recompute them deliberately — otherwise the correction will look like a regression. One asymmetry worth capturing in the docs: this is only a double count on the ad-hoc path. On the profile path, Major — Level 1 omits the largest init-time allocation for this exact kernel
While Major — partitioning now depends on how fpA_intB was enabledThe estimator reads only the This PR removes the justification that made that divergence safe:
but keeps describing the Worth noting: my test run used the env-var path. No test covers the session-config path, so this gap is currently invisible to CI. Suggest resolving the effective option once at EP/session level, feeding both capability estimation and kernel creation, and adding a config-only budget regression test. Major — undeclared behavior change: max-shape inference now feeds ad-hoc output sizingThe new It also runs against Two small things in the same block:
Minor
Pre-existing, not introduced hereIn What I think is genuinely well done
Happy to share the exact build commands or rerun anything against a revision. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1faf9517-5062-4655-a967-3834143334d0
This is a valid observation about precision, but I want to clarify the CUDA initialization sequence. Before PrePack() runs, ORT’s allocation planner assigns each initializer to its first consumer’s device. SaveInitializedTensors() then allocates and copies all CUDA initializers to that device. Only afterward does ORT create kernels and invoke PrePack() sequentially node by node. Therefore, although the model’s external data may originally be mmap-backed on the host, the source initializers relevant to CUDA prepacking already consume CUDA device memory when PrePack() begins. For runtime prepacking, the source CUDA initializer and newly allocated packed destination coexist until the source’s final successful prepack consumer releases it. Packed destinations remain live for the session. Level-1 consequently reports a conservative initialization-time upper bound; it does not yet model the sequential release of source initializers or calculate the maximum live set across the prepack order. Exact peak accounting would require explicit source and destination lifetimes, initializer consumer counts, and prepack ordering. The offline-prepacked case is different. MatMulNBits::PrePack_B() reuses an already GPU-resident offline-prepacked B initializer in place and does not allocate another B destination. The base accountant still charges the retained initializer, while persistent_prepack_bytes now excludes the nonexistent duplicate B allocation. Newly allocated scale and zero-point prepack destinations remain included. A CUDA integration test verifies that the offline-prepacked weight is not charged twice. I’ve clarified this contract in Level1MemoryEstimate : it describes additional persistent prepack destinations and initialization scratch, while original CUDA initializer storage remains part of the accountant’s base initializer cost |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1faf9517-5062-4655-a967-3834143334d0
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1faf9517-5062-4655-a967-3834143334d0
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1faf9517-5062-4655-a967-3834143334d0
Document that Level-1 prepack estimates include only allocations additional to already-accounted initializers, and distinguish sequential MatMulNBits tactic profiling from PrePack scratch. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cdd38fe6-fcf3-45c2-acea-b8e6206a7839
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1faf9517-5062-4655-a967-3834143334d0
Tianlei Wu (tianleiwu)
left a comment
There was a problem hiding this comment.
Both of my earlier blocking threads are verified fixed on this head and resolved: pass-2 admission now sees the pass-1 survivor budget, and the lazy tactic-profiler scratch is reserved at max(configured bucket, RoundUpProfileM(M)) (or kMaxProfileM for dynamic M), each with a matching regression.
Two issues remain in the code that landed with those fixes.
-
The provisional reservation can leak permanently.
pass1_node_costsis built from every node of every accounting-enabled pass-1 capability, but the rollback loop iteratesnodes_temporarily_assigned_to_ep, whichTryAssignNodesonly populates for nodes it newly tagged. It returnsfalseup front (assigning nothing) when any node in a capability isnullptror already carries a different EP type, and it skips nodes that already carry this EP type. Those nodes keep theirAddConsumedAmountforever — phantom budget, which is exactly what the previous deferred-commit design avoided. Latent for the in-tree CUDA EP today, butGetCapabilityForEPis generic and nothing enforces the invariant. Deriving both sets fromnodes_temporarily_assigned_to_epis a one-line fix. -
Initialization scratch is summed per node although it is a shared, one-at-a-time allocation. Now that Level 1 gates the hard budget rather than only logging, charging the full fpA_intB tactic-profiler scratch to every
MatMulNBitsnode is a very large over-estimate:profileTacticsfreesworkspace_tmpbefore returning, ands_profilerManager'smMNKProfileMapis shared by all kernels so same-shape nodes allocate nothing at all. The direction is safe, but the magnitude can push entire models to CPU once a budget is configured.
Smaller items are inline. Also, per AGENTS.md, please build and run the tests before merge — the CUDA integration tests encode exact byte budgets that depend on the new profiler-scratch formula, and the PR description states tests were not run.
Positives worth calling out: replacing the collapsed workspace scalar with a structured Level1MemoryEstimate is the right model; RoundUpProfileM fixes a real signed-overflow in std::min(nextPowerOfTwo(v), getMaxProfileM()); bundling bytes and source in one WorkspaceEstimateSelection prevents the reported source from diverging from the charged size; and CommitWorkspaceEstimate computing all three totals into locals before assigning keeps state consistent when SafeInt throws.
Limit NHWC pass accounting to assigned nodes, report sequential initialization scratch as a peak, and narrow estimator configuration state. Align MatMulNBits profiler sizing and add regression coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4fe164a4-9b80-4d39-9e66-c97749980ef2
|
Addressed the latest review round in 539a10d:
The normal Release CUDA targets build, and all 22 targeted framework/accounting tests pass. The CUDA internal MatMulNBits test source also compiles; running that internal-test module on this Windows build remains blocked by the existing |
Resolve MatMulNBits eligibility and workspace-estimation conflicts while retaining the updated zero-point, bias, and compact-runner behavior from main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4fe164a4-9b80-4d39-9e66-c97749980ef2
Bracket the measured MatMulNBits additive estimate in the CUDA integration test and allow the intentional minimal-build framework size increase. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4fe164a4-9b80-4d39-9e66-c97749980ef2
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4fe164a4-9b80-4d39-9e66-c97749980ef2
Tianlei Wu (tianleiwu)
left a comment
There was a problem hiding this comment.
The earlier two-pass accounting, accepted-node commit, overflow handling, configuration, and profiler-sizing concerns are fixed on this head, and I resolved those addressed threads. One blocking lifetime mismatch remains in the existing MatMulNBits profiler-scratch thread: scratch for an unseen bucket can be allocated during Run(), but the new Level-1 estimate classifies all profiler scratch as initialization-only and excludes it from the hard partitioning budget. The same expression also sums non-overlapping constructor and PrePack scratch instead of reporting their peak. Details and the requested regression coverage are in the continued inline thread.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4fe164a4-9b80-4d39-9e66-c97749980ef2
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4fe164a4-9b80-4d39-9e66-c97749980ef2
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4fe164a4-9b80-4d39-9e66-c97749980ef2
Tianlei Wu (tianleiwu)
left a comment
There was a problem hiding this comment.
One blocking budget issue and one smaller reporting inconsistency remain. The structured lifetime split, checked arithmetic, two-pass NHWC reservation/rollback, shared estimator math, and the executed static lazy-profile regression are strong improvements. The earlier lazy-profile lifetime concern is fixed on this head; however, the estimator still treats a propagated max-shape hint like an exact runtime M, so sparse initial profile buckets can leave a valid smaller runtime bucket's lazy scratch unreserved.
Preserve planning-bound shape provenance when estimating lazy tactic profiling scratch, and attribute transient-only Level-1 estimates in stats-backed accounting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4fe164a4-9b80-4d39-9e66-c97749980ef2
Tianlei Wu (tianleiwu)
left a comment
There was a problem hiding this comment.
The bounded-shape and stats-attribution fixes on this head are correct, and I resolved those prior threads. Three issues remain: two newly added MatMulNBits assertions conflict with the lazy-profile accounting and fail current CUDA/TensorRT CI; NHWC pass-two admission can permanently reject a candidate based on a provisional cost that is immediately rolled back; and the roadmap still documents the opposite initialization-scratch policy. Details are inline.
Retry final NHWC capability admission after dropped provisional costs release a stopped budget, update bounded MatMulNBits assertions, and align accounting documentation with implemented lifetimes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1faf9517-5062-4655-a967-3834143334d0
Rename temporary prepack memory to initialization scratch and document runtime transient peak accounting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolve the Android minimal binary threshold by combining the main and workspace-accounting size increases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
The NHWC retry path can remove a surviving node’s reservation and subsequently admit it at zero cost, violating the configured memory budget.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 23/23 changed files
- Comments generated: 2
- Review effort level: Balanced
Preserve confirmed pass-one survivor reservations and fused capabilities when a budgeted second pass stops before reaching them. Add ordering and fused-capability regressions, and clarify initialization scratch reporting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Recompute surviving pass-one costs after complete discovery so shared initializers remain deduplicated, preserve single-node compiled survivor capabilities, and add focused regressions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1faf9517-5062-4655-a967-3834143334d0
Re-probe mixed and optimization-only survivors, preserve complete survivor capability groups, and avoid replacing accounted overlaps that own pending resource state. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reconcile accounted survivor overlaps before final admission, rebuild shared-initializer ownership for the retained set, and retry when budget truncation changes the result. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1faf9517-5062-4655-a967-3834143334d0
Use stable node indices for post-partition assertions because function inlining may delete the original graph nodes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1faf9517-5062-4655-a967-3834143334d0
Tianlei Wu (tianleiwu)
left a comment
There was a problem hiding this comment.
Summary
This update completes workspace-memory accounting and reporting for resource-constrained graph partitioning, specifically integrating Level-1 and Level-2 memory estimation across the two-pass NHWC layout transformation flow.
Key Highlights
- Survivor Discovery & Reconciliation: Pass-2 survivor discovery is executed without budget gating, ensuring later survivors are not prematurely dropped due to an early budget cutoff. Reservations for all confirmed survivors are rebuilt before evaluating pass-2 candidate admission.
- Compiling Capabilities: Compiling/fused survivor capabilities are reconciled iteratively against accountant-aware groupings to guarantee stable convergence.
- Shared Initializers: Shared initializers are charged exactly once to the finalized survivor set, avoiding double-counting or phantom reservations.
- Documentation & Inventory: Diagnostic, non-additive initialization scratch is clearly separated from budgeted runtime workspace and persistent prepack memory.
- Test Coverage: Added
NhwcTwoPassAccountingPreservesLaterSurvivorReservationcovering theRelu -> Conv -> LogSoftmax1000 KiB scenario along with comprehensive tests for fused, overlapping, and mixed survivor capabilities. Test node lifetime has been hardened to use stable node indices.
All prior review comments have been verified and resolved, and all CI pipelines are passing.
## Summary - Preserve accepted per-node workspace reservations after graph partitioning and through post-partition graph mutations. - Compare final Level-2 workspace declarations with the reservations used during partitioning. - Report larger, equal, smaller, missing-declaration, missing-reservation, and orphaned-reservation counts and byte totals per graph. - Warn by default on declaration overruns and support strict constrained-memory validation with `session.strict_workspace_verification=1`. ## Behavior and scope Verification runs after kernels are created and constant tensors are prepacked, when final Level-2 requirements are available. Strict mode fails session initialization when a declaration exceeds its reservation or when an untracked mutation leaves a nonzero orphaned reservation. Missing partition reservations and zero-byte orphans remain diagnostic-only. Reservation ownership now follows fused capabilities, selector actions, shared and manual fusions, bottom-up removals, intentional eliminations, and training recomputation clones. This includes MatMulScale and QDQ cleanup, Attention and EmbedLayerNorm cleanup paths, Level-4 initializer fusion, GroupQueryAttention, BitmaskDropout, BiasSoftmaxDropout, SCE loss gradient, Triton, and MemoryOptimizer transformations. Strict verification is rejected for ORT-format loads because partition-time reservations are not serialized. Reservation storage and verification remain excluded from ordinary minimal builds; extended-minimal ORT-format loads reject strict mode explicitly. This PR does not repartition the graph or change runtime workspace allocation. #31962 introduced Level-1 workspace estimation and reporting; activation-aware CUDA workspace preallocation remains follow-up work in #32071. ## Validation - Windows Release `onnxruntime_test_all` build: 123 focused tests passed, 4 hardware-dependent tests skipped. - Windows training-enabled Release build: all 10 focused mutation-accounting tests passed. - Windows extended-minimal build: strict ORT-format rejection test passed. - Windows no-ops MinSizeRel minimal shared-library build passed. - `lintrunner` and `git diff --check` passed. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cdd38fe6-fcf3-45c2-acea-b8e6206a7839 Copilot-Session: 1faf9517-5062-4655-a967-3834143334d0 Copilot-Session: 991be248-6618-43aa-8d4b-5305e52bcd40
Description
Adds workspace-memory accounting and reporting for resource-constrained graph partitioning.
GetCapabilityand commits it only for accepted nodesmax(profile, estimator)when both exist, and fallback when neither is availableMatMulNBitsworkspace estimator before CUDA budget decisionsSizeBasedResourceAccountantto reflect that it handles profile, fallback, and operator-estimated costsRelated to #29775.
Testing
Added resource-accountant and two-pass partitioning unit coverage, including profile-versus-estimator comparison reporting. Tests were not run for this update.