onnxruntime
646540ec - Fix NHWC second-pass EP assignment cleanup (#27858)

Commit
69 days ago
Fix NHWC second-pass EP assignment cleanup (#27858) ## Description This change fixes two related bugs in the NHWC two-pass partitioning flow used by layout-preferring execution providers (QNN, CUDA with `prefer_nhwc`, etc.): 1. **Stale EP ownership.** Nodes claimed during the first `GetCapability` pass could remain assigned to the EP even when the second pass (run after layout transformation) no longer selected them. That stale ownership stranded CPU-kernel nodes on the EP, causing initialization failures such as `Kernel not found` or invalid NHWC graph state. 2. **Phantom resource-accountant budget.** When resource-aware partitioning is enabled, the tentative first-pass tags committed memory budget that was never rolled back for nodes dropped in the second pass. That phantom budget leaked into later accounting decisions and could incorrectly starve subsequent partitions. The fix makes first-pass EP tags **tentative**: they exist only so the layout transformer and the second `GetCapability` pass can recognize candidate nodes, and they commit no budget. Budget is committed only for the nodes that survive the second pass. ## Summary of Changes ### Graph partitioning fix | File | Change | |------|--------| | `onnxruntime/core/framework/graph_partitioner.cc` | Track nodes that were only temporarily assigned during the first NHWC capability pass and clear those assignments for nodes the second pass no longer claims. | | `onnxruntime/core/framework/graph_partitioner.cc` | Clear temporary assignments on early exits from layout transformation failure and load cancellation so the graph is never left half-assigned. | | `onnxruntime/core/framework/graph_partitioner.cc` | Skip resource-accountant accounting during the tentative first pass (`TryAssignNodes`); capture per-node first-pass costs before `capabilities.clear()`, then commit budget only for survivors via a deferred-commit step. | | `include/onnxruntime/core/graph/indexed_sub_graph.h` | Add a read-only `GetNodeCost(size_t)` accessor so the deferred-commit step can reuse the costs computed in the first pass. | ### Regression coverage | File | Change | |------|--------| | `onnxruntime/test/internal_testing_ep/internal_testing_partitioning_tests.cc` | `NhwcSecondPassDropFallsBackFromCpuKernelNode`: custom NHWC test EP claims `Conv` and `LogSoftmax` on pass 1, drops `LogSoftmax` on pass 2, and verifies the dropped node falls back to a CPU kernel instead of staying owned by the EP. | | `onnxruntime/test/internal_testing_ep/internal_testing_partitioning_tests.cc` | `NhwcTwoPassAccountingCommitsOnlySurvivors`: accounting-aware NHWC test EP (registered as `kCudaExecutionProvider`) verifies budget is committed once for the surviving `Conv` and never for the dropped `LogSoftmax` (no phantom budget, no double-count). | | `onnxruntime/test/python/transformers/test_cuda_plugin_ep.py` | `test_nhwc_conv_with_resource_accounting`: CUDA plugin-EP smoke test combining NHWC opt-in with `session.resource_cuda_partitioning_settings`. | ### Documentation | File | Change | |------|--------| | `docs/annotated_partitioning/PartitioningWithAnnotationsAndMemoryConstraints.md` | Describe how tentative first-pass NHWC tags interact with resource budgeting (budget committed only for second-pass survivors). | | `docs/cuda_plugin_ep/cuda_plugin_ep_design.md` | Note that first-pass tags are tentative and that plugin EPs should attach costs only on first-pass capabilities, mirroring the in-tree CUDA EP. | ## NHWC opt-in note NHWC-preferred layout is opt-in: - In-tree CUDA EP: provider option `{"prefer_nhwc": "1"}` (alias `prefer_nhwc_layout`). - CUDA plugin EP: session option `{"ep.cuda.prefer_nhwc_layout": "1"}`. ## Testing Build the unit test target, then run: ``` ./onnxruntime_test_all --gtest_filter="InternalTestingEP.Nhwc*:ResourceAccountantTest.*:SessionStateTest.TestResourceAwarePartitioning*" ``` Before this fix, the second-pass drop scenario failed with: ``` [ONNXRuntimeError] : 9 : NOT_IMPLEMENTED : Failed to find kernel for LogSoftmax(13) (node:'node_token_0' ep:'TwoPassNhwcTestExecutionProvider'). Kernel not found ``` Verification performed: - CPU build: target builds, lint clean, targeted partitioning/accounting tests pass. - CUDA 13 build (NHWC ops enabled): builds clean; the filtered suite reports 11 tests passed, including the new `NhwcTwoPassAccountingCommitsOnlySurvivors` and the CUDA-only `SessionStateTest.TestResourceAwarePartitioning*` cases. ## Motivation and Context This was found during analysis of a "Windows x64 QNN CI Pipeline (static_lib)" test failure. QNN and other NHWC-preferred EPs use a two-pass partitioning flow around layout transformation. The first pass marks candidate nodes so layout transformation knows what to rewrite, but those assignments were not being rolled back before the second capability query. If the second pass rejected a previously claimed node, the node could remain incorrectly owned by the EP even though it was no longer part of a compiled partition. With resource-aware partitioning enabled, the dropped node's budget was also leaked. The new regression tests cover both control-flow paths without depending on end-to-end QNN behavior, keeping the failure focused on `GraphPartitioner` ownership and accounting semantics rather than backend-specific compilation details.
Author
Parents
Loading