From 18b5d2bf7b092fef0f0ebf97cff3f06265ab781a Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Wed, 21 Jan 2026 12:10:26 -0800 Subject: [PATCH 01/57] Introduce annotation query function Add session options string and parsing code along with the unit test Introduce layering configuration Refine LayeringRuleMatcher and add tests Add OrtEpDevice matching logic and tests Change the Matcher interface to match one rule against pontentially many devices Add matching again tranditional EPs Create LayeringIndex Add LayeringIndex and tests Adjust config parsing to detect errrors Adjust Create sig Implement WeightsSizeBasedAccountant --- .../core/framework/resource_accountant.h | 16 +- include/onnxruntime/core/graph/graph.h | 15 + .../onnxruntime_session_options_config_keys.h | 15 + .../core/framework/graph_partitioner.cc | 2 +- .../core/framework/layering_annotations.cc | 513 ++++++++++ .../core/framework/layering_annotations.h | 253 +++++ .../core/framework/resource_accountant.cc | 98 +- .../core/framework/tensorprotoutils.cc | 13 + onnxruntime/core/framework/tensorprotoutils.h | 10 + onnxruntime/core/graph/graph.cc | 7 + .../providers/cuda/cuda_execution_provider.cc | 6 +- .../framework/layering_annotations_test.cc | 957 ++++++++++++++++++ .../test/framework/session_state_test.cc | 1 + .../test/framework/tensorutils_test.cc | 58 ++ 14 files changed, 1942 insertions(+), 22 deletions(-) create mode 100644 onnxruntime/core/framework/layering_annotations.cc create mode 100644 onnxruntime/core/framework/layering_annotations.h create mode 100644 onnxruntime/test/framework/layering_annotations_test.cc diff --git a/include/onnxruntime/core/framework/resource_accountant.h b/include/onnxruntime/core/framework/resource_accountant.h index b072e27816463..99bfcca869572 100644 --- a/include/onnxruntime/core/framework/resource_accountant.h +++ b/include/onnxruntime/core/framework/resource_accountant.h @@ -45,12 +45,16 @@ class IResourceAccountant { virtual ResourceCount GetConsumedAmount() const = 0; virtual void AddConsumedAmount(const ResourceCount& amount) = 0; virtual void RemoveConsumedAmount(const ResourceCount& amount) = 0; - virtual ResourceCount ComputeResourceCount(const Node& node) const = 0; + virtual ResourceCount ComputeResourceCount(const Node& node) = 0; std::optional GetThreshold() const { return threshold_; } + void SetThreshold(const ResourceCount& threshold) { + threshold_ = threshold; + } + void SetStopAssignment() noexcept { stop_assignment_ = true; } @@ -114,11 +118,6 @@ class NodeStatsRecorder { void DumpStats(const std::filesystem::path& model_path) const; - [[nodiscard]] static Status CreateAccountants( - const ConfigOptions& config_options, - const std::filesystem::path& model_path, - std::optional& acc_map); - private: void DumpStats(std::ostream& os) const; @@ -126,4 +125,9 @@ class NodeStatsRecorder { std::unique_ptr impl_; }; +Status CreateAccountants( + const ConfigOptions& config_options, + const std::filesystem::path& model_path, + std::optional& acc_map); + } // namespace onnxruntime diff --git a/include/onnxruntime/core/graph/graph.h b/include/onnxruntime/core/graph/graph.h index 804f4557fd321..79a6bd7cde779 100644 --- a/include/onnxruntime/core/graph/graph.h +++ b/include/onnxruntime/core/graph/graph.h @@ -175,6 +175,15 @@ class Node { void SetSinceVersion(int since_version) noexcept { since_version_ = since_version; } #if !defined(ORT_MINIMAL_BUILD) + void SetLayeringAnnotation(std::string annotation) { layering_annotation_ = std::move(annotation); } + + const std::string& GetLayeringAnnotation() const { return layering_annotation_; } + + void ClearLayeringAnnotation() { + std::string t; + layering_annotation_.swap(t); + } + /** Gets the Node's OpSchema. @remarks The graph containing this node must be resolved, otherwise nullptr will be returned. */ const ONNX_NAMESPACE::OpSchema* Op() const noexcept { return op_; } @@ -568,6 +577,8 @@ class Node { friend class Graph; Node(NodeIndex index, Graph& graph) : index_(index), graph_(&graph), can_be_saved_(true) {} + const Graph* GetContainingGraph() const noexcept { return graph_; } + protected: #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) // internal only method to allow selected classes to directly alter the input/output definitions and arg counts @@ -685,6 +696,10 @@ class Node { // Graph instances for subgraphs that are owned by this Node std::vector> subgraphs_; +#if !defined(ORT_MINIMAL_BUILD) + std::string layering_annotation_; +#endif + // Can be saved? The node cannot be saved anymore if removable attributes have been cleared. bool can_be_saved_; }; diff --git a/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h b/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h index 1ea147a0079cc..6701c19084c12 100644 --- a/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h +++ b/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h @@ -332,6 +332,21 @@ static const char* const kOrtSessionOptionsCollectNodeMemoryStatsToFile = "sessi static const char* const kOrtSessionOptionsResourceCudaPartitioningSettings = "session.resource_cuda_partitioning_settings"; +/// +/// This is a setting that contains string annotations or annotation prefixes to be matched +/// against individual nodes metadata entry 'layer_ann' to guide layer assignment during partitioning. +/// The value is a semicolon separated list of strings or string prefixes per device. +/// Format: device1(annotation1, annotation2, ...); device2(annotation1, =annotation3, ...);... +/// Where: +/// - device1, device2, ... are the recognized device names to be matched against EPs configured in +/// the given session. +/// - annotation1, annotation2, ... are the exact annotation strings to be matched against node annotations +/// - =annotation3 indicates a prefix match for annotation3. Any node annotation that starts with +/// 'annotation3' will be matched. +/// TODO: add a list of recognized devices here. +/// +static const char* const kOrtSessionOptionsLayerAssignmentSettings = "session.layer_assignment_settings"; + // Enable EP context feature to dump the partitioned graph which includes the EP context into Onnx file. // The dumped Onnx model with EP context can be used for future inference to avoid the EP graph partitioning/compile overhead. // "0": disable. (default) diff --git a/onnxruntime/core/framework/graph_partitioner.cc b/onnxruntime/core/framework/graph_partitioner.cc index 9cb2111670ba6..810613a55caf5 100644 --- a/onnxruntime/core/framework/graph_partitioner.cc +++ b/onnxruntime/core/framework/graph_partitioner.cc @@ -1323,7 +1323,7 @@ Status GraphPartitioner::Partition(Graph& graph, FuncManager& func_mgr, // We use this only if Resource Aware Partitioning is enabled for any of the EPs // The map is empty if not created if not enabled std::optional ep_acc_map; - ORT_RETURN_IF_ERROR(NodeStatsRecorder::CreateAccountants(config_options, graph.ModelPath(), ep_acc_map)); + ORT_RETURN_IF_ERROR(CreateAccountants(config_options, graph.ModelPath(), ep_acc_map)); bool disable_model_compile = config_options.GetConfigOrDefault(kOrtSessionOptionsDisableModelCompile, "0") == "1"; ORT_RETURN_IF_ERROR(PartitionOnnxFormatModel(partition_params, mode, providers_, kernel_registry_mgr_, diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc new file mode 100644 index 0000000000000..f4f6c9e37c47b --- /dev/null +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -0,0 +1,513 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "core/graph/constants.h" +#include "core/common/narrow.h" +#include "core/common/string_utils.h" +#include "core/framework/layering_annotations.h" +#include "core/framework/ortmemoryinfo.h" +#include "core/session/abi_devices.h" +#include "core/framework/execution_providers.h" +#include "core/graph/graph.h" + +#include + +namespace onnxruntime { + +common::Status LayeringRules::FromConfigString(const std::string& config_value, LayeringRules& rules) { + rules.rules.clear(); + if (config_value.empty()) { + return common::Status::OK(); + } + + auto entries = utils::SplitString(config_value, ";"); + for (const auto& e : entries) { + auto entry = utils::TrimString(e); + if (entry.empty()) { + continue; + } + + const size_t open_paren = entry.find('('); + const size_t close_paren = entry.find(')'); + + if (open_paren == std::string::npos) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid layering config: Missing '(' in entry: ", entry); + } + if (close_paren == std::string::npos) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid layering config: Missing ')' in entry: ", entry); + } + if (close_paren < open_paren) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid layering config: ')' comes before '(' in entry: ", entry); + } + + std::string device = entry.substr(0, open_paren); + device = utils::TrimString(device); + + if (device.empty()) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid layering config: Empty device name in entry: ", entry); + } + + std::string annotations_list = entry.substr(open_paren + 1, close_paren - open_paren - 1); + auto annotations = utils::SplitString(annotations_list, ","); + for (auto& a : annotations) { + auto ann = utils::TrimString(a); + if (ann.empty()) { + continue; + } + + bool prefix_match = false; + if (ann[0] == '=') { + prefix_match = true; + ann = ann.substr(1); + ann = utils::TrimString(ann); + } + + if (ann.empty()) { + continue; + } + + rules.rules.push_back({device, std::move(ann), prefix_match}); + } + } + + return common::Status::OK(); +} + +LayeringRuleMatcher::LayeringRuleMatcher(const LayeringRules& rules) { + for (size_t i = 0; i < rules.rules.size(); ++i) { + const auto& rule = rules.rules[i]; + ORT_ENFORCE(!rule.annotation.empty(), "Layering rule annotation cannot be empty"); + if (rule.prefix_match) { + AddPrefixRule(rule.annotation, i); + } else { + AddExactRule(rule.annotation, i); + } + } +} + +std::optional LayeringRuleMatcher::Match(const std::string& node_annotation) const { + std::optional best_match = std::nullopt; + + // 1. Check Prefix Matches via Trie. Prefix have priority over exact matches. + const TrieNode* current = &root_; + + // No empty annotations + // so we omit checking the root. + + for (char c : node_annotation) { + if (best_match && *best_match == 0) { + // Optimization: If we already found index 0, we can't do better. + return best_match; + } + + auto child_it = current->children.find(c); + if (child_it == current->children.end()) { + break; + } + current = child_it->second.get(); + if (current->rule_index) { + UpdateBestMatch(best_match, *current->rule_index); + } + } + + if (best_match) { + return best_match; + } + + // 2. Check Exact Matches (fallback) + auto it = exact_match_rules_.find(node_annotation); + if (it != exact_match_rules_.end()) { + best_match = it->second; + } + + return best_match; +} + +namespace { +bool CaseInsensitiveCompare(std::string_view a, std::string_view b) { + return std::equal(a.begin(), a.end(), b.begin(), b.end(), + [](char c1, char c2) { return std::tolower(c1) == std::tolower(c2); }); +} + +bool TryParseIndex(const std::string& str, uint32_t& index) { + char* end = nullptr; + const char* ptr = str.c_str(); + errno = 0; + unsigned long val = std::strtoul(ptr, &end, 10); + if (errno != 0 || end != ptr + str.size()) { + return false; + } + index = narrow(val); + return true; +} +} // namespace + +std::optional EpLayeringMatcher::Match(gsl::span ep_devices, + const LayerAnnotation& rule) { + const std::string& target_full = rule.device; + const auto colon_pos = target_full.find(':'); + const std::string target_type_str = (colon_pos == std::string::npos) ? target_full : target_full.substr(0, colon_pos); + // vendor or index or uuid, if present + std::string target_specifier; + if (colon_pos != std::string::npos) { + target_specifier = target_full.substr(colon_pos + 1); + } + + for (const auto* ep_device_ptr : ep_devices) { + if (!ep_device_ptr) { + continue; + } + const OrtEpDevice& ep_device = *ep_device_ptr; + + bool matched = false; + + // Helper to check device type from MemInfo if Hardware device logic fails/is absent + auto check_mem_device_type = [&](OrtDevice::DeviceType type) -> bool { + if (ep_device.device_memory_info) { + return ep_device.device_memory_info->device.Type() == type; + } + return false; + }; + + // 1. Exact Name / Alias match + // "cpu" + if (CaseInsensitiveCompare(target_type_str, "cpu")) { + if (ep_device.ep_name == kCpuExecutionProvider) { + matched = true; + } else if (ep_device.device && ep_device.device->type == OrtHardwareDeviceType_CPU) { + matched = true; + } else if (check_mem_device_type(OrtDevice::CPU)) { + matched = true; + } + } // "gpu" + else if (CaseInsensitiveCompare(target_type_str, "gpu")) { + // If simple "gpu" + if (target_specifier.empty()) { + if (ep_device.device && ep_device.device->type == OrtHardwareDeviceType_GPU) { + matched = true; + } else if (check_mem_device_type(OrtDevice::GPU)) { + matched = true; + } // Heuristic fallback for common GPU EPs if hardware info is missing. Should we also check for TRT here? + else if (ep_device.ep_name == kCudaExecutionProvider || ep_device.ep_name == kDmlExecutionProvider) { + matched = true; + } + } else { + // "gpu:" or "gpu:" + if (ep_device.device && ep_device.device->type == OrtHardwareDeviceType_GPU) { + uint32_t index = std::numeric_limits::max(); + if (TryParseIndex(target_specifier, index)) { + // gpu: + if (ep_device.device->device_id == index) { + matched = true; + } + } else { + // gpu: + if (CaseInsensitiveCompare(ep_device.device->vendor, target_specifier)) { + matched = true; + } + // Check against vendor ID + else if (CaseInsensitiveCompare(target_specifier, "nvidia") && + ep_device.device->vendor_id == OrtDevice::VendorIds::NVIDIA) { + matched = true; + } else if (CaseInsensitiveCompare(target_specifier, "amd") && + ep_device.device->vendor_id == OrtDevice::VendorIds::AMD) { + matched = true; + } else if (CaseInsensitiveCompare(target_specifier, "intel") && + ep_device.device->vendor_id == OrtDevice::VendorIds::INTEL) { + matched = true; + } + // Special shortcuts heuristics: gpu:nvidia -> CUDA + else if (CaseInsensitiveCompare(target_specifier, "nvidia") && + ep_device.ep_name == kCudaExecutionProvider) { + matched = true; + } + } + } + } + } + // "accelerator" (not cpu) + else if (CaseInsensitiveCompare(target_type_str, "accelerator")) { + if (ep_device.ep_name != kCpuExecutionProvider) { + // If we don't have HW info, assuming non-CPU EP is an accelerator. + // If we do have HW info, check it's not CPU. + const bool is_cpu_hw = (ep_device.device && ep_device.device->type == OrtHardwareDeviceType_CPU); + const bool is_cpu_mem = check_mem_device_type(OrtDevice::CPU); + + if (!is_cpu_hw && !is_cpu_mem) { + matched = true; + } + } + } // "npu" + else if (CaseInsensitiveCompare(target_type_str, "npu")) { + if (ep_device.device && ep_device.device->type == OrtHardwareDeviceType_NPU) { + matched = true; + } else if (ep_device.ep_name == kQnnExecutionProvider || ep_device.ep_name == kVitisAIExecutionProvider) { + // Heuristic for known NPU providers if HW device info is missing + // XXX: These can run on CPU as well, need to see if there any check that is missing. + matched = true; + } + } + // "fpga" + else if (CaseInsensitiveCompare(target_type_str, "fpga")) { + // No OrtHardwareDeviceType_FPGA currently, rely on OrtDevice::FPGA + if (check_mem_device_type(OrtDevice::FPGA)) { + matched = true; + } + } + // "cuda" + else if (CaseInsensitiveCompare(target_type_str, "cuda")) { + if (ep_device.ep_name == kCudaExecutionProvider) { + matched = true; + } + } + // "dml" + else if (CaseInsensitiveCompare(target_type_str, "dml")) { + if (ep_device.ep_name == kDmlExecutionProvider) { + matched = true; + } + } + // Fallback: Exact EP name string match (e.g. "MyCustomEP") + else if (ep_device.ep_name == target_full) { + matched = true; + } + + if (matched) { + return ep_device.ep_name; + } + } + + return std::nullopt; +} + +std::optional EpLayeringMatcher::Match(const ExecutionProviders& providers, const LayerAnnotation& rule) { + const std::string& target_full = rule.device; + const auto colon_pos = target_full.find(':'); + const std::string target_type_str = (colon_pos == std::string::npos) ? target_full : target_full.substr(0, colon_pos); + std::string target_specifier; + if (colon_pos != std::string::npos) { + target_specifier = target_full.substr(colon_pos + 1); + } + + for (const auto& ep_shared_ptr : providers) { + if (!ep_shared_ptr) { + continue; + } + const IExecutionProvider& ep = *ep_shared_ptr; + const std::string& ep_name = ep.Type(); + const OrtDevice& device = ep.GetDevice(); + + bool matched = false; + + // 1. Exact Name / Alias match + // "cpu" + if (CaseInsensitiveCompare(target_type_str, "cpu")) { + if (ep_name == kCpuExecutionProvider) { + matched = true; + } else if (device.Type() == OrtDevice::CPU) { + matched = true; + } + } // "gpu" + else if (CaseInsensitiveCompare(target_type_str, "gpu")) { + // If simple "gpu" + if (target_specifier.empty()) { + if (device.Type() == OrtDevice::GPU) { + matched = true; + } // Heuristics, XXX: Should we also check for TRT here? + else if (ep_name == kCudaExecutionProvider || ep_name == kDmlExecutionProvider) { + matched = true; + } + } else { + // "gpu:" or "gpu:" + if (device.Type() == OrtDevice::GPU) { + uint32_t index = std::numeric_limits::max(); + if (TryParseIndex(target_specifier, index)) { + // gpu: + if (device.Id() == static_cast(index)) { + matched = true; + } + } else { + // gpu: checking against Vendor ID + if (CaseInsensitiveCompare(target_specifier, "nvidia") && + device.Vendor() == OrtDevice::VendorIds::NVIDIA) { + matched = true; + } else if (CaseInsensitiveCompare(target_specifier, "amd") && + device.Vendor() == OrtDevice::VendorIds::AMD) { + matched = true; + } else if (CaseInsensitiveCompare(target_specifier, "intel") && + device.Vendor() == OrtDevice::VendorIds::INTEL) { + matched = true; + } + // Special shortcuts heuristics: gpu:nvidia -> CUDA + else if (CaseInsensitiveCompare(target_specifier, "nvidia") && ep_name == kCudaExecutionProvider) { + matched = true; + } + } + } + } + } + // "accelerator" (not cpu) + else if (CaseInsensitiveCompare(target_type_str, "accelerator")) { + if (ep_name != kCpuExecutionProvider) { + if (device.Type() != OrtDevice::CPU) { + matched = true; + } + } + } // "npu" + else if (CaseInsensitiveCompare(target_type_str, "npu")) { + if (device.Type() == OrtDevice::NPU) { + matched = true; + } else if (ep_name == kQnnExecutionProvider || ep_name == kVitisAIExecutionProvider) { + matched = true; + } + } + // "fpga" + else if (CaseInsensitiveCompare(target_type_str, "fpga")) { + if (device.Type() == OrtDevice::FPGA) { + matched = true; + } + } + // "cuda" + else if (CaseInsensitiveCompare(target_type_str, "cuda")) { + if (ep_name == kCudaExecutionProvider) { + matched = true; + } + } + // "dml" + else if (CaseInsensitiveCompare(target_type_str, "dml")) { + if (ep_name == kDmlExecutionProvider) { + matched = true; + } + } + // Fallback: Exact EP name string match (e.g. "MyCustomEP") + else if (ep_name == target_full) { + matched = true; + } + + if (matched) { + return ep_name; + } + } + + return std::nullopt; +} + +LayeringIndex LayeringIndex::Create(Graph& graph, + EpNameToLayeringIndices ep_map, + LayeringIndexToEpName rule_map, + const LayeringRuleMatcher& matcher) { + // 1. Create LayeringIndex instance with pre-computed maps + LayeringIndex index(std::move(ep_map), std::move(rule_map)); + + // 2. Traverse the graph and index nodes + index.ProcessGraph(graph, matcher, std::nullopt); + + return index; +} + +Status LayeringIndex::Create(Graph& graph, + const std::string& config_string, + gsl::span ep_devices, + const ExecutionProviders& ep_providers, + const logging::Logger& logger, + std::optional& layering_index) { + LayeringRules rules; + ORT_RETURN_IF_ERROR(LayeringRules::FromConfigString(config_string, rules)); + + LOGS(logger, INFO) << "Parsed " << rules.rules.size() << " layering rules from config."; + + if (rules.rules.empty()) { + // Return no index indicating no layering + layering_index.reset(); + return Status::OK(); + } + + // Identify which EPs satisfy which rules + EpNameToLayeringIndices ep_map; + LayeringIndexToEpName rule_map; + + size_t matched_rule_count = 0; + + for (size_t i = 0, lim = rules.rules.size(); i < lim; ++i) { + const auto& rule = rules.rules[i]; + + // 1. Try matching against ep_devices (from session options) + std::optional matched_ep = EpLayeringMatcher::Match(ep_devices, rule); + + // 2. If not matched, try matching against Registered EPs + if (!matched_ep) { + matched_ep = EpLayeringMatcher::Match(ep_providers, rule); + } + + if (matched_ep) { + const std::string& ep_type = *matched_ep; + ep_map[ep_type].insert(i); + // Ensure 1:1 mapping from rule index to EP type + // Note: A rule index refers to a unique entry in LayeringRules::rules vector. + // So 'i' is unique. + rule_map[i] = ep_type; + matched_rule_count++; + LOGS(logger, VERBOSE) << "Layering Rule " << i << " (" << rule.device << " -> " << rule.annotation + << ") mapped to EP: " << ep_type; + } else { + LOGS(logger, WARNING) << "Layering Rule " << i << " (" << rule.device << " -> " << rule.annotation + << ") could not be mapped to any available Execution Provider."; + } + } + + LOGS(logger, INFO) << "LayeringIndex created. Matched " << matched_rule_count + << " out of " << rules.rules.size() << " rules to available Execution Providers."; + + LayeringRuleMatcher matcher(rules); + layering_index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), matcher); + return Status::OK(); +} + +// Process to to bottom-up assign layering indices to nodes +void LayeringIndex::ProcessGraph(Graph& graph, const LayeringRuleMatcher& matcher, + std::optional parent_layer_id) { + // 3. Create entry for this graph instance + GraphLayeringIndex& current_graph_index = graph_index_[&graph]; + + for (auto& node : graph.Nodes()) { + std::optional matched_rule_idx = std::nullopt; + + // 4. For every node query its annotation + const std::string& annotation = node.GetLayeringAnnotation(); + if (!annotation.empty()) { + // If it has an annotation try to match it + matched_rule_idx = matcher.Match(annotation); + // Save memory and clear the annotation since it's no longer needed + node.ClearLayeringAnnotation(); + } + + // 5. If node has no annotation, inherit from subgraph parent node + if (!matched_rule_idx && parent_layer_id) { + matched_rule_idx = parent_layer_id; + } + + // Record assignment if we have a match + if (matched_rule_idx) { + const size_t rule_idx = *matched_rule_idx; + + // Only assign if this rule maps to a valid EP in our configuration + if (layering_index_to_ep_name_.count(rule_idx)) { + ORT_IGNORE_RETURN_VALUE(current_graph_index.node_to_layering_index_.insert_or_assign(node.Index(), rule_idx)); + ORT_IGNORE_RETURN_VALUE(current_graph_index.layer_to_node_ids_[rule_idx].insert(node.Index())); + } else { + // reset since no valid EP mapping + // so it does not propagate to sub-graphs if any + matched_rule_idx = std::nullopt; + } + } + + // Recurse for subgraphs + if (node.ContainsSubgraph()) { + const std::optional subgraph_parent_assignment = matched_rule_idx; + for (auto& [attr_name, subgraph] : node.GetMutableMapOfAttributeNameToSubgraph()) { + ProcessGraph(*subgraph, matcher, subgraph_parent_assignment); + } + } + } +} + +} // namespace onnxruntime \ No newline at end of file diff --git a/onnxruntime/core/framework/layering_annotations.h b/onnxruntime/core/framework/layering_annotations.h new file mode 100644 index 0000000000000..8a7cd7dba6cd2 --- /dev/null +++ b/onnxruntime/core/framework/layering_annotations.h @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#pragma once + +#include "core/common/inlined_containers.h" +#include "core/common/status.h" +#include "core/graph/basic_types.h" +#include "core/common/logging/logging.h" +#include "gsl/gsl" +#include +#include +#include +#include + +struct OrtEpDevice; + +namespace onnxruntime { +class ExecutionProviders; +class Graph; + +/// +/// Annotation extracted from kOrtSessionOptionsLayerAssignmentSettings session configuration option. +/// +struct LayerAnnotation { + std::string device; + std::string annotation; + bool prefix_match; +}; + +/// +/// This struct is a container for layering rules extracted from the kOrtSessionOptionsLayerAssignmentSettings +/// session configuration option. +/// +struct LayeringRules { + std::vector rules; + /// + /// Parses the layering rules from the given configuration string. + /// + /// The configuration string to parse. + /// Output parameter where the parsed rules will be stored. + /// Status indicating success or failure (e.g. due to format errors). + static common::Status FromConfigString(const std::string& config_value, LayeringRules& rules); +}; + +/// +/// This class matches node annotations against layering rules. +/// +class LayeringRuleMatcher { + public: + explicit LayeringRuleMatcher(const LayeringRules& rules); + + /// + /// The method returns the index of the best matching rule for the given annotation + /// if it exists + /// + /// annotation retrieved from protobuf node metadata + /// index of the matching LayeringRule if it exists + std::optional Match(const std::string& node_annotation) const; + + private: + struct TrieNode { + InlinedHashMap> children; + std::optional rule_index; + }; + + TrieNode root_; + InlinedHashMap exact_match_rules_; + + void AddExactRule(const std::string& annotation, size_t index) { + // Only store the first occurrence (lowest index) + exact_match_rules_.insert({annotation, index}); + } + + void AddPrefixRule(const std::string& annotation, size_t index) { + TrieNode* current = &root_; + for (char c : annotation) { + auto p = current->children.insert({c, nullptr}); + if (p.second) { + p.first->second = std::make_unique(); + } + current = p.first->second.get(); + } + + // Only store if strictly better (lower index) or not set + // Since we iterate rules 0..N, if a rule index is already set for this node, + // it corresponds to a higher priority rule, so we skip overwriting it. + if (!current->rule_index) { + current->rule_index = index; + } + } + + void UpdateBestMatch(std::optional& current_best, size_t candidate) const { + if (!current_best || candidate < *current_best) { + current_best = candidate; + } + } +}; + +namespace EpLayeringMatcher { +/// +/// Matches a list of available OrtEpDevices against the device string specified in the LayerAnnotation. +/// Returns the EP Type string of the first device that matches the rule. +/// +/// The list of available EP devices. +/// The rule containing the device designator. +/// Optional containing the matched EP type, nullopt otherwise. +std::optional Match(gsl::span ep_devices, + const LayerAnnotation& rule); + +/// +/// Matches a collection of ExecutionProviders against the device string specified in the LayerAnnotation. +/// Returns the EP Type string of the first provider that matches the rule. +/// +/// The collection of available Execution Providers. +/// The rule containing the device designator. +/// Optional containing the matched EP type, nullopt otherwise. +std::optional Match(const ExecutionProviders& providers, const LayerAnnotation& rule); +}; // namespace EpLayeringMatcher + +// This class contains indexing information about the entire graph +// per sub-graph info is stored in graph_index_ +class LayeringIndex { + public: + // mapping of EP name/type to a set of LayeringRule indices mapped to that EP. + using EpNameToLayeringIndices = InlinedHashMap>; + // mapping of LayeringRule index to EP name/type, reverse of the above + using LayeringIndexToEpName = InlinedHashMap; + + /// + /// Creates a fully initialized LayeringIndex. + /// + /// The graph to traverse and index. + /// Pre-populated mapping of EP names to their applicable rule indices. + /// Pre-populated mapping of rule indices to EP names. + /// Matcher to resolve node annotations to rule indices. + static LayeringIndex Create(Graph& graph, + EpNameToLayeringIndices ep_map, + LayeringIndexToEpName rule_map, + const LayeringRuleMatcher& matcher); + + /// + /// Factory method that creates a LayeringIndex by parsing configuration, matching rules against + /// available devices/providers, and indexing the graph. + /// + /// The graph to index. + /// The configuration string containing layering rules. + /// Available OrtEpDevices to match rules against. + /// Available ExecutionProviders to match rules against (fallback). + /// Logger for reporting information/errors. + /// Output parameter for the created LayeringIndex. Returns no index if + /// no valid layering rules discovered. + /// Status indicating success or failure. + static Status Create(Graph& graph, + const std::string& config_string, + gsl::span ep_devices, + const ExecutionProviders& ep_providers, + const logging::Logger& logger, + std::optional& layering_index); + + // Returns the Layering Rule indices mapped to the EP if any + std::optional>> + GetLayeringRulesForThisEp(const std::string& ep_type) const { + auto hit = ep_name_to_layering_indices_.find(ep_type); + if (hit == ep_name_to_layering_indices_.end()) { + return {}; + } + return hit->second; + } + + // This function returns an index for the Layering rule the node is assigned to if any + std::optional GetNodeAssignment(const Graph& graph, NodeIndex node_id) const { + auto hit = graph_index_.find(&graph); + if (hit == graph_index_.end()) { + // this should not be possible + assert(false); + return {}; + } + + // Nodes in subgraph that were not annotated has already inherited their + // annotation if any from the parent node of the subgraph + const auto& graph_layering_index = hit->second; + auto layer_hit = graph_layering_index.node_to_layering_index_.find(node_id); + if (layer_hit != graph_layering_index.node_to_layering_index_.end()) { + return layer_hit->second; + } + return {}; + } + + // This is used when an EP fails to claim a node during partitioning so we make it + // available for other EPs + void MakeNodeUnassigned(const Graph& graph, NodeIndex node_id) { + auto hit = graph_index_.find(&graph); + if (hit == graph_index_.end()) { + // this should not be possible + assert(false); + return; + } + auto& graph_layering_index = hit->second; + auto node_to_layer_hit = graph_layering_index.node_to_layering_index_.find(node_id); + std::optional layer_idx; + if (node_to_layer_hit != graph_layering_index.node_to_layering_index_.end()) { + // Get the layer index + layer_idx = node_to_layer_hit->second; + graph_layering_index.node_to_layering_index_.erase(node_to_layer_hit); + } + + // Remove node from layer collection + if (layer_idx) { + auto layer_to_nodes_hit = graph_layering_index.layer_to_node_ids_.find(*layer_idx); + if (layer_to_nodes_hit != graph_layering_index.layer_to_node_ids_.end()) { + layer_to_nodes_hit->second.erase(node_id); + } + } + } + + private: + // These stay constant + EpNameToLayeringIndices ep_name_to_layering_indices_; + LayeringIndexToEpName layering_index_to_ep_name_; + + using SetOfNodes = InlinedHashSet; + using LayerIndexToNodes = InlinedHashMap; + using NodeIndexToLayeringIndex = InlinedHashMap; + + /// + /// This struct contains the result of layering assignment for a graph. + /// The struct first reflects pre-assignment according to the configuration. + /// However, as we partition the graph, some nodes may be moved to unassigned sections + /// to make them available to subsequent partitioning passes. + /// + struct GraphLayeringIndex { + // Node to layering idx assignment map 1:1 + // If the node is not in this map, it is unassigned + NodeIndexToLayeringIndex node_to_layering_index_; + // This map contains mapping of LayeringRule index to the list of node ids + // Revers from the above 1:M + LayerIndexToNodes layer_to_node_ids_; + }; + + LayeringIndex() = default; + + LayeringIndex(EpNameToLayeringIndices ep_name_to_layering_indices, LayeringIndexToEpName layering_index_to_ep_name) + : ep_name_to_layering_indices_(std::move(ep_name_to_layering_indices)), + layering_index_to_ep_name_(std::move(layering_index_to_ep_name)) {} + + // Graph and sub-graphs mapping to their indices + InlinedHashMap graph_index_; + + void ProcessGraph(Graph& graph, const LayeringRuleMatcher& matcher, std::optional parent_layer_id); +}; + +} // namespace onnxruntime diff --git a/onnxruntime/core/framework/resource_accountant.cc b/onnxruntime/core/framework/resource_accountant.cc index 0665cc1951e60..a08d6e625abee 100644 --- a/onnxruntime/core/framework/resource_accountant.cc +++ b/onnxruntime/core/framework/resource_accountant.cc @@ -11,6 +11,7 @@ #include "core/framework/config_options.h" #include "core/framework/murmurhash3.h" +#include "core/framework/tensorprotoutils.h" #include "core/graph/constants.h" #include "core/graph/graph.h" #include "core/session/onnxruntime_session_options_config_keys.h" @@ -20,15 +21,18 @@ namespace onnxruntime { // Use this accountant if your resource can be counted with size_t type -class SizeTAccountant : public IResourceAccountant { +// This accountant uses NodeAllocationStats to compute resource consumption per node +// which can be collected and saved to a file OR loaded from a file and used for partitioning. +// This is currently used for CUDA EP. +class SizeBasedStatsAccountant : public IResourceAccountant { public: - SizeTAccountant() = default; - ~SizeTAccountant() = default; + SizeBasedStatsAccountant() = default; + ~SizeBasedStatsAccountant() = default; - SizeTAccountant(size_t threshold, InlinedHashMap&& node_stats) + SizeBasedStatsAccountant(size_t threshold, InlinedHashMap&& node_stats) : IResourceAccountant(threshold), node_stats_(std::move(node_stats)) {} - explicit SizeTAccountant(InlinedHashMap&& node_stats) + explicit SizeBasedStatsAccountant(InlinedHashMap&& node_stats) : IResourceAccountant(), node_stats_(std::move(node_stats)) {} ResourceCount GetConsumedAmount() const noexcept override { @@ -46,7 +50,7 @@ class SizeTAccountant : public IResourceAccountant { } } - ResourceCount ComputeResourceCount(const Node& node) const override { + ResourceCount ComputeResourceCount(const Node& node) override { const auto node_name = MakeUniqueNodeName(node); auto hit = node_stats_.find(node_name); if (hit != node_stats_.end()) { @@ -62,6 +66,67 @@ class SizeTAccountant : public IResourceAccountant { InlinedHashMap node_stats_; }; +// Use this accountant if your resource can be counted with size_t type +// This accountant calculates the resource consumption based on node consumed +// weights since those are the biggest consumers. It prevents double accounting. +// The accountant is used for ad-hoc partitioning when runtime consumables are not +// known (see SizeBasedStatsAccountant above for recording and replaying consumption +// based on real runs) but we want to run out of the box on as many environments +// as possible. +class WeightsSizeBasedAccountant : public IResourceAccountant { + public: + WeightsSizeBasedAccountant() = default; + ~WeightsSizeBasedAccountant() = default; + + ResourceCount GetConsumedAmount() const noexcept override { + return consumed_amount_; + } + + void AddConsumedAmount(const ResourceCount& amount) noexcept override { + if (std::holds_alternative(amount)) { + consumed_amount_ += std::get(amount); + } + } + void RemoveConsumedAmount(const ResourceCount& amount) noexcept override { + if (std::holds_alternative(amount)) { + consumed_amount_ -= std::get<0>(amount); + } + } + + ResourceCount ComputeResourceCount(const Node& node) override { + const auto* graph = node.GetContainingGraph(); + if (!graph) return static_cast(0); + + size_t total_size = 0; + for (const auto* input_def : node.InputDefs()) { + if (!input_def->Exists()) continue; + + const auto& name = input_def->Name(); + bool check_outer_scope = true; + const auto* tensor_proto = graph->GetInitializer(name, check_outer_scope); + + if (tensor_proto) { + if (accounted_weights_.find(name) != accounted_weights_.end()) { + continue; + } + + size_t size = 0; + auto status = utils::GetSizeInBytesFromTensorProto<0>(*tensor_proto, &size); + + if (status.IsOK()) { + total_size += size; + accounted_weights_.insert(name); + } + } + } + return total_size; + } + + private: + size_t consumed_amount_ = 0; + InlinedHashSet accounted_weights_; +}; + struct NodeStatsRecorder::Impl { std::filesystem::path node_stats_path; // This is a node name to allocation stats map @@ -155,10 +220,11 @@ static Status LoadNodeAllocationStats( return Status::OK(); } -Status NodeStatsRecorder::CreateAccountants( +Status CreateAccountants( const ConfigOptions& config_options, const std::filesystem::path& model_path, std::optional& acc_map) { + std::optional result; // Check if CUDA partitioning settings are provided const std::string resource_partitioning_settings = config_options.GetConfigOrDefault( kOrtSessionOptionsResourceCudaPartitioningSettings, ""); @@ -173,7 +239,6 @@ Status NodeStatsRecorder::CreateAccountants( InlinedHashMap loaded_stats; ORT_RETURN_IF_ERROR(LoadNodeAllocationStats(model_path, splits[1], loaded_stats)); - std::optional result; auto& map = result.emplace(); if (!splits[0].empty()) { @@ -181,21 +246,28 @@ Status NodeStatsRecorder::CreateAccountants( ORT_RETURN_IF_ERROR(ParseStringWithClassicLocale(std::string{splits[0]}, cuda_memory_limit)); cuda_memory_limit = SafeInt(cuda_memory_limit) * 1024; // to bytes map.insert_or_assign(kCudaExecutionProvider, - std::make_unique(cuda_memory_limit, - std::move(loaded_stats))); + std::make_unique(cuda_memory_limit, + std::move(loaded_stats))); } else { map.insert_or_assign(kCudaExecutionProvider, - std::make_unique(std::move(loaded_stats))); + std::make_unique(std::move(loaded_stats))); } - - acc_map = std::move(result); } else { return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid format for: ", kOrtSessionOptionsResourceCudaPartitioningSettings, " : expecting comma separated fields"); } + } else { + const std::string layer_assignments = + config_options.GetConfigOrDefault(kOrtSessionOptionsLayerAssignmentSettings, ""); + if (!layer_assignments.empty()) { + auto& map = result.emplace(); + map.insert_or_assign(kCudaExecutionProvider, + std::make_unique()); + } } + acc_map = std::move(result); return Status::OK(); } diff --git a/onnxruntime/core/framework/tensorprotoutils.cc b/onnxruntime/core/framework/tensorprotoutils.cc index d981f610a4097..bf352b777861a 100644 --- a/onnxruntime/core/framework/tensorprotoutils.cc +++ b/onnxruntime/core/framework/tensorprotoutils.cc @@ -2227,5 +2227,18 @@ Status UnpackInitializerData(const ONNX_NAMESPACE::TensorProto& initializer, std return UnpackInitializerData(initializer, std::filesystem::path(), unpacked_tensor); } +std::optional GetNodeProtoLayeringAnnotation(const ONNX_NAMESPACE::NodeProto& node_proto) { + std::optional result; + for (const auto& prop : node_proto.metadata_props()) { + if (prop.key() == kNodeProtoLayerAnnotation) { + if (!prop.value().empty()) { + result = prop.value(); + } + break; + } + } + return result; +} + } // namespace utils } // namespace onnxruntime diff --git a/onnxruntime/core/framework/tensorprotoutils.h b/onnxruntime/core/framework/tensorprotoutils.h index 8c9f64e9fbb9f..b6591a6eac571 100644 --- a/onnxruntime/core/framework/tensorprotoutils.h +++ b/onnxruntime/core/framework/tensorprotoutils.h @@ -637,5 +637,15 @@ common::Status UnpackInitializerData(const ONNX_NAMESPACE::TensorProto& initiali */ common::Status UnpackInitializerData(const ONNX_NAMESPACE::TensorProto& initializer, std::vector& unpacked_tensor); + +constexpr const char* kNodeProtoLayerAnnotation = "layer_ann"; + +/** + * This function examines the given node proto and looks into its metadata_props. + * It returns the first value found for the key kNodeProtoLayerAnnotation. + * Empty annotations are ignored. + * If no annotation is found, std::nullopt is returned. + */ +std::optional GetNodeProtoLayeringAnnotation(const ONNX_NAMESPACE::NodeProto& node_proto); } // namespace utils } // namespace onnxruntime diff --git a/onnxruntime/core/graph/graph.cc b/onnxruntime/core/graph/graph.cc index dd3eb59b7fafb..60b2e1f86115e 100644 --- a/onnxruntime/core/graph/graph.cc +++ b/onnxruntime/core/graph/graph.cc @@ -4393,6 +4393,13 @@ Node& Graph::AddNode(const NodeProto& node_proto, &attributes, node_proto.domain()); +#ifndef ORT_MINIMAL_BUILD + auto maybe_annotation = utils::GetNodeProtoLayeringAnnotation(node_proto); + if (maybe_annotation) { + new_node.SetLayeringAnnotation(std::move(*maybe_annotation)); + } +#endif + // Perf optimization: temporarily set NodeProto in Node so we don't need to call Node::ToProto prior to // calling onnx::check_node // NOTE: We don't handle a node with kOnnxDomainAlias. The entry in schema_registry_ uses kOnnxDomain, diff --git a/onnxruntime/core/providers/cuda/cuda_execution_provider.cc b/onnxruntime/core/providers/cuda/cuda_execution_provider.cc index eb29e4edbf897..20af494c680f2 100644 --- a/onnxruntime/core/providers/cuda/cuda_execution_provider.cc +++ b/onnxruntime/core/providers/cuda/cuda_execution_provider.cc @@ -2938,16 +2938,18 @@ CUDAExecutionProvider::GetCapability(const onnxruntime::GraphViewer& graph, } auto threshold = resource_accountant->GetThreshold(); - if (!threshold.has_value()) { + if (!threshold) { // info_.gpu_mem_limit is for BFC arena size_t free_memory, total_memory; if (0 != cudaMemGetInfo(&free_memory, &total_memory)) { memory_threshold = info_.gpu_mem_limit; + LOGS(logger, WARNING) + << "CUDA_EP failed to get available GPU memory info. Using info_.gpu_mem_limit instead: " << info_.gpu_mem_limit; } else { memory_threshold = std::min(free_memory, info_.gpu_mem_limit); } } else { - memory_threshold = std::get<0>(threshold.value()); + memory_threshold = std::get<0>(*threshold); } consumed_memory = std::get<0>(resource_accountant->GetConsumedAmount()); diff --git a/onnxruntime/test/framework/layering_annotations_test.cc b/onnxruntime/test/framework/layering_annotations_test.cc new file mode 100644 index 0000000000000..1038f586ae194 --- /dev/null +++ b/onnxruntime/test/framework/layering_annotations_test.cc @@ -0,0 +1,957 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "core/framework/ortmemoryinfo.h" +#include "core/framework/layering_annotations.h" +#include "core/session/abi_devices.h" +#include "core/framework/execution_provider.h" +#include "core/framework/ortdevice.h" +#include "core/graph/constants.h" +#include "core/graph/model.h" // For Model, Graph +#include "gtest/gtest.h" +#include "core/framework/execution_providers.h" + +namespace onnxruntime { +namespace test { + +TEST(LayeringRuleMatcherTest, ExactMatches) { + LayeringRules rules; + rules.rules.push_back({"Device1", "Annotation1", false}); // Index 0 + rules.rules.push_back({"Device2", "Annotation2", false}); // Index 1 + + LayeringRuleMatcher matcher(rules); + + { + auto result = matcher.Match("Annotation1"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0u); + } + { + auto result = matcher.Match("Annotation2"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 1u); + } + { + auto result = matcher.Match("Annotation3"); + EXPECT_FALSE(result.has_value()); + } +} + +TEST(LayeringRuleMatcherTest, PrefixMatches) { + LayeringRules rules; + rules.rules.push_back({"Device1", "Prefix1", true}); // Index 0: =Prefix1 + rules.rules.push_back({"Device2", "Pre", true}); // Index 1: =Pre + + LayeringRuleMatcher matcher(rules); + + // "Prefix1Suffix" matches "Prefix1" (idx 0) and "Pre" (idx 1). 0 < 1, so 0. + { + auto result = matcher.Match("Prefix1Suffix"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0u); + } + + // "PreSuffix" matches "Pre" (idx 1). "Prefix1" does not match. + { + auto result = matcher.Match("PreSuffix"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 1u); + } + + // "Other" matches nothing + { + auto result = matcher.Match("Other"); + EXPECT_FALSE(result.has_value()); + } +} + +TEST(LayeringRuleMatcherTest, PriorityPrefixOverExact) { + // Prefix matches should take precedence over exact matches regardless of order. + + // Case 1: Prefix rule comes before Exact rule + { + LayeringRules rules; + rules.rules.push_back({"Device1", "A", true}); // Index 0: =A (Prefix) + rules.rules.push_back({"Device2", "AB", false}); // Index 1: AB (Exact) + + LayeringRuleMatcher matcher(rules); + // "AB" matches prefix "A" (idx 0) and exact "AB" (idx 1). + // Since prefix matches are checked first and returned if found, we expect 0. + auto result = matcher.Match("AB"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0u); + } + + // Case 2: Exact rule comes before Prefix rule + { + LayeringRules rules; + rules.rules.push_back({"Device1", "AB", false}); // Index 0: AB (Exact) + rules.rules.push_back({"Device2", "A", true}); // Index 1: =A (Prefix) + + LayeringRuleMatcher matcher(rules); + // "AB" matches exact "AB" (idx 0) and prefix "A" (idx 1). + // Priority says Prefix matches are returned first. + auto result = matcher.Match("AB"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 1u); + } +} + +TEST(LayeringRuleMatcherTest, LongestOrShortestPrefixPriority) { + // If multiple prefix rules match, the one with the lowest index (earliest in config) wins. + + // Case 1: Shorter prefix first + { + LayeringRules rules; + rules.rules.push_back({"Device1", "A", true}); // Index 0 + rules.rules.push_back({"Device2", "AB", true}); // Index 1 + + LayeringRuleMatcher matcher(rules); + // "ABC" matches "A" (0) and "AB" (1). Since 0 < 1, best match is 0. + auto result = matcher.Match("ABC"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0u); + } + + // Case 2: Longer prefix first + { + LayeringRules rules; + rules.rules.push_back({"Device1", "AB", true}); // Index 0 + rules.rules.push_back({"Device2", "A", true}); // Index 1 + + LayeringRuleMatcher matcher(rules); + // "ABC" matches "AB" (0) and "A" (1). Since 0 < 1, best match is 0. + auto result = matcher.Match("ABC"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0u); + } +} + +TEST(LayeringRuleMatcherTest, OverlappingExactMatchPriority) { + // If duplicates exist, first one wins. + LayeringRules rules; + rules.rules.push_back({"Device1", "A", false}); // Index 0 + rules.rules.push_back({"Device2", "A", false}); // Index 1 + + LayeringRuleMatcher matcher(rules); + auto result = matcher.Match("A"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0u); +} + +TEST(LayeringRuleMatcherTest, OverlappingPrefixMatchPriority) { + // If duplicates exist, first one wins. + LayeringRules rules; + rules.rules.push_back({"Device1", "A", true}); // Index 0 + rules.rules.push_back({"Device2", "A", true}); // Index 1 + + LayeringRuleMatcher matcher(rules); + auto result = matcher.Match("AB"); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, 0u); +} + +namespace { + +// Helper to construct OrtEpDevice wrappers for testing +struct TestEpDevice { + std::string ep_name; + OrtHardwareDevice hw_device; + bool has_hw_device = false; + OrtMemoryInfo mem_info; + bool has_mem_info = false; + + // We need to keep the structures alive while OrtEpDevice points to them + OrtEpDevice Get() const { + OrtEpDevice ep; + ep.ep_name = ep_name; + ep.device = has_hw_device ? &hw_device : nullptr; + ep.device_memory_info = has_mem_info ? &mem_info : nullptr; + return ep; + } +}; + +TestEpDevice CreateEp(const std::string& name) { + TestEpDevice ep; + ep.ep_name = name; + return ep; +} + +TestEpDevice CreateHwEp(const std::string& name, OrtHardwareDeviceType type, uint32_t vendor_id = 0, + uint32_t device_id = 0, const std::string& vendor_str = std::string()) { + TestEpDevice ep; + ep.ep_name = name; + ep.hw_device = {type, vendor_id, device_id, vendor_str, {}}; + ep.has_hw_device = true; + return ep; +} + +TestEpDevice CreateMemEp(const std::string& name, OrtDevice::DeviceType type, int device_id = 0) { + TestEpDevice ep; + ep.ep_name = name; + // Note: OrtMemoryInfo name doesn't matter for logic now, but required for ctor + ep.mem_info = OrtMemoryInfo("TestMem", OrtAllocatorType::OrtDeviceAllocator, + OrtDevice(type, OrtDevice::MemType::DEFAULT, OrtDevice::VendorIds::NONE, + static_cast(device_id)), + OrtMemType::OrtMemTypeDefault); + ep.has_mem_info = true; + return ep; +} + +} // namespace + +TEST(EpLayeringMatcherTest, MatchCPU) { + LayerAnnotation rule = {"CPU", "Anno1", false}; + + // Case 1: EP Name kCpuExecutionProvider + { + auto test_ep = CreateEp(kCpuExecutionProvider); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kCpuExecutionProvider); + } + + // Case 2: Hardware Device CPU + { + auto test_ep = CreateHwEp("SomeCPU_EP", OrtHardwareDeviceType_CPU); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "SomeCPU_EP"); + } + + // Case 3: Memory Info CPU + { + auto test_ep = CreateMemEp("MemCPU_EP", OrtDevice::CPU); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MemCPU_EP"); + } +} + +TEST(EpLayeringMatcherTest, MatchGPU) { + LayerAnnotation rule = {"GPU", "Anno1", false}; + + // Case 1: Hardware Device GPU + { + auto test_ep = CreateHwEp("MyGPU_EP", OrtHardwareDeviceType_GPU); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MyGPU_EP"); + } + + // Case 2: Memory Info GPU + { + auto test_ep = CreateMemEp("MemGPU_EP", OrtDevice::GPU); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MemGPU_EP"); + } + + // Case 3: Heuristic kCudaExecutionProvider + { + auto test_ep = CreateEp(kCudaExecutionProvider); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kCudaExecutionProvider); + } + + // Case 4: Heuristic kDmlExecutionProvider + { + auto test_ep = CreateEp(kDmlExecutionProvider); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kDmlExecutionProvider); + } +} + +TEST(EpLayeringMatcherTest, MatchSpecificGPU_VendorString) { + LayerAnnotation rule = {"gpu:nvidia", "Anno1", false}; + + // Case 1: Vendor String Match + { + auto test_ep = CreateHwEp("MyNvidia_EP", OrtHardwareDeviceType_GPU, 0, 0, "NVIDIA"); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MyNvidia_EP"); + } + + // Case 2: Vendor String Mismatch + { + auto test_ep = CreateHwEp("MyAMD_EP", OrtHardwareDeviceType_GPU, 0, 0, "AMD"); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + EXPECT_FALSE(result.has_value()); + } +} + +TEST(EpLayeringMatcherTest, MatchSpecificGPU_VendorId) { + LayerAnnotation rule_intel = {"gpu:intel", "Anno1", false}; + LayerAnnotation rule_nvidia = {"gpu:nvidia", "Anno2", false}; + LayerAnnotation rule_amd = {"gpu:amd", "Anno3", false}; + + // Case 1: Vendor ID Match Intel + { + auto test_ep = CreateHwEp("Intel_EP", OrtHardwareDeviceType_GPU, OrtDevice::VendorIds::INTEL); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule_intel); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "Intel_EP"); + } + + // Case 2: Vendor ID Match Nvidia + { + auto test_ep = CreateHwEp("Nvidia_EP", OrtHardwareDeviceType_GPU, OrtDevice::VendorIds::NVIDIA); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule_nvidia); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "Nvidia_EP"); + } + + // Case 3: Vendor ID Match AMD + { + auto test_ep = CreateHwEp("AMD_EP", OrtHardwareDeviceType_GPU, OrtDevice::VendorIds::AMD); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule_amd); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "AMD_EP"); + } +} + +TEST(EpLayeringMatcherTest, MatchSpecificGPU_Heuristic) { + LayerAnnotation rule = {"gpu:nvidia", "Anno1", false}; + + // Case 1: kCudaExecutionProvider -> nvidia + { + // Need an EP with GPU HW type but generic vendor info to trigger the heuristic + auto test_ep_hw = CreateHwEp(kCudaExecutionProvider, OrtHardwareDeviceType_GPU); + OrtEpDevice ep_device = test_ep_hw.Get(); + std::vector devices = {&ep_device}; + + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kCudaExecutionProvider); + } +} + +TEST(EpLayeringMatcherTest, MatchSpecificGPU_Index) { + LayerAnnotation rule = {"gpu:1", "Anno1", false}; + + // Case 1: ID Match + { + auto test_ep = CreateHwEp("GPU1", OrtHardwareDeviceType_GPU, 0, 1); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "GPU1"); + } + + // Case 2: ID Mismatch + { + auto test_ep = CreateHwEp("GPU0", OrtHardwareDeviceType_GPU, 0, 0); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + EXPECT_FALSE(result.has_value()); + } +} + +TEST(EpLayeringMatcherTest, MatchAccelerator) { + LayerAnnotation rule = {"accelerator", "Anno1", false}; + + // Case 1: CPU EP should NOT match + { + auto test_ep = CreateEp(kCpuExecutionProvider); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + EXPECT_FALSE(result.has_value()); + } + + // Case 2: Custom EP, No HW/Mem info, considered accelerator + { + auto test_ep = CreateEp("MyCustomAccel"); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MyCustomAccel"); + } + + // Case 3: GPU HW is an accelerator + { + auto test_ep = CreateHwEp("MyGPU", OrtHardwareDeviceType_GPU); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MyGPU"); + } +} + +TEST(EpLayeringMatcherTest, MatchNPU) { + LayerAnnotation rule = {"npu", "Anno1", false}; + + // Case 1: Hardware NPU + { + auto test_ep = CreateHwEp("MyNPU", OrtHardwareDeviceType_NPU); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MyNPU"); + } + + // Case 2: QNN Heuristic + { + auto test_ep = CreateEp(kQnnExecutionProvider); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kQnnExecutionProvider); + } +} + +TEST(EpLayeringMatcherTest, MatchFPGA) { + LayerAnnotation rule = {"fpga", "Anno1", false}; + + // Case 1: MemInfo says FPGA + { + auto test_ep = CreateMemEp("MyFPGA", OrtDevice::FPGA); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MyFPGA"); + } +} + +TEST(EpLayeringMatcherTest, MatchDirectDesignators) { + LayerAnnotation rule_cuda = {"cuda", "A", false}; + LayerAnnotation rule_dml = {"dml", "B", false}; + + { + auto test_ep = CreateEp(kCudaExecutionProvider); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule_cuda); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kCudaExecutionProvider); + } + { + auto test_ep = CreateEp(kDmlExecutionProvider); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule_dml); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kDmlExecutionProvider); + } +} + +TEST(EpLayeringMatcherTest, MatchExactEPName) { + LayerAnnotation rule = {"MyCustomEP", "Anno1", false}; + + { + auto test_ep = CreateEp("MyCustomEP"); + OrtEpDevice ep_device = test_ep.Get(); + std::vector devices = {&ep_device}; + auto result = EpLayeringMatcher::Match(devices, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MyCustomEP"); + } +} + +namespace { + +// Minimal concrete implementation of IExecutionProvider for testing +class MockExecutionProvider : public IExecutionProvider { + public: + MockExecutionProvider(const std::string& type, OrtDevice device) + : IExecutionProvider(type, device) {} + + std::shared_ptr GetKernelRegistry() const override { return nullptr; } +}; + +} // namespace + +TEST(EpLayeringMatcherTest, MatchExecutionProviders_CPU) { + LayerAnnotation rule = {"CPU", "Anno1", false}; + ExecutionProviders providers; + + // Add CPU provider + auto cpu_ep = std::make_shared(kCpuExecutionProvider, OrtDevice(OrtDevice::CPU, OrtDevice::MemType::DEFAULT, 0, 0)); + ASSERT_STATUS_OK(providers.Add(kCpuExecutionProvider, cpu_ep)); + + // Add a GPU provider (should be skipped for CPU rule) + auto gpu_ep = std::make_shared(kCudaExecutionProvider, OrtDevice(OrtDevice::GPU, OrtDevice::MemType::DEFAULT, 0, 0)); + ASSERT_STATUS_OK(providers.Add(kCudaExecutionProvider, gpu_ep)); + + auto result = EpLayeringMatcher::Match(providers, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kCpuExecutionProvider); +} + +TEST(EpLayeringMatcherTest, MatchExecutionProviders_GPU) { + LayerAnnotation rule = {"GPU", "Anno1", false}; + ExecutionProviders providers; + + // Add CPU provider (should be skipped) + auto cpu_ep = std::make_shared(kCpuExecutionProvider, OrtDevice(OrtDevice::CPU, OrtDevice::MemType::DEFAULT, 0, 0)); + ASSERT_STATUS_OK(providers.Add(kCpuExecutionProvider, cpu_ep)); + + // Add CUDA provider (GPU) + auto gpu_ep = std::make_shared(kCudaExecutionProvider, OrtDevice(OrtDevice::GPU, OrtDevice::MemType::DEFAULT, 0, 0)); + ASSERT_STATUS_OK(providers.Add(kCudaExecutionProvider, gpu_ep)); + + auto result = EpLayeringMatcher::Match(providers, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kCudaExecutionProvider); +} + +TEST(EpLayeringMatcherTest, MatchExecutionProviders_GPU_Specific) { + LayerAnnotation rule = {"gpu:nvidia", "Anno1", false}; // Assumes heuristics or vendor ID logic + ExecutionProviders providers; + + // Add CPU provider + auto cpu_ep = std::make_shared(kCpuExecutionProvider, OrtDevice(OrtDevice::CPU, OrtDevice::MemType::DEFAULT, 0, 0)); + ASSERT_STATUS_OK(providers.Add(kCpuExecutionProvider, cpu_ep)); + + // Add CUDA provider (NVIDIA vendor ID) + auto gpu_ep = std::make_shared(kCudaExecutionProvider, + OrtDevice(OrtDevice::GPU, OrtDevice::MemType::DEFAULT, OrtDevice::VendorIds::NVIDIA, 0)); + ASSERT_STATUS_OK(providers.Add(kCudaExecutionProvider, gpu_ep)); + + auto result = EpLayeringMatcher::Match(providers, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, kCudaExecutionProvider); +} + +TEST(EpLayeringMatcherTest, MatchExecutionProviders_NoMatch) { + LayerAnnotation rule = {"GPU", "Anno1", false}; + ExecutionProviders providers; + + // Only CPU provider available + auto cpu_ep = std::make_shared(kCpuExecutionProvider, OrtDevice(OrtDevice::CPU, OrtDevice::MemType::DEFAULT, 0, 0)); + ASSERT_STATUS_OK(providers.Add(kCpuExecutionProvider, cpu_ep)); + + auto result = EpLayeringMatcher::Match(providers, rule); + EXPECT_FALSE(result.has_value()); +} + +TEST(EpLayeringMatcherTest, MatchExecutionProviders_Accelerator) { + LayerAnnotation rule = {"accelerator", "Anno1", false}; + ExecutionProviders providers; + + // Add CPU + auto cpu_ep = std::make_shared(kCpuExecutionProvider, OrtDevice(OrtDevice::CPU, OrtDevice::MemType::DEFAULT, 0, 0)); + ASSERT_STATUS_OK(providers.Add(kCpuExecutionProvider, cpu_ep)); + + // Add custom accelerator + auto accel_ep = std::make_shared("MyAccel", OrtDevice(OrtDevice::NPU, OrtDevice::MemType::DEFAULT, 0, 0)); + ASSERT_STATUS_OK(providers.Add("MyAccel", accel_ep)); + + auto result = EpLayeringMatcher::Match(providers, rule); + ASSERT_TRUE(result.has_value()); + EXPECT_EQ(*result, "MyAccel"); +} + +TEST(LayeringIndexTest, AssignNodesBasedOnAnnotations) { + // 1. Setup Graph + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + // Create nodes + // Node 0: "AnnotatedNode" -> Annotated with "RuleA" + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* input_arg = &graph.GetOrCreateNodeArg("input", &type_proto); + NodeArg* output_arg0 = &graph.GetOrCreateNodeArg("output0", &type_proto); + Node& node0 = graph.AddNode("node0", "Abs", "Node 0", {input_arg}, {output_arg0}); + node0.SetLayeringAnnotation("RuleA"); + + // Node 1: "UnannotatedNode" -> No annotation + NodeArg* output_arg1 = &graph.GetOrCreateNodeArg("output1", &type_proto); + Node& node1 = graph.AddNode("node1", "Abs", "Node 1", {output_arg0}, {output_arg1}); + // No annotation + + // Node 2: "AnnotatedNode2" -> Annotated with "RuleB" + NodeArg* output_arg2 = &graph.GetOrCreateNodeArg("output2", &type_proto); + Node& node2 = graph.AddNode("node2", "Abs", "Node 2", {output_arg1}, {output_arg2}); + node2.SetLayeringAnnotation("RuleB"); + + ASSERT_STATUS_OK(graph.Resolve()); + + // 2. Setup Rules and Matcher + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); // Index 0 + rules.rules.push_back({"DeviceB", "RuleB", false}); // Index 1 + LayeringRuleMatcher matcher(rules); + + // 3. Setup Pre-computed Mappings (simulating Partitioning Manager) + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + ep_map["DeviceB"].insert(1); + + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + rule_map[1] = "DeviceB"; + + // 4. Create LayeringIndex + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), matcher); + + // 5. Verify Assignments + // Node 0: Annotated "RuleA" -> Index 0 -> DeviceA + auto assign0 = index.GetNodeAssignment(graph, node0.Index()); + ASSERT_TRUE(assign0.has_value()); + EXPECT_EQ(*assign0, 0u); + + // Node 1: Unannotated -> Should generally map to nothing (unless defaulting logic exists, + // but current impl leaves unannotated in main graph as unassigned) + auto assign1 = index.GetNodeAssignment(graph, node1.Index()); + EXPECT_FALSE(assign1.has_value()); + + // Node 2: Annotated "RuleB" -> Index 1 -> DeviceB + auto assign2 = index.GetNodeAssignment(graph, node2.Index()); + ASSERT_TRUE(assign2.has_value()); + EXPECT_EQ(*assign2, 1u); +} + +TEST(LayeringIndexTest, AssignNodeWithInvalidEpMapping) { + // Scenario: Node annotated with a rule that maps to an EP that is NOT present/valid + + // 1. Setup Graph with one node annotated "RuleX" + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* input_arg = &graph.GetOrCreateNodeArg("input", &type_proto); + NodeArg* output_arg = &graph.GetOrCreateNodeArg("output", &type_proto); + + Node& node = graph.AddNode("node", "Abs", "Node", {input_arg}, {output_arg}); + node.SetLayeringAnnotation("RuleX"); + + ASSERT_STATUS_OK(graph.Resolve()); + + // 2. Setup Rules: RuleX exists at index 0, maps to "PhantomDevice" + LayeringRules rules; + rules.rules.push_back({"PhantomDevice", "RuleX", false}); // Index 0 + LayeringRuleMatcher matcher(rules); + + // 3. Setup Mappings: But "PhantomDevice" is NOT in the mappings (simulating EP unavailable) + LayeringIndex::EpNameToLayeringIndices ep_map; + // ep_map["PhantomDevice"] is empty/missing + + LayeringIndex::LayeringIndexToEpName rule_map; + // rule_map[0] is missing + + // 4. Create Index + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), matcher); + + // 5. Verify: Node should NOT be assigned because the mapped EP is missing + auto assign = index.GetNodeAssignment(graph, node.Index()); + EXPECT_FALSE(assign.has_value()); +} + +TEST(LayeringIndexTest, SubgraphInheritance) { + // Scenario: Annotated Node containing a subgraph. + // Nodes inside subgraph (unannotated) should inherit parent's assignment. + + // 1. Setup Parent Graph + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_BOOL); + NodeArg* cond_arg = &graph.GetOrCreateNodeArg("cond", &type_proto); + NodeArg* output_arg = &graph.GetOrCreateNodeArg("output", &type_proto); + + // Create "If" node + Node& if_node = graph.AddNode("if_node", "If", "If Node", {cond_arg}, {output_arg}); + if_node.SetLayeringAnnotation("RuleA"); // Annotate Parent + + auto build_subgraph = [](ONNX_NAMESPACE::GraphProto& proto, const std::string& graph_name, + const std::string& node_name, const std::string& input_name, const std::string& output_name) { + proto.set_name(graph_name); + // Inputs: Implicit from outer scope for 'cond' + + auto* node = proto.add_node(); + node->set_name(node_name); + node->set_op_type("Identity"); + node->add_input(input_name); + node->add_output(output_name); + + auto* out_vi = proto.add_output(); + out_vi->set_name(output_name); + out_vi->mutable_type()->mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_BOOL); + }; + + // Create Subgraph (then_branch) + ONNX_NAMESPACE::GraphProto then_graph_proto; + build_subgraph(then_graph_proto, "then_graph", "sub_node", "cond", "sub_out"); + if_node.AddAttribute("then_branch", then_graph_proto); + + // Create 'else_branch' + ONNX_NAMESPACE::GraphProto else_graph_proto; + build_subgraph(else_graph_proto, "else_graph", "else_sub_node", "cond", "else_sub_out"); + if_node.AddAttribute("else_branch", else_graph_proto); + + // First Resolve to create subgraph instances + ASSERT_STATUS_OK(graph.Resolve()); + + // Get subgraph instances (checked to ensure they exist) + Graph* then_graph = if_node.GetMutableGraphAttribute("then_branch"); + ASSERT_NE(then_graph, nullptr); + Graph* else_graph = if_node.GetMutableGraphAttribute("else_branch"); + ASSERT_NE(else_graph, nullptr); + + // 2. Setup Rules + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); // Index 0 + LayeringRuleMatcher matcher(rules); + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + + // 3. Create Index + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), matcher); + + // 4. Verify Parent Assignment + auto assign_parent = index.GetNodeAssignment(graph, if_node.Index()); + ASSERT_TRUE(assign_parent.has_value()); + EXPECT_EQ(*assign_parent, 0u); + + // 5. Verify Subgraph Node Assignment (Inheritance) + bool validated_then = false; + for (const auto& node : then_graph->Nodes()) { + if (node.OpType() == "Identity") { + auto assign_sub = index.GetNodeAssignment(*then_graph, node.Index()); + ASSERT_TRUE(assign_sub.has_value()) << "Subgraph node should inherit parent annotation"; + EXPECT_EQ(*assign_sub, 0u); + validated_then = true; + } + } + ASSERT_TRUE(validated_then); +} + +TEST(LayeringIndexTest, SubgraphOverride) { + // Scenario: Annotated Node containing a subgraph. + // Node inside subgraph HAS annotation -> Should override inheritance. + + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_BOOL); + NodeArg* cond_arg = &graph.GetOrCreateNodeArg("cond", &type_proto); + NodeArg* output_arg = &graph.GetOrCreateNodeArg("output", &type_proto); + + Node& if_node = graph.AddNode("if_node", "If", "If Node", {cond_arg}, {output_arg}); + if_node.SetLayeringAnnotation("RuleA"); // Annotate Parent = Rule A (Index 0) + + auto build_subgraph = [](ONNX_NAMESPACE::GraphProto& proto, const std::string& graph_name, + const std::string& node_name, const std::string& input_name, const std::string& output_name) { + proto.set_name(graph_name); + + auto* node = proto.add_node(); + node->set_name(node_name); + node->set_op_type("Identity"); + node->add_input(input_name); + node->add_output(output_name); + + auto* out_vi = proto.add_output(); + out_vi->set_name(output_name); + out_vi->mutable_type()->mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_BOOL); + }; + + ONNX_NAMESPACE::GraphProto then_graph_proto; + build_subgraph(then_graph_proto, "then_graph", "sub_node", "cond", "sub_out"); + if_node.AddAttribute("then_branch", then_graph_proto); + + ONNX_NAMESPACE::GraphProto else_graph_proto; + build_subgraph(else_graph_proto, "else_graph", "else_sub_node", "cond", "else_sub_out"); + if_node.AddAttribute("else_branch", else_graph_proto); + + ASSERT_STATUS_OK(graph.Resolve()); + + Graph* then_graph = if_node.GetMutableGraphAttribute("then_branch"); + ASSERT_NE(then_graph, nullptr); + + // Find sub_node to set annotation + Node* sub_node = nullptr; + for (auto& node : then_graph->Nodes()) { + if (node.Name() == "sub_node") { + sub_node = &node; + break; + } + } + ASSERT_NE(sub_node, nullptr); + + // OVERRIDE: Annotate sub_node with Rule B + sub_node->SetLayeringAnnotation("RuleB"); + + // Rules: RuleA(0)->DeviceA, RuleB(1)->DeviceB + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); + rules.rules.push_back({"DeviceB", "RuleB", false}); + LayeringRuleMatcher matcher(rules); + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + ep_map["DeviceB"].insert(1); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + rule_map[1] = "DeviceB"; + + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), matcher); + + // Verify Parent = 0 + auto assign_parent = index.GetNodeAssignment(graph, if_node.Index()); + ASSERT_TRUE(assign_parent.has_value()); + EXPECT_EQ(*assign_parent, 0u); + + // Verify Sub = 1 (Override) + auto assign_sub = index.GetNodeAssignment(*then_graph, sub_node->Index()); + ASSERT_TRUE(assign_sub.has_value()); + EXPECT_EQ(*assign_sub, 1u); +} + +TEST(LayeringRulesTest, LayeringRulesParsing) { + // Test empty string + { + LayeringRules rules; + ASSERT_STATUS_OK(LayeringRules::FromConfigString("", rules)); + EXPECT_TRUE(rules.rules.empty()); + } + + // Test simple valid string + { + LayeringRules rules; + ASSERT_STATUS_OK(LayeringRules::FromConfigString("EP1(Annotation1)", rules)); + ASSERT_EQ(rules.rules.size(), 1u); + EXPECT_EQ(rules.rules[0].device, "EP1"); + EXPECT_EQ(rules.rules[0].annotation, "Annotation1"); + EXPECT_FALSE(rules.rules[0].prefix_match); + } + + // Test multiple annotations for one device + { + LayeringRules rules; + ASSERT_STATUS_OK(LayeringRules::FromConfigString("EP1(Annotation1, Annotation2)", rules)); + ASSERT_EQ(rules.rules.size(), 2u); + EXPECT_EQ(rules.rules[0].device, "EP1"); + EXPECT_EQ(rules.rules[0].annotation, "Annotation1"); + EXPECT_FALSE(rules.rules[0].prefix_match); + EXPECT_EQ(rules.rules[1].device, "EP1"); + EXPECT_EQ(rules.rules[1].annotation, "Annotation2"); + EXPECT_FALSE(rules.rules[1].prefix_match); + } + + // Test multiple devices + { + LayeringRules rules; + ASSERT_STATUS_OK(LayeringRules::FromConfigString("EP1(Annotation1); EP2(Annotation2)", rules)); + ASSERT_EQ(rules.rules.size(), 2u); + EXPECT_EQ(rules.rules[0].device, "EP1"); + EXPECT_EQ(rules.rules[0].annotation, "Annotation1"); + EXPECT_FALSE(rules.rules[0].prefix_match); + EXPECT_EQ(rules.rules[1].device, "EP2"); + EXPECT_EQ(rules.rules[1].annotation, "Annotation2"); + EXPECT_FALSE(rules.rules[1].prefix_match); + } + + // Test prefix match + { + LayeringRules rules; + ASSERT_STATUS_OK(LayeringRules::FromConfigString("EP1(=Annotation1)", rules)); + ASSERT_EQ(rules.rules.size(), 1u); + EXPECT_EQ(rules.rules[0].device, "EP1"); + EXPECT_EQ(rules.rules[0].annotation, "Annotation1"); + EXPECT_TRUE(rules.rules[0].prefix_match); + } + + // Test trimming whitespace + { + LayeringRules rules; + ASSERT_STATUS_OK(LayeringRules::FromConfigString(" EP1 ( Annotation1 , =Annotation2 ) ; EP2 ( Annotation3 ) ", rules)); + ASSERT_EQ(rules.rules.size(), 3u); + EXPECT_EQ(rules.rules[0].device, "EP1"); + EXPECT_EQ(rules.rules[0].annotation, "Annotation1"); + EXPECT_FALSE(rules.rules[0].prefix_match); + EXPECT_EQ(rules.rules[1].device, "EP1"); + EXPECT_EQ(rules.rules[1].annotation, "Annotation2"); + EXPECT_TRUE(rules.rules[1].prefix_match); + EXPECT_EQ(rules.rules[2].device, "EP2"); + EXPECT_EQ(rules.rules[2].annotation, "Annotation3"); + EXPECT_FALSE(rules.rules[2].prefix_match); + } +} + +TEST(LayeringRulesTest, FromConfigString_InvalidFormat) { + LayeringRules rules; + + // Error: Missing parentheses structure entirely + EXPECT_FALSE(LayeringRules::FromConfigString("Device1Annotation1", rules).IsOK()); + + // Error: Missing closing parenthesis + EXPECT_FALSE(LayeringRules::FromConfigString("Device1(Annotation1", rules).IsOK()); + + // Error: Missing opening parenthesis (or only closing present) + EXPECT_FALSE(LayeringRules::FromConfigString("Device1Annotation1)", rules).IsOK()); + + // Error: Parentheses reversed + EXPECT_FALSE(LayeringRules::FromConfigString("Device1)Annotation1(", rules).IsOK()); + + // Error: Empty device name (starts with parenthesis) + EXPECT_FALSE(LayeringRules::FromConfigString("(Annotation1)", rules).IsOK()); +} + +TEST(LayeringRulesTest, FromConfigString_IgnoresEmptyEntries) { + LayeringRules rules; + // "; ;" should result in 0 rules but Status::OK + ASSERT_STATUS_OK(LayeringRules::FromConfigString("; ;", rules)); + EXPECT_TRUE(rules.rules.empty()); +} + +} // namespace test +} // namespace onnxruntime \ No newline at end of file diff --git a/onnxruntime/test/framework/session_state_test.cc b/onnxruntime/test/framework/session_state_test.cc index ed2b98e5280b5..87d5757c0bfd4 100644 --- a/onnxruntime/test/framework/session_state_test.cc +++ b/onnxruntime/test/framework/session_state_test.cc @@ -9,6 +9,7 @@ #include "core/framework/execution_providers.h" #include "core/framework/graph_partitioner.h" #include "core/framework/kernel_registry.h" +#include "core/framework/layering_annotations.h" #include "core/framework/op_kernel.h" #include "core/framework/bfc_arena.h" #include "core/framework/ep_context_options.h" diff --git a/onnxruntime/test/framework/tensorutils_test.cc b/onnxruntime/test/framework/tensorutils_test.cc index 0d7b583faf27b..904e8622045b8 100644 --- a/onnxruntime/test/framework/tensorutils_test.cc +++ b/onnxruntime/test/framework/tensorutils_test.cc @@ -586,5 +586,63 @@ TEST_F(PathValidationTest, ValidateExternalDataPathWithSymlinkOutside) { ASSERT_FALSE(utils::ValidateExternalDataPath(base_dir_, "outside_link.bin").IsOK()); } +TEST(TensorProtoUtilsTest, GetNodeProtoLayeringAnnotation) { + // Case 1: Annotation exists + { + ONNX_NAMESPACE::NodeProto node_proto; + node_proto.set_name("test_node"); + auto* prop = node_proto.add_metadata_props(); + prop->set_key(utils::kNodeProtoLayerAnnotation); + prop->set_value("foo"); + + auto result = utils::GetNodeProtoLayeringAnnotation(node_proto); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), "foo"); + } + + // Case 2: Annotation missing (empty metadata_props) + { + ONNX_NAMESPACE::NodeProto node_proto; + node_proto.set_name("test_node"); + + auto result = utils::GetNodeProtoLayeringAnnotation(node_proto); + EXPECT_FALSE(result.has_value()); + } + + // Case 3: Other metadata exists, but not the annotation + { + ONNX_NAMESPACE::NodeProto node_proto; + node_proto.set_name("test_node"); + auto* prop = node_proto.add_metadata_props(); + prop->set_key("some_other_key"); + prop->set_value("some_value"); + + auto result = utils::GetNodeProtoLayeringAnnotation(node_proto); + EXPECT_FALSE(result.has_value()); + } + + // Case 4: Multiple metadata, including the annotation + { + ONNX_NAMESPACE::NodeProto node_proto; + node_proto.set_name("test_node"); + + auto* prop1 = node_proto.add_metadata_props(); + prop1->set_key("some_other_key"); + prop1->set_value("some_value"); + + auto* prop2 = node_proto.add_metadata_props(); + prop2->set_key(utils::kNodeProtoLayerAnnotation); + prop2->set_value("bar"); + + auto* prop3 = node_proto.add_metadata_props(); + prop3->set_key("yet_another_key"); + prop3->set_value("baz"); + + auto result = utils::GetNodeProtoLayeringAnnotation(node_proto); + EXPECT_TRUE(result.has_value()); + EXPECT_EQ(result.value(), "bar"); + } +} + } // namespace test } // namespace onnxruntime From 1eeba1a1a7fe806186511ecf79fde1f19f58db76 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Wed, 28 Jan 2026 16:01:20 -0800 Subject: [PATCH 02/57] Wire annotations to partitioning interface. Duplicate layering annotations for AddNode in L1 transformers. --- .../core/framework/graph_partitioner.cc | 29 ++++++++++++------- .../core/framework/graph_partitioner.h | 2 ++ .../core/framework/layering_annotations.cc | 6 +++- .../core/framework/layering_annotations.h | 4 +++ .../core/optimizer/embed_layer_norm_fusion.cc | 10 ++++--- .../core/optimizer/matmul_add_fusion.cc | 5 ++++ .../ensure_unique_dq_for_node_unit.cc | 2 ++ .../qdq_transformer/qdq_propagation.cc | 6 ++++ .../weight_bias_quantization.cc | 7 +++++ .../qdq_transformer/where_dummy_dq.cc | 2 ++ onnxruntime/core/optimizer/reshape_fusion.cc | 1 + onnxruntime/core/optimizer/utils.cc | 7 +++++ onnxruntime/core/optimizer/utils.h | 2 ++ onnxruntime/core/session/inference_session.cc | 15 +++++++++- .../framework/layering_annotations_test.cc | 6 +++- 15 files changed, 87 insertions(+), 17 deletions(-) diff --git a/onnxruntime/core/framework/graph_partitioner.cc b/onnxruntime/core/framework/graph_partitioner.cc index 810613a55caf5..cf078a96a412f 100644 --- a/onnxruntime/core/framework/graph_partitioner.cc +++ b/onnxruntime/core/framework/graph_partitioner.cc @@ -16,6 +16,7 @@ #include "core/framework/kernel_lookup.h" #include "core/framework/kernel_registry_manager.h" #include "core/framework/kernel_registry.h" +#include "core/framework/layering_annotations.h" #include "core/framework/resource_accountant.h" #include "core/graph/function.h" #include "core/graph/function_utils.h" @@ -69,6 +70,7 @@ struct PartitionParams { std::reference_wrapper debug_graph_fn; #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) std::reference_wrapper on_partition_assignment_fn; + LayeringIndex* layering_index; }; } // namespace @@ -150,6 +152,7 @@ struct GetCapabilityForEPParams { IResourceAccountant* resource_accountant; std::reference_wrapper graph_optimizer_registry; std::reference_wrapper check_load_cancellation_fn; + LayeringIndex* layering_index; // Added member }; auto get_capabilities = [](const IExecutionProvider& ep, @@ -430,7 +433,8 @@ static Status PartitionOnnxFormatModelImpl(Graph& graph, FuncManager& func_mgr, const OnPartitionAssignmentFunction& on_partition_assignment_fn, const logging::Logger& logger, IResourceAccountant* resource_accountant, const GraphOptimizerRegistry& graph_optimizer_registry, - bool disable_model_compile) { + bool disable_model_compile, + LayeringIndex* layering_index) { // Added arg // handle testing edge case where optimizers or constant lifting results in graph with no nodes. // doing it here saves all providers checking for this in GetCapability if (graph.NumberOfNodes() == 0) { @@ -448,7 +452,8 @@ static Status PartitionOnnxFormatModelImpl(Graph& graph, FuncManager& func_mgr, check_load_cancellation_fn, on_partition_assignment_fn, logger, resource_accountant, - graph_optimizer_registry, disable_model_compile)); + graph_optimizer_registry, disable_model_compile, + layering_index)); // Pass through } } @@ -474,7 +479,8 @@ static Status PartitionOnnxFormatModelImpl(Graph& graph, FuncManager& func_mgr, std::cref(debug_graph_fn), resource_accountant, std::ref(graph_optimizer_registry), - std::cref(check_load_cancellation_fn)}; + std::cref(check_load_cancellation_fn), + layering_index}; // Pass param ORT_RETURN_IF_ERROR(GetCapabilityForEP(get_capability_params, logger)); if (capabilities.empty()) { @@ -663,8 +669,8 @@ static Status InlineNodes(Graph& graph, bool& modified_graph) { } } - // See if the node with no provider can be inlined. If one such nodes can be - // successfully inlined, we re-run the partitioner on the modified graph. + // See if the node with no provider can be inlined. If one such nodes can be successfully inlined, + // we re-run the partitioner on the modified graph. // NOTE: Inlining the function will change the nodes in the Graph instance, so we can't do that while iterating // using graph.Nodes(). InlinedVector nodes_to_inline; @@ -1018,7 +1024,7 @@ static Status PartitionOnnxFormatModel(const PartitionParams& partition_params, KernelRegistryManager& kernel_registry_manager, const std::optional& acc_map, const GraphOptimizerRegistry& graph_optimizer_registry, - const logging::Logger& logger, bool disable_model_compile) { + const logging::Logger& logger, bool disable_model_compile) { // Added arg bool modified_graph = false; auto& graph = partition_params.graph.get(); @@ -1046,7 +1052,8 @@ static Status PartitionOnnxFormatModel(const PartitionParams& partition_params, check_load_cancellation_fn, on_partition_assignment_fn, logger, resource_accountant, graph_optimizer_registry, - disable_model_compile)); + disable_model_compile, + partition_params.layering_index)); // Pass param } // expand any nodes that have an ONNX function definition but no matching ORT kernel. @@ -1259,9 +1266,10 @@ Status GraphPartitioner::Partition(Graph& graph, FuncManager& func_mgr, const layout_transformation::TransformLayoutFunction& transform_layout_function, const ConfigOptions& config_options, const logging::Logger& logger, + LayeringIndex* layering_index, Mode mode, const epctx::ModelGenOptions& ep_context_gen_options, - const layout_transformation::DebugGraphFn& debug_graph_fn) const { + const layout_transformation::DebugGraphFn& debug_graph_fn) const { // Added arg // It is a greedy partitioning algorithm per provider preferences user provided when calling ONNX RUNTIME right now. // 1. Execution providers' capabilities are checked one by one. // 2. All sub-graphs that an execution provider returns will be assigned to it if it's not assigned yet. @@ -1292,7 +1300,8 @@ Status GraphPartitioner::Partition(Graph& graph, FuncManager& func_mgr, std::ref(fused_node_unique_id), std::cref(transform_layout_function), std::cref(debug_graph_fn), - std::cref(on_partition_assignment_fn_)}; + std::cref(on_partition_assignment_fn_), + layering_index}; #else // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) @@ -1328,7 +1337,7 @@ Status GraphPartitioner::Partition(Graph& graph, FuncManager& func_mgr, bool disable_model_compile = config_options.GetConfigOrDefault(kOrtSessionOptionsDisableModelCompile, "0") == "1"; ORT_RETURN_IF_ERROR(PartitionOnnxFormatModel(partition_params, mode, providers_, kernel_registry_mgr_, ep_acc_map, *graph_optimizer_registry_, logger, - disable_model_compile)); + disable_model_compile)); // Pass param if (ep_context_gen_options.enable) { ORT_RETURN_IF_ERROR(CreateEpContextModel(providers_, graph, ep_context_gen_options, logger)); diff --git a/onnxruntime/core/framework/graph_partitioner.h b/onnxruntime/core/framework/graph_partitioner.h index eb70b9f89933d..4de9d94781b18 100644 --- a/onnxruntime/core/framework/graph_partitioner.h +++ b/onnxruntime/core/framework/graph_partitioner.h @@ -13,6 +13,7 @@ namespace onnxruntime { class ExecutionProviders; class KernelRegistryManager; +class LayeringIndex; class Model; struct ConfigOptions; @@ -60,6 +61,7 @@ class GraphPartitioner { const layout_transformation::TransformLayoutFunction& transform_layout_function, const ConfigOptions& config_options, const logging::Logger& logger, + LayeringIndex* layering_index, Mode mode = Mode::kNormal, const epctx::ModelGenOptions& ep_context_gen_options = {}, const layout_transformation::DebugGraphFn& debug_graph_fn = {}) const; diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc index f4f6c9e37c47b..5fc5bc9aa889a 100644 --- a/onnxruntime/core/framework/layering_annotations.cc +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#if !defined(ORT_MINIMAL_BUILD) + #include "core/graph/constants.h" #include "core/common/narrow.h" #include "core/common/string_utils.h" @@ -510,4 +512,6 @@ void LayeringIndex::ProcessGraph(Graph& graph, const LayeringRuleMatcher& matche } } -} // namespace onnxruntime \ No newline at end of file +} // namespace onnxruntime + +#endif // !defined(ORT_MINIMAL_BUILD) diff --git a/onnxruntime/core/framework/layering_annotations.h b/onnxruntime/core/framework/layering_annotations.h index 8a7cd7dba6cd2..93fdf5cef23ec 100644 --- a/onnxruntime/core/framework/layering_annotations.h +++ b/onnxruntime/core/framework/layering_annotations.h @@ -3,6 +3,8 @@ #pragma once +#if !defined(ORT_MINIMAL_BUILD) + #include "core/common/inlined_containers.h" #include "core/common/status.h" #include "core/graph/basic_types.h" @@ -251,3 +253,5 @@ class LayeringIndex { }; } // namespace onnxruntime + +#endif // !defined(ORT_MINIMAL_BUILD) diff --git a/onnxruntime/core/optimizer/embed_layer_norm_fusion.cc b/onnxruntime/core/optimizer/embed_layer_norm_fusion.cc index 9e35550e2f845..e13b8a16b8c81 100644 --- a/onnxruntime/core/optimizer/embed_layer_norm_fusion.cc +++ b/onnxruntime/core/optimizer/embed_layer_norm_fusion.cc @@ -17,7 +17,7 @@ using namespace ONNX_NAMESPACE; using namespace onnxruntime::common; namespace onnxruntime { // Add a Cast to convert Input from int64 to int32. -static NodeArg* CastToInt32(Graph& graph, NodeArg* input, ProviderType provider_type) { +static NodeArg* CastToInt32(Graph& graph, NodeArg* input, const Node& source_node) { auto data_type = input->TypeAsProto()->tensor_type().elem_type(); if (data_type == ONNX_NAMESPACE::TensorProto_DataType_INT32) { return input; @@ -42,7 +42,8 @@ static NodeArg* CastToInt32(Graph& graph, NodeArg* input, ProviderType provider_ // Add attribute: "to" = 6 node.AddAttribute("to", int64_t{ONNX_NAMESPACE::TensorProto_DataType_INT32}); - node.SetExecutionProviderType(provider_type); + optimizer_utils::DuplicateNodeAnnotation(source_node, node); + node.SetExecutionProviderType(source_node.GetExecutionProviderType()); return &cast32; } @@ -487,9 +488,9 @@ static void CreateEmbedLayernormNode(Graph& graph, NodeArg* segment_embedding, Node& layer_norm_node) { // Cast input_ids and segment_ids to int32 if needed. - input_ids = CastToInt32(graph, input_ids, layer_norm_node.GetExecutionProviderType()); + input_ids = CastToInt32(graph, input_ids, layer_norm_node); if (segment_ids != nullptr && segment_embedding != nullptr) { - segment_ids = CastToInt32(graph, segment_ids, layer_norm_node.GetExecutionProviderType()); + segment_ids = CastToInt32(graph, segment_ids, layer_norm_node); } NodeArg place_holder("", nullptr); @@ -527,6 +528,7 @@ static void CreateEmbedLayernormNode(Graph& graph, } // Assign provider to this new node. Provider should be same as the provider for old node. + optimizer_utils::DuplicateNodeAnnotation(layer_norm_node, embed_layer_norm_node); embed_layer_norm_node.SetExecutionProviderType(layer_norm_node.GetExecutionProviderType()); } diff --git a/onnxruntime/core/optimizer/matmul_add_fusion.cc b/onnxruntime/core/optimizer/matmul_add_fusion.cc index 5db61877811aa..095e4b7f9c317 100644 --- a/onnxruntime/core/optimizer/matmul_add_fusion.cc +++ b/onnxruntime/core/optimizer/matmul_add_fusion.cc @@ -7,6 +7,7 @@ #include "core/optimizer/graph_transformer_utils.h" #include "core/optimizer/initializer.h" #include "core/optimizer/matmul_add_fusion.h" +#include "core/optimizer/utils.h" #include #include @@ -205,6 +206,8 @@ Status MatMulAddFusion::ApplyImpl(Graph& graph, bool& modified, int graph_level, Node& reshape_node = graph.AddNode(graph.GenerateNodeName(name + "_reshape"), "Reshape", "Reshape for " + name, {is_input ? gemm_input_defs[0] : new_arg, shape_arg}, {is_input ? new_arg : gemm_output_defs[0]}); + // Runs before partitioning + optimizer_utils::DuplicateNodeAnnotation(matmul_node, reshape_node); reshape_node.SetExecutionProviderType(matmul_node.GetExecutionProviderType()); return &reshape_node; }; @@ -218,6 +221,8 @@ Status MatMulAddFusion::ApplyImpl(Graph& graph, bool& modified, int graph_level, Node& gemm_node = graph.AddNode(graph.GenerateNodeName(matmul_node.Name() + "/MatMulAddFusion"), "Gemm", "fused Matmul and Add", gemm_input_defs, gemm_output_defs); + // Runs before partitioning + optimizer_utils::DuplicateNodeAnnotation(matmul_node, gemm_node); gemm_node.SetExecutionProviderType(matmul_node.GetExecutionProviderType()); if (need_reshape) { diff --git a/onnxruntime/core/optimizer/qdq_transformer/ensure_unique_dq_for_node_unit.cc b/onnxruntime/core/optimizer/qdq_transformer/ensure_unique_dq_for_node_unit.cc index 9d53e28921784..4dd8dbd45a255 100644 --- a/onnxruntime/core/optimizer/qdq_transformer/ensure_unique_dq_for_node_unit.cc +++ b/onnxruntime/core/optimizer/qdq_transformer/ensure_unique_dq_for_node_unit.cc @@ -10,6 +10,7 @@ #include "core/graph/graph_utils.h" #include "core/graph/graph_viewer.h" #include "core/optimizer/qdq_transformer/qdq_util.h" +#include "core/optimizer/utils.h" namespace onnxruntime { @@ -56,6 +57,7 @@ Status DuplicateDQForOutputEdge(const graph_utils::GraphEdge& original_dq_output &original_dq_node.GetAttributes(), original_dq_node.Domain()); + optimizer_utils::DuplicateNodeAnnotation(original_dq_node, new_dq_node); // set up edges // remove DQ -> Y graph_utils::GraphEdge::RemoveGraphEdges(graph, {original_dq_output_edge}); diff --git a/onnxruntime/core/optimizer/qdq_transformer/qdq_propagation.cc b/onnxruntime/core/optimizer/qdq_transformer/qdq_propagation.cc index 7b518947138a5..5cb7c47083846 100644 --- a/onnxruntime/core/optimizer/qdq_transformer/qdq_propagation.cc +++ b/onnxruntime/core/optimizer/qdq_transformer/qdq_propagation.cc @@ -194,6 +194,8 @@ Status InsertQDQPairs(Graph& graph, gsl::span insertion } } + optimizer_utils::DuplicateNodeAnnotation(*src_node, q_node); + // Add edge from src to Q node. src_node->MutableOutputDefs()[first_edge.src->arg_idx] = &pre_q_nodearg; graph.AddEdge(src_node->Index(), q_node.Index(), first_edge.src->arg_idx, 0); @@ -221,6 +223,10 @@ Status InsertQDQPairs(Graph& graph, gsl::span insertion &dq_attrs, // attributes qdq_domain); + if (src_node) { + optimizer_utils::DuplicateNodeAnnotation(*src_node, dq_node); + } + ORT_RETURN_IF_NOT(graph.SetOpSchemaFromRegistryForNode(dq_node), "Failed to set op schema for added DQ node."); Node* dst_node = insertion_edge.GetMutableNodeAtEnd(graph, ExtendedGraphEdge::End::Destination); diff --git a/onnxruntime/core/optimizer/qdq_transformer/weight_bias_quantization.cc b/onnxruntime/core/optimizer/qdq_transformer/weight_bias_quantization.cc index 5a6eb82c3e6c0..2019f27f7b5b3 100644 --- a/onnxruntime/core/optimizer/qdq_transformer/weight_bias_quantization.cc +++ b/onnxruntime/core/optimizer/qdq_transformer/weight_bias_quantization.cc @@ -190,6 +190,7 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph Node& weight_q_node = graph.AddNode( graph.GenerateNodeArgName(node.Name() + "_weight_q"), QDQ::QOpName, "Weight Q node", {node.MutableInputDefs()[1], weight_scale_arg, &weight_zp_arg}, {&weight_q_arg}, nullptr, node.Domain()); + optimizer_utils::DuplicateNodeAnnotation(node, weight_q_node); // DQ from int8 to float32. NodeArg& weight_dq_arg = @@ -197,6 +198,7 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph Node& weight_dq_node = graph.AddNode(graph.GenerateNodeArgName(node.Name() + "_weight_dq"), QDQ::DQOpName, "Weight DQ node", {&weight_q_arg, weight_scale_arg, &weight_zp_arg}, {&weight_dq_arg}, nullptr, node.Domain()); + optimizer_utils::DuplicateNodeAnnotation(node, weight_dq_node); graph.AddEdge(weight_q_node.Index(), weight_dq_node.Index(), 0, 0); node.MutableInputDefs()[1] = &weight_dq_arg; graph.AddEdge(weight_dq_node.Index(), node.Index(), 0, 1); @@ -212,6 +214,7 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph Node& mul_node = graph.AddNode(graph.GenerateNodeName(node.Name() + "_scale"), "Mul", "Bias scale node", {dq_0.MutableInputDefs()[1], weight_scale_arg}, {&bias_scale_arg}, nullptr, node.Domain()); + optimizer_utils::DuplicateNodeAnnotation(node, mul_node); // fp_bias / scale. NodeArg& bias_div_arg = @@ -219,6 +222,7 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph Node& div_node = graph.AddNode(graph.GenerateNodeName(node.Name() + "_bias_div"), "Div", "Bias div node", {node.MutableInputDefs()[2], &bias_scale_arg}, {&bias_div_arg}, nullptr, node.Domain()); + optimizer_utils::DuplicateNodeAnnotation(node, div_node); graph.AddEdge(mul_node.Index(), div_node.Index(), 0, 1); // Round(fp_bias / scale). @@ -227,6 +231,7 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph Node& round_node = graph.AddNode(graph.GenerateNodeName(node.Name() + "_bias_div_round"), "Round", "Bias div round node", {&bias_div_arg}, {&bias_div_round_arg}, nullptr, node.Domain()); + optimizer_utils::DuplicateNodeAnnotation(node, round_node); graph.AddEdge(div_node.Index(), round_node.Index(), 0, 0); // Cast(Round(fp_bias / scale)) to int32. @@ -237,6 +242,7 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(node.Name() + "_bias_int32"), &bias_int32_type_proto); Node& cast_node = graph.AddNode(graph.GenerateNodeName(node.Name() + "_bias_int32"), "Cast", "Bias INT32 node", {&bias_div_round_arg}, {&bias_int32_arg}, nullptr, node.Domain()); + optimizer_utils::DuplicateNodeAnnotation(node, cast_node); cast_node.AddAttribute("to", static_cast(ONNX_NAMESPACE::TensorProto_DataType_INT32)); graph.AddEdge(round_node.Index(), cast_node.Index(), 0, 0); @@ -246,6 +252,7 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph Node& bias_dq_node = graph.AddNode(graph.GenerateNodeName(node.Name() + "_bias_dq"), QDQ::DQOpName, "Bias DQ node", {&bias_int32_arg, &bias_scale_arg}, {&bias_dq_arg}, nullptr, node.Domain()); + optimizer_utils::DuplicateNodeAnnotation(node, bias_dq_node); if (!is_per_tensor_scale) { bias_dq_node.AddAttribute("axis", static_cast(0)); } diff --git a/onnxruntime/core/optimizer/qdq_transformer/where_dummy_dq.cc b/onnxruntime/core/optimizer/qdq_transformer/where_dummy_dq.cc index 9bd91e7916ecb..082a42b32a963 100644 --- a/onnxruntime/core/optimizer/qdq_transformer/where_dummy_dq.cc +++ b/onnxruntime/core/optimizer/qdq_transformer/where_dummy_dq.cc @@ -137,6 +137,8 @@ Status WhereDummyDq::InsertDummyDQ(Node& node, Graph& graph, bool& modified, con nullptr, dq_node->Domain()); + optimizer_utils::DuplicateNodeAnnotation(node, dummy_dq_node); + node.MutableInputDefs()[const_idx] = &dummy_dq_arg; if (graph.GetConsumerNodes(where_inputs[const_idx]->Name()).size() == 0) { graph.RemoveInitializedTensor(where_inputs[const_idx]->Name()); diff --git a/onnxruntime/core/optimizer/reshape_fusion.cc b/onnxruntime/core/optimizer/reshape_fusion.cc index daab9bba278aa..d72f9186dbd39 100644 --- a/onnxruntime/core/optimizer/reshape_fusion.cc +++ b/onnxruntime/core/optimizer/reshape_fusion.cc @@ -496,6 +496,7 @@ bool ReshapeFusion::FuseContiguousReshapes(Node& reshape, Graph& graph) { Node& reshape_node = graph.AddNode(graph.GenerateNodeName(name + "_new_reshape"), "Reshape", "Reshape for " + name, {contiguous_reshapes[0].get().MutableInputDefs()[0], shape_arg}, {contiguous_reshapes.back().get().MutableOutputDefs()[0]}); + optimizer_utils::DuplicateNodeAnnotation(reshape, reshape_node); reshape_node.SetExecutionProviderType(contiguous_reshapes[0].get().GetExecutionProviderType()); graph_utils::FinalizeNodeFusion(graph, contiguous_reshapes, reshape_node); diff --git a/onnxruntime/core/optimizer/utils.cc b/onnxruntime/core/optimizer/utils.cc index 4a323eefe1fe7..6d40b389d5fa3 100644 --- a/onnxruntime/core/optimizer/utils.cc +++ b/onnxruntime/core/optimizer/utils.cc @@ -495,6 +495,13 @@ bool IsScalar(const NodeArg& input_arg) { return dim_size == 0 || (dim_size == 1 && shape->dim(0).has_dim_value() && shape->dim(0).dim_value() == 1); } +void DuplicateNodeAnnotation(const Node& src, Node& dst) { + const auto& src_annotation = src.GetLayeringAnnotation(); + if (!src_annotation.empty()) { + dst.SetLayeringAnnotation(src_annotation); + } +} + template bool GetScalarInitializerValue(const onnxruntime::Graph& graph, const onnxruntime::NodeArg& input_arg, T& value, bool is_constant) { diff --git a/onnxruntime/core/optimizer/utils.h b/onnxruntime/core/optimizer/utils.h index 857640f861238..2f9b48df7a75f 100644 --- a/onnxruntime/core/optimizer/utils.h +++ b/onnxruntime/core/optimizer/utils.h @@ -175,6 +175,8 @@ bool CheckOutputEdges(const Graph& graph, const Node& node, size_t expected_outp // Check if NodeArg takes in a scalar tensor. bool IsScalar(const NodeArg& input_arg); +void DuplicateNodeAnnotation(const Node& src, Node& dst); + #endif // #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) } // namespace optimizer_utils diff --git a/onnxruntime/core/session/inference_session.cc b/onnxruntime/core/session/inference_session.cc index 0944be87591e2..ce5a613d02e1f 100644 --- a/onnxruntime/core/session/inference_session.cc +++ b/onnxruntime/core/session/inference_session.cc @@ -29,6 +29,7 @@ #include "core/framework/kernel_registry.h" #include "core/framework/kernel_type_str_resolver.h" #include "core/framework/kernel_type_str_resolver_utils.h" +#include "core/framework/layering_annotations.h" #include "core/framework/mldata_type_utils.h" #include "core/framework/TensorSeq.h" #include "core/framework/tensorprotoutils.h" @@ -1493,9 +1494,21 @@ common::Status InferenceSession::TransformGraph(onnxruntime::Graph& graph, bool } } + LayeringIndex* layering_index = nullptr; +#if !defined(ORT_MINIMAL_BUILD) + std::optional layering_index_storage; + const auto layering_config = session_options_.config_options.GetConfigOrDefault(kOrtSessionOptionsLayerAssignmentSettings, ""); + if (!layering_config.empty()) { + ORT_RETURN_IF_ERROR_SESSIONID_(LayeringIndex::Create(graph, layering_config, {}, execution_providers_, + *session_logger_, layering_index_storage)); + if (layering_index_storage) { + layering_index = &layering_index_storage.value(); + } + } +#endif // Do partitioning based on execution providers' capabilities. ORT_RETURN_IF_ERROR_SESSIONID_(partitioner.Partition(graph, session_state_->GetMutableFuncMgr(), transform_layout_fn, - session_options_.config_options, *session_logger_, + session_options_.config_options, *session_logger_, layering_index, mode, session_options_.GetEpContextGenerationOptions(), debug_graph_fn)); // Get graph optimizations loop level from session config, if not present, set to default value of 1 as per diff --git a/onnxruntime/test/framework/layering_annotations_test.cc b/onnxruntime/test/framework/layering_annotations_test.cc index 1038f586ae194..b99de1a705152 100644 --- a/onnxruntime/test/framework/layering_annotations_test.cc +++ b/onnxruntime/test/framework/layering_annotations_test.cc @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#if !defined(ORT_MINIMAL_BUILD) + #include "core/framework/ortmemoryinfo.h" #include "core/framework/layering_annotations.h" #include "core/session/abi_devices.h" @@ -954,4 +956,6 @@ TEST(LayeringRulesTest, FromConfigString_IgnoresEmptyEntries) { } } // namespace test -} // namespace onnxruntime \ No newline at end of file +} // namespace onnxruntime + +#endif // ORT_MINIMAL_BUILD From debc8dd240329d7526164303881e00d5bdca2480 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 29 Jan 2026 10:54:50 -0800 Subject: [PATCH 03/57] Fix up annotations with Transpose Optimizer --- .../onnx_transpose_optimization.cc | 15 +++++++++++++++ .../transpose_optimization/optimizer_api.h | 12 ++++++++++++ .../ort_optimizer_api_impl.cc | 11 +++++++++++ onnxruntime/core/session/inference_session.cc | 1 + onnxruntime/test/framework/session_state_test.cc | 8 +++++--- 5 files changed, 44 insertions(+), 3 deletions(-) diff --git a/onnxruntime/core/optimizer/transpose_optimization/onnx_transpose_optimization.cc b/onnxruntime/core/optimizer/transpose_optimization/onnx_transpose_optimization.cc index 6f2538bcde3b1..3b46f582dd7c9 100644 --- a/onnxruntime/core/optimizer/transpose_optimization/onnx_transpose_optimization.cc +++ b/onnxruntime/core/optimizer/transpose_optimization/onnx_transpose_optimization.cc @@ -528,6 +528,7 @@ static bool MakeQDQNodeUnit(api::GraphRef& graph, const api::NodeRef& dq_node) { // Add Q auto new_q_node = MakeQuantizeOp(graph, dq_domain, inputs, axis, dq_node.GetAttributeInt("block_size"), dq_node.GetAttributeInt("output_dtype"), dq_node.GetAttributeInt("saturate")); + new_q_node->SetLayeringAnnotation(dq_node.GetLayeringAnnotation()); auto q_node_outputs = new_q_node->Outputs(); // copy value info from the dq input for the type information, and update the shape to match next_node's output @@ -540,6 +541,7 @@ static bool MakeQDQNodeUnit(api::GraphRef& graph, const api::NodeRef& dq_node) { // Add DQ auto new_dq_node = MakeDequantizeOp(graph, dq_domain, inputs, axis, dq_node.GetAttributeInt("block_size")); + new_dq_node->SetLayeringAnnotation(dq_node.GetLayeringAnnotation()); auto dq_node_outputs = new_dq_node->Outputs(); // straight copy of value info as the type and shape are the same as next_node's output @@ -1004,6 +1006,7 @@ static void UnsqueezeInput(OptimizerCtx& ctx, api::NodeRef& node, size_t i, cons // (see Case 2). if (consumers->nodes.size() > 0) { auto squeeze_ptr = MakeSqueezeOrUnsqueeze(ctx.opset, ctx.graph, "Squeeze", value_to_modify, axes); + squeeze_ptr->SetLayeringAnnotation(node.GetLayeringAnnotation()); api::NodeRef& squeeze = *squeeze_ptr; std::string_view sq_out = squeeze.Outputs()[0]; ctx.graph.CopyValueInfo(value_to_modify, sq_out); @@ -1072,6 +1075,7 @@ static void UnsqueezeInput(OptimizerCtx& ctx, api::NodeRef& node, size_t i, cons // Case 3: Add an Unsqueeze node. auto unsqueeze_ptr = MakeSqueezeOrUnsqueeze(ctx.opset, ctx.graph, "Unsqueeze", input, axes); + unsqueeze_ptr->SetLayeringAnnotation(node.GetLayeringAnnotation()); api::NodeRef& unsqueeze = *unsqueeze_ptr; std::string_view unsq_out = unsqueeze.Outputs()[0]; ctx.graph.CopyValueInfo(input, unsq_out); @@ -1204,6 +1208,7 @@ static void TransposeInputImpl(api::GraphRef& graph, api::NodeRef& node, size_t // Transpose the initializer. If there are existing consumers, add Transpose nodes to them using perm_inv // to counteract the effect. These Transposes will hopefully be optimized out later. auto transpose_inv_ptr = MakeTranspose(graph, constant_to_modify, perm_inv); + transpose_inv_ptr->SetLayeringAnnotation(node.GetLayeringAnnotation()); api::NodeRef& transpose_inv = *transpose_inv_ptr; std::string_view transpose_out = transpose_inv.Outputs()[0]; graph.CopyValueInfo(constant_to_modify, transpose_out); @@ -1264,6 +1269,7 @@ static void TransposeInputImpl(api::GraphRef& graph, api::NodeRef& node, size_t // the other Transpose. const std::vector& perm_combined = ComposePerm(*perm2, perm); auto transpose_ptr = MakeTranspose(graph, inp_node->Inputs()[0], perm_combined); + transpose_ptr->SetLayeringAnnotation(node.GetLayeringAnnotation()); api::NodeRef& transpose = *transpose_ptr; std::string_view transpose_out = transpose.Outputs()[0]; graph.CopyValueInfo(input, transpose_out); @@ -1298,6 +1304,7 @@ static void TransposeInputImpl(api::GraphRef& graph, api::NodeRef& node, size_t // Case 4: Add a new Transpose op auto transpose_ptr = MakeTranspose(graph, input, perm); + transpose_ptr->SetLayeringAnnotation(node.GetLayeringAnnotation()); api::NodeRef& transpose = *transpose_ptr; std::string_view transpose_out = transpose.Outputs()[0]; graph.CopyValueInfo(input, transpose_out); @@ -1373,6 +1380,7 @@ std::string_view TransposeOutput(api::GraphRef& graph, api::NodeRef& node, size_ // X -> Node -> Y, Transpose auto transpose = MakeTranspose(graph, "", perm); + transpose->SetLayeringAnnotation(node.GetLayeringAnnotation()); // X -> Node -> *Y', Transpose -> Y *shape/dtype not set graph.MoveOutput(node, i, *transpose, 0); @@ -1727,6 +1735,7 @@ static bool HandleShape(HandlerArgs& args) { // X -> Shape -> Y, Gather std::vector gather_inputs{"", perm_const}; auto gather_ptr = args.ctx.graph.AddNode("Gather", "Gather", gather_inputs, /*num_outputs*/ 1); + gather_ptr->SetLayeringAnnotation(args.node.GetLayeringAnnotation()); api::NodeRef& gather = *gather_ptr; gather.SetAttributeInt("axis", 0); @@ -1770,6 +1779,7 @@ static void PermuteInput(api::GraphRef& graph, api::NodeRef& node, size_t i, con std::string_view gather_indices_const = AddInitializerInt64(graph, /*shape*/ {rank_int}, perm); std::vector gather_inputs{input_name, gather_indices_const}; auto gather_ptr = graph.AddNode("Gather", "Gather", gather_inputs, /*num_outputs*/ 1); + gather_ptr->SetLayeringAnnotation(node.GetLayeringAnnotation()); api::NodeRef& gather = *gather_ptr; std::string_view gather_output = gather.Outputs()[0]; graph.CopyValueInfo(input_name, gather_output); @@ -2218,6 +2228,7 @@ static bool HandleTile(HandlerArgs& args) { std::string_view perm_inv_const = AddInitializerInt64(args.ctx.graph, perm_shape, args.perm_inv); std::vector gather_inputs{repeats_inp, perm_inv_const}; auto gather_node_ptr = args.ctx.graph.AddNode("Gather", "Gather", gather_inputs, /*num_outputs*/ 1); + gather_node_ptr->SetLayeringAnnotation(args.node.GetLayeringAnnotation()); api::NodeRef& gather_node = *gather_node_ptr; std::string_view gather_output = gather_node.Outputs()[0]; args.ctx.graph.CopyValueInfo(repeats_inp, gather_output); @@ -2268,6 +2279,7 @@ static void RemoveCancelingTransposeNodes(HandlerArgs& args) { // despite computing the same value. Use an Identity op instead. std::vector single_empty_input{""}; auto identity_ptr = args.ctx.graph.AddNode("Identity", "Identity", single_empty_input, /*num_outputs*/ 1); + identity_ptr->SetLayeringAnnotation(args.node.GetLayeringAnnotation()); api::NodeRef& identity = *identity_ptr; args.ctx.graph.MoveOutput(args.node, 0, identity, 0); identity.SetInput(0, transpose_input); @@ -2300,6 +2312,7 @@ static bool HandleTransposeImpl(HandlerArgs& args, const std::vector& n // use the same input as the 1st Transpose, move the output from the Reshape to the new Transpose node, // and remove the Reshape node. new_node = args.ctx.graph.AddNode("Transpose", "Transpose", {args.transpose.Inputs()[0]}, 1); + new_node->SetLayeringAnnotation(args.node.GetLayeringAnnotation()); args.ctx.graph.MoveOutput(args.node, 0, *new_node, 0); args.ctx.graph.RemoveNode(args.node); } else { @@ -2970,6 +2983,7 @@ static bool TryFixTransposeMissingDQ(OptimizerCtx& ctx, api::NodeRef& transpose_ // Add Q auto new_q_node = MakeQuantizeOp(ctx.graph, q_domain, inputs, axis, q_node.GetAttributeInt("block_size"), q_node.GetAttributeInt("output_dtype"), q_node.GetAttributeInt("saturate")); + new_q_node->SetLayeringAnnotation(transpose_node.GetLayeringAnnotation()); auto new_q_node_output = new_q_node->Outputs()[0]; // Copy value info from the q output for the type information, and update the shape to match Transpose's input @@ -2982,6 +2996,7 @@ static bool TryFixTransposeMissingDQ(OptimizerCtx& ctx, api::NodeRef& transpose_ // Add new DQ. auto new_dq_node = MakeDequantizeOp(ctx.graph, q_domain, inputs, axis, q_node.GetAttributeInt("block_size")); + new_dq_node->SetLayeringAnnotation(transpose_node.GetLayeringAnnotation()); auto new_dq_node_output = new_dq_node->Outputs()[0]; ctx.graph.CopyValueInfo(transpose_input_name, new_dq_node_output); diff --git a/onnxruntime/core/optimizer/transpose_optimization/optimizer_api.h b/onnxruntime/core/optimizer/transpose_optimization/optimizer_api.h index 6ff4da05fbf57..4ee5a65b9b9fb 100644 --- a/onnxruntime/core/optimizer/transpose_optimization/optimizer_api.h +++ b/onnxruntime/core/optimizer/transpose_optimization/optimizer_api.h @@ -258,6 +258,18 @@ class NodeRef { /// Id virtual int64_t Id() const = 0; + /// + /// Get the layering annotation of the node. + /// + /// annotation + virtual std::string_view GetLayeringAnnotation() const = 0; + + /// + /// Set layering annotation + /// + /// + virtual void SetLayeringAnnotation(std::string_view annotation) = 0; + virtual ~NodeRef() {}; }; diff --git a/onnxruntime/core/optimizer/transpose_optimization/ort_optimizer_api_impl.cc b/onnxruntime/core/optimizer/transpose_optimization/ort_optimizer_api_impl.cc index 6a02ca3578da2..5d5ed663cca05 100644 --- a/onnxruntime/core/optimizer/transpose_optimization/ort_optimizer_api_impl.cc +++ b/onnxruntime/core/optimizer/transpose_optimization/ort_optimizer_api_impl.cc @@ -105,6 +105,14 @@ class ApiNode final : public api::NodeRef { int SinceVersion() const override; int64_t Id() const override; + std::string_view GetLayeringAnnotation() const override { + return node_.GetLayeringAnnotation(); + } + + void SetLayeringAnnotation(std::string_view annotation) override { + node_.SetLayeringAnnotation(std::string(annotation)); + } + private: ORT_DISALLOW_COPY_ASSIGNMENT_AND_MOVE(ApiNode); }; @@ -763,6 +771,9 @@ std::unique_ptr ApiGraph::CopyNode(const api::NodeRef& source_node source_node.Outputs().size(), domain, new_node_since_version, source_node.GetExecutionProviderType()); + const auto& layering_annotation = source_node.GetLayeringAnnotation(); + node.SetLayeringAnnotation(std::string(layering_annotation)); + std::unique_ptr new_node = std::make_unique(node, graph_); new_node->CopyAttributes(source_node); diff --git a/onnxruntime/core/session/inference_session.cc b/onnxruntime/core/session/inference_session.cc index ce5a613d02e1f..26748494ad7ec 100644 --- a/onnxruntime/core/session/inference_session.cc +++ b/onnxruntime/core/session/inference_session.cc @@ -2021,6 +2021,7 @@ Status PartitionOrtFormatModel(onnxruntime::Graph& graph, transform_layout_fn, sess_options.config_options, logger, + nullptr /*layering_index*/, GraphPartitioner::Mode::kOrtFormatLoad)); #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) diff --git a/onnxruntime/test/framework/session_state_test.cc b/onnxruntime/test/framework/session_state_test.cc index 87d5757c0bfd4..b8131de7f6050 100644 --- a/onnxruntime/test/framework/session_state_test.cc +++ b/onnxruntime/test/framework/session_state_test.cc @@ -281,7 +281,7 @@ TEST_P(SessionStateTestP, TestInitializerProcessing) { graph, modified, execution_provider, std::move(cpu_allocator), debug_graph_fn); }, sess_options.config_options, - DefaultLoggingManager().DefaultLogger())); + DefaultLoggingManager().DefaultLogger(), nullptr /*layering_index*/)); ASSERT_STATUS_OK(session_state.FinalizeSessionState(oss.str(), krm)); @@ -368,7 +368,8 @@ TEST(SessionStateTest, TestInitializerMemoryAllocatedUsingNonArenaMemory) { cpu_allocator, debug_graph_fn); }, sess_options.config_options, - default_logger)); + default_logger, + nullptr /*layering_index*/)); EXPECT_STATUS_OK(session_state.FinalizeSessionState(model.ModelPath(), krm)); @@ -456,7 +457,8 @@ void LoadWithResourceAwarePartitioning(const ORTCHAR_T* model_path, layout_transformation::DebugGraphFn debug_graph_fn; ASSERT_STATUS_OK( partitioner.Partition(graph, session_state.GetMutableFuncMgr(), transform_layout_fn, - sess_options.config_options, default_logger, GraphPartitioner::Mode::kNormal, + sess_options.config_options, default_logger, nullptr /*layering_index*/, + GraphPartitioner::Mode::kNormal, epctx::ModelGenOptions{}, debug_graph_fn)); From 8f0ff86ec372a6eca144348733c7f266919943d0 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 29 Jan 2026 12:30:40 -0800 Subject: [PATCH 04/57] Add ORT_EXTENDED_MINIMAL build --- onnxruntime/core/framework/layering_annotations.cc | 11 +++++++---- onnxruntime/core/framework/layering_annotations.h | 8 ++++++-- onnxruntime/core/framework/resource_accountant.cc | 2 +- onnxruntime/core/session/inference_session.cc | 4 ++-- .../test/framework/layering_annotations_test.cc | 4 ++-- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc index 5fc5bc9aa889a..c73f30684cae5 100644 --- a/onnxruntime/core/framework/layering_annotations.cc +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#if !defined(ORT_MINIMAL_BUILD) +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) #include "core/graph/constants.h" #include "core/common/narrow.h" @@ -433,7 +433,10 @@ Status LayeringIndex::Create(Graph& graph, const auto& rule = rules.rules[i]; // 1. Try matching against ep_devices (from session options) - std::optional matched_ep = EpLayeringMatcher::Match(ep_devices, rule); + std::optional matched_ep; + if (!ep_devices.empty()) { + matched_ep = EpLayeringMatcher::Match(ep_devices, rule); + } // 2. If not matched, try matching against Registered EPs if (!matched_ep) { @@ -464,7 +467,7 @@ Status LayeringIndex::Create(Graph& graph, return Status::OK(); } -// Process to to bottom-up assign layering indices to nodes +// Process top to bottom-up assign layering indices to nodes void LayeringIndex::ProcessGraph(Graph& graph, const LayeringRuleMatcher& matcher, std::optional parent_layer_id) { // 3. Create entry for this graph instance @@ -514,4 +517,4 @@ void LayeringIndex::ProcessGraph(Graph& graph, const LayeringRuleMatcher& matche } // namespace onnxruntime -#endif // !defined(ORT_MINIMAL_BUILD) +#endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) diff --git a/onnxruntime/core/framework/layering_annotations.h b/onnxruntime/core/framework/layering_annotations.h index 93fdf5cef23ec..b061a56392a1a 100644 --- a/onnxruntime/core/framework/layering_annotations.h +++ b/onnxruntime/core/framework/layering_annotations.h @@ -3,7 +3,7 @@ #pragma once -#if !defined(ORT_MINIMAL_BUILD) +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) #include "core/common/inlined_containers.h" #include "core/common/status.h" @@ -254,4 +254,8 @@ class LayeringIndex { } // namespace onnxruntime -#endif // !defined(ORT_MINIMAL_BUILD) +#else +namespace onnxruntime { +class LayeringIndex; +} +#endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) diff --git a/onnxruntime/core/framework/resource_accountant.cc b/onnxruntime/core/framework/resource_accountant.cc index a08d6e625abee..ce9a9d4d17358 100644 --- a/onnxruntime/core/framework/resource_accountant.cc +++ b/onnxruntime/core/framework/resource_accountant.cc @@ -102,7 +102,7 @@ class WeightsSizeBasedAccountant : public IResourceAccountant { if (!input_def->Exists()) continue; const auto& name = input_def->Name(); - bool check_outer_scope = true; + constexpr bool check_outer_scope = true; const auto* tensor_proto = graph->GetInitializer(name, check_outer_scope); if (tensor_proto) { diff --git a/onnxruntime/core/session/inference_session.cc b/onnxruntime/core/session/inference_session.cc index 26748494ad7ec..08549161de313 100644 --- a/onnxruntime/core/session/inference_session.cc +++ b/onnxruntime/core/session/inference_session.cc @@ -1495,7 +1495,7 @@ common::Status InferenceSession::TransformGraph(onnxruntime::Graph& graph, bool } LayeringIndex* layering_index = nullptr; -#if !defined(ORT_MINIMAL_BUILD) +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) std::optional layering_index_storage; const auto layering_config = session_options_.config_options.GetConfigOrDefault(kOrtSessionOptionsLayerAssignmentSettings, ""); if (!layering_config.empty()) { @@ -1505,7 +1505,7 @@ common::Status InferenceSession::TransformGraph(onnxruntime::Graph& graph, bool layering_index = &layering_index_storage.value(); } } -#endif +#endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) // Do partitioning based on execution providers' capabilities. ORT_RETURN_IF_ERROR_SESSIONID_(partitioner.Partition(graph, session_state_->GetMutableFuncMgr(), transform_layout_fn, session_options_.config_options, *session_logger_, layering_index, diff --git a/onnxruntime/test/framework/layering_annotations_test.cc b/onnxruntime/test/framework/layering_annotations_test.cc index b99de1a705152..cf54141add09a 100644 --- a/onnxruntime/test/framework/layering_annotations_test.cc +++ b/onnxruntime/test/framework/layering_annotations_test.cc @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#if !defined(ORT_MINIMAL_BUILD) +#if !defined(ORT_MINIMAL_BUILD) && defined(ORT_EXTENDED_MINIMAL_BUILD) #include "core/framework/ortmemoryinfo.h" #include "core/framework/layering_annotations.h" @@ -958,4 +958,4 @@ TEST(LayeringRulesTest, FromConfigString_IgnoresEmptyEntries) { } // namespace test } // namespace onnxruntime -#endif // ORT_MINIMAL_BUILD +#endif // !defined(ORT_MINIMAL_BUILD) && defined(ORT_EXTENDED_MINIMAL_BUILD) From f7a422ed20829e66960c18bf72bdd28d5ddb5fdc Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 29 Jan 2026 16:22:49 -0800 Subject: [PATCH 05/57] Move rules and matcher inside the index --- .../core/framework/layering_annotations.cc | 16 +++++++--------- .../core/framework/layering_annotations.h | 12 ++++++++---- .../test/framework/layering_annotations_test.cc | 14 +++++--------- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc index c73f30684cae5..2e68f2f68f216 100644 --- a/onnxruntime/core/framework/layering_annotations.cc +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -396,12 +396,12 @@ std::optional EpLayeringMatcher::Match(const ExecutionProviders& pr LayeringIndex LayeringIndex::Create(Graph& graph, EpNameToLayeringIndices ep_map, LayeringIndexToEpName rule_map, - const LayeringRuleMatcher& matcher) { + LayeringRules layering_rules) { // 1. Create LayeringIndex instance with pre-computed maps - LayeringIndex index(std::move(ep_map), std::move(rule_map)); + LayeringIndex index(std::move(layering_rules), std::move(ep_map), std::move(rule_map)); // 2. Traverse the graph and index nodes - index.ProcessGraph(graph, matcher, std::nullopt); + index.ProcessGraph(graph, std::nullopt); return index; } @@ -462,14 +462,12 @@ Status LayeringIndex::Create(Graph& graph, LOGS(logger, INFO) << "LayeringIndex created. Matched " << matched_rule_count << " out of " << rules.rules.size() << " rules to available Execution Providers."; - LayeringRuleMatcher matcher(rules); - layering_index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), matcher); + layering_index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); return Status::OK(); } // Process top to bottom-up assign layering indices to nodes -void LayeringIndex::ProcessGraph(Graph& graph, const LayeringRuleMatcher& matcher, - std::optional parent_layer_id) { +void LayeringIndex::ProcessGraph(Graph& graph, std::optional parent_layer_id) { // 3. Create entry for this graph instance GraphLayeringIndex& current_graph_index = graph_index_[&graph]; @@ -480,7 +478,7 @@ void LayeringIndex::ProcessGraph(Graph& graph, const LayeringRuleMatcher& matche const std::string& annotation = node.GetLayeringAnnotation(); if (!annotation.empty()) { // If it has an annotation try to match it - matched_rule_idx = matcher.Match(annotation); + matched_rule_idx = matcher_.Match(annotation); // Save memory and clear the annotation since it's no longer needed node.ClearLayeringAnnotation(); } @@ -509,7 +507,7 @@ void LayeringIndex::ProcessGraph(Graph& graph, const LayeringRuleMatcher& matche if (node.ContainsSubgraph()) { const std::optional subgraph_parent_assignment = matched_rule_idx; for (auto& [attr_name, subgraph] : node.GetMutableMapOfAttributeNameToSubgraph()) { - ProcessGraph(*subgraph, matcher, subgraph_parent_assignment); + ProcessGraph(*subgraph, subgraph_parent_assignment); } } } diff --git a/onnxruntime/core/framework/layering_annotations.h b/onnxruntime/core/framework/layering_annotations.h index b061a56392a1a..fdcde51fba649 100644 --- a/onnxruntime/core/framework/layering_annotations.h +++ b/onnxruntime/core/framework/layering_annotations.h @@ -139,7 +139,7 @@ class LayeringIndex { static LayeringIndex Create(Graph& graph, EpNameToLayeringIndices ep_map, LayeringIndexToEpName rule_map, - const LayeringRuleMatcher& matcher); + LayeringRules layering_rules); /// /// Factory method that creates a LayeringIndex by parsing configuration, matching rules against @@ -217,6 +217,8 @@ class LayeringIndex { } private: + LayeringRules rules_; + LayeringRuleMatcher matcher_; // These stay constant EpNameToLayeringIndices ep_name_to_layering_indices_; LayeringIndexToEpName layering_index_to_ep_name_; @@ -242,14 +244,16 @@ class LayeringIndex { LayeringIndex() = default; - LayeringIndex(EpNameToLayeringIndices ep_name_to_layering_indices, LayeringIndexToEpName layering_index_to_ep_name) - : ep_name_to_layering_indices_(std::move(ep_name_to_layering_indices)), + LayeringIndex(LayeringRules layering_rules, EpNameToLayeringIndices ep_name_to_layering_indices, LayeringIndexToEpName layering_index_to_ep_name) + : rules_(std::move(layering_rules)), + matcher_(rules_), + ep_name_to_layering_indices_(std::move(ep_name_to_layering_indices)), layering_index_to_ep_name_(std::move(layering_index_to_ep_name)) {} // Graph and sub-graphs mapping to their indices InlinedHashMap graph_index_; - void ProcessGraph(Graph& graph, const LayeringRuleMatcher& matcher, std::optional parent_layer_id); + void ProcessGraph(Graph& graph, std::optional parent_layer_id); }; } // namespace onnxruntime diff --git a/onnxruntime/test/framework/layering_annotations_test.cc b/onnxruntime/test/framework/layering_annotations_test.cc index cf54141add09a..e8049c4f08e3e 100644 --- a/onnxruntime/test/framework/layering_annotations_test.cc +++ b/onnxruntime/test/framework/layering_annotations_test.cc @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -#if !defined(ORT_MINIMAL_BUILD) && defined(ORT_EXTENDED_MINIMAL_BUILD) +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) #include "core/framework/ortmemoryinfo.h" #include "core/framework/layering_annotations.h" @@ -625,7 +625,7 @@ TEST(LayeringIndexTest, AssignNodesBasedOnAnnotations) { rule_map[1] = "DeviceB"; // 4. Create LayeringIndex - auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), matcher); + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); // 5. Verify Assignments // Node 0: Annotated "RuleA" -> Index 0 -> DeviceA @@ -668,7 +668,6 @@ TEST(LayeringIndexTest, AssignNodeWithInvalidEpMapping) { // 2. Setup Rules: RuleX exists at index 0, maps to "PhantomDevice" LayeringRules rules; rules.rules.push_back({"PhantomDevice", "RuleX", false}); // Index 0 - LayeringRuleMatcher matcher(rules); // 3. Setup Mappings: But "PhantomDevice" is NOT in the mappings (simulating EP unavailable) LayeringIndex::EpNameToLayeringIndices ep_map; @@ -678,8 +677,7 @@ TEST(LayeringIndexTest, AssignNodeWithInvalidEpMapping) { // rule_map[0] is missing // 4. Create Index - auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), matcher); - + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); // 5. Verify: Node should NOT be assigned because the mapped EP is missing auto assign = index.GetNodeAssignment(graph, node.Index()); EXPECT_FALSE(assign.has_value()); @@ -744,7 +742,6 @@ TEST(LayeringIndexTest, SubgraphInheritance) { // 2. Setup Rules LayeringRules rules; rules.rules.push_back({"DeviceA", "RuleA", false}); // Index 0 - LayeringRuleMatcher matcher(rules); LayeringIndex::EpNameToLayeringIndices ep_map; ep_map["DeviceA"].insert(0); @@ -752,7 +749,7 @@ TEST(LayeringIndexTest, SubgraphInheritance) { rule_map[0] = "DeviceA"; // 3. Create Index - auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), matcher); + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); // 4. Verify Parent Assignment auto assign_parent = index.GetNodeAssignment(graph, if_node.Index()); @@ -836,7 +833,6 @@ TEST(LayeringIndexTest, SubgraphOverride) { LayeringRules rules; rules.rules.push_back({"DeviceA", "RuleA", false}); rules.rules.push_back({"DeviceB", "RuleB", false}); - LayeringRuleMatcher matcher(rules); LayeringIndex::EpNameToLayeringIndices ep_map; ep_map["DeviceA"].insert(0); @@ -845,7 +841,7 @@ TEST(LayeringIndexTest, SubgraphOverride) { rule_map[0] = "DeviceA"; rule_map[1] = "DeviceB"; - auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), matcher); + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); // Verify Parent = 0 auto assign_parent = index.GetNodeAssignment(graph, if_node.Index()); From 1ef4078c88ff2f437cd63fbdd78d0ef2a0eb333f Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 29 Jan 2026 17:55:14 -0800 Subject: [PATCH 06/57] Add Update with tests --- include/onnxruntime/core/graph/graph.h | 7 +- .../core/framework/layering_annotations.cc | 69 ++++++++++++++++++- .../core/framework/layering_annotations.h | 14 ++-- .../framework/layering_annotations_test.cc | 43 ++++++++++++ 4 files changed, 123 insertions(+), 10 deletions(-) diff --git a/include/onnxruntime/core/graph/graph.h b/include/onnxruntime/core/graph/graph.h index 79a6bd7cde779..9b931b8658ff4 100644 --- a/include/onnxruntime/core/graph/graph.h +++ b/include/onnxruntime/core/graph/graph.h @@ -174,10 +174,11 @@ class Node { */ void SetSinceVersion(int since_version) noexcept { since_version_ = since_version; } -#if !defined(ORT_MINIMAL_BUILD) void SetLayeringAnnotation(std::string annotation) { layering_annotation_ = std::move(annotation); } - const std::string& GetLayeringAnnotation() const { return layering_annotation_; } + const std::string& GetLayeringAnnotation() const noexcept { return layering_annotation_; } + +#if !defined(ORT_MINIMAL_BUILD) void ClearLayeringAnnotation() { std::string t; @@ -696,9 +697,7 @@ class Node { // Graph instances for subgraphs that are owned by this Node std::vector> subgraphs_; -#if !defined(ORT_MINIMAL_BUILD) std::string layering_annotation_; -#endif // Can be saved? The node cannot be saved anymore if removable attributes have been cleared. bool can_be_saved_; diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc index 2e68f2f68f216..bd2ab7429a8a1 100644 --- a/onnxruntime/core/framework/layering_annotations.cc +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -469,7 +469,17 @@ Status LayeringIndex::Create(Graph& graph, // Process top to bottom-up assign layering indices to nodes void LayeringIndex::ProcessGraph(Graph& graph, std::optional parent_layer_id) { // 3. Create entry for this graph instance - GraphLayeringIndex& current_graph_index = graph_index_[&graph]; + bool was_updated = false; + std::optional new_index; + GraphLayeringIndex* current_graph_index_ptr = nullptr; + auto found = graph_index_.find(&graph); + if (found != graph_index_.end()) { + current_graph_index_ptr = &found->second; + } else { + new_index.emplace(); + current_graph_index_ptr = &(*new_index); + } + GraphLayeringIndex& current_graph_index = *current_graph_index_ptr; for (auto& node : graph.Nodes()) { std::optional matched_rule_idx = std::nullopt; @@ -496,6 +506,7 @@ void LayeringIndex::ProcessGraph(Graph& graph, std::optional parent_laye if (layering_index_to_ep_name_.count(rule_idx)) { ORT_IGNORE_RETURN_VALUE(current_graph_index.node_to_layering_index_.insert_or_assign(node.Index(), rule_idx)); ORT_IGNORE_RETURN_VALUE(current_graph_index.layer_to_node_ids_[rule_idx].insert(node.Index())); + was_updated = true; } else { // reset since no valid EP mapping // so it does not propagate to sub-graphs if any @@ -511,8 +522,64 @@ void LayeringIndex::ProcessGraph(Graph& graph, std::optional parent_laye } } } + if (was_updated && new_index) { + graph_index_.emplace(&graph, std::move(*new_index)); + } } +void LayeringIndex::Update(Graph& graph, gsl::span nodes) { + // Ensure we have an entry for this graph (creating it if it doesn't exist, though typically it should) + bool was_updated = false; + std::optional new_index; + GraphLayeringIndex* current_graph_index_ptr = nullptr; + auto found = graph_index_.find(&graph); + if (found != graph_index_.end()) { + current_graph_index_ptr = &found->second; + } else { + new_index.emplace(); + current_graph_index_ptr = &(*new_index); + } + + auto& current_graph_index = *current_graph_index_ptr; + + for (NodeIndex node_index : nodes) { + // GetMutableNode because we want to ClearLayeringAnnotation if we use it + Node* node = graph.GetNode(node_index); + if (!node) { + continue; + } + + const std::string& annotation = node->GetLayeringAnnotation(); + if (!annotation.empty()) { + auto matched_rule_idx = matcher_.Match(annotation); + // Consume the annotation + node->ClearLayeringAnnotation(); + + if (matched_rule_idx) { + const size_t rule_idx = *matched_rule_idx; + + // Only assign if this rule maps to a valid EP in our configuration + if (layering_index_to_ep_name_.count(rule_idx)) { + // Check if already assigned to a DIFFERENT rule, if so clean up old mapping + auto prev_assign = current_graph_index.node_to_layering_index_.find(node_index); + if (prev_assign != current_graph_index.node_to_layering_index_.end()) { + size_t old_rule = prev_assign->second; + if (old_rule != rule_idx) { + current_graph_index.layer_to_node_ids_[old_rule].erase(node_index); + } + } + + ORT_IGNORE_RETURN_VALUE(current_graph_index.node_to_layering_index_.insert_or_assign(node_index, rule_idx)); + ORT_IGNORE_RETURN_VALUE(current_graph_index.layer_to_node_ids_[rule_idx].insert(node_index)); + was_updated = true; + } + } + } + } + if (was_updated && new_index) { + graph_index_.emplace(&graph, std::move(*new_index)); + } +} } // namespace onnxruntime #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) diff --git a/onnxruntime/core/framework/layering_annotations.h b/onnxruntime/core/framework/layering_annotations.h index fdcde51fba649..58565b34e0256 100644 --- a/onnxruntime/core/framework/layering_annotations.h +++ b/onnxruntime/core/framework/layering_annotations.h @@ -174,8 +174,6 @@ class LayeringIndex { std::optional GetNodeAssignment(const Graph& graph, NodeIndex node_id) const { auto hit = graph_index_.find(&graph); if (hit == graph_index_.end()) { - // this should not be possible - assert(false); return {}; } @@ -194,8 +192,6 @@ class LayeringIndex { void MakeNodeUnassigned(const Graph& graph, NodeIndex node_id) { auto hit = graph_index_.find(&graph); if (hit == graph_index_.end()) { - // this should not be possible - assert(false); return; } auto& graph_layering_index = hit->second; @@ -206,7 +202,6 @@ class LayeringIndex { layer_idx = node_to_layer_hit->second; graph_layering_index.node_to_layering_index_.erase(node_to_layer_hit); } - // Remove node from layer collection if (layer_idx) { auto layer_to_nodes_hit = graph_layering_index.layer_to_node_ids_.find(*layer_idx); @@ -216,6 +211,15 @@ class LayeringIndex { } } + /// + /// Updates the layering index for a specific set of nodes in a graph. + /// This checks if the nodes have annotations, and if so, matches them against the rules + /// and updates the assignment. + /// + /// The graph containing the nodes. + /// Pixels of nodes to check and update. + void Update(Graph& graph, gsl::span nodes); + private: LayeringRules rules_; LayeringRuleMatcher matcher_; diff --git a/onnxruntime/test/framework/layering_annotations_test.cc b/onnxruntime/test/framework/layering_annotations_test.cc index e8049c4f08e3e..ecbdd0ae15beb 100644 --- a/onnxruntime/test/framework/layering_annotations_test.cc +++ b/onnxruntime/test/framework/layering_annotations_test.cc @@ -854,6 +854,49 @@ TEST(LayeringIndexTest, SubgraphOverride) { EXPECT_EQ(*assign_sub, 1u); } +TEST(LayeringIndexTest, UpdateIndex) { + // 1. Setup Graph with one node + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* input_arg = &graph.GetOrCreateNodeArg("input", &type_proto); + NodeArg* output_arg = &graph.GetOrCreateNodeArg("output", &type_proto); + + Node& node = graph.AddNode("node", "Abs", "Node", {input_arg}, {output_arg}); + ASSERT_STATUS_OK(graph.Resolve()); + + // 2. Setup Rules and Index + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); // Index 0 + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + + // Creates index (node has no annotation, so not assigned) + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + EXPECT_FALSE(index.GetNodeAssignment(graph, node.Index()).has_value()); + + // 3. Update Node with Annotation + node.SetLayeringAnnotation("RuleA"); + + // 4. Call Update + std::vector nodes_to_update = {node.Index()}; + index.Update(graph, nodes_to_update); + + // 5. Verify Assignment + auto assignment = index.GetNodeAssignment(graph, node.Index()); + ASSERT_TRUE(assignment.has_value()); + EXPECT_EQ(*assignment, 0u); +} + TEST(LayeringRulesTest, LayeringRulesParsing) { // Test empty string { From 828eca398f5c1109e29aad1e127eb828db171b02 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Fri, 30 Jan 2026 11:32:11 -0800 Subject: [PATCH 07/57] TODO: Consider not removing annotations since the the Update after layout trnasformation may rely on them. Also [ RUN ] AttentionTest.Attention3DDefault GPU Compute Capability: SM 6.1 (value: 610) Assertion failed: data.IsUnfused(), file D:\dev\ort_trans\onnxruntime\contrib_ops\cuda\bert\attention_prepare_qkv.cu, line 318 This may be related to uninitialized memory. --- .../core/framework/graph_partitioner.cc | 93 ++++++++++++++++++- 1 file changed, 89 insertions(+), 4 deletions(-) diff --git a/onnxruntime/core/framework/graph_partitioner.cc b/onnxruntime/core/framework/graph_partitioner.cc index cf078a96a412f..6456f5ac3914a 100644 --- a/onnxruntime/core/framework/graph_partitioner.cc +++ b/onnxruntime/core/framework/graph_partitioner.cc @@ -196,10 +196,78 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l auto& capabilities = params.capabilities.get(); const auto& graph_optimizer_registry = params.graph_optimizer_registry.get(); + InlinedVector assigned_filteredin_nodes; + // Helper to create a GraphViewer that filters nodes based on layering_index if present. + auto create_graph_viewer = [&](std::unique_ptr& sub_graph_holder) -> std::unique_ptr { +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + if (params.layering_index) { + sub_graph_holder = std::make_unique(); + sub_graph_holder->nodes.reserve(graph.NumberOfNodes()); + + auto rules_opt = params.layering_index->GetLayeringRulesForThisEp(ep_type); + + for (auto& node : graph.Nodes()) { + auto rule_idx_opt = params.layering_index->GetNodeAssignment(graph, node.Index()); + bool include = true; + if (rule_idx_opt) { + // If node has an assignment, include it only if it is assigned to this EP + if (!rules_opt || rules_opt->get().count(*rule_idx_opt) == 0) { + include = false; + } else { + assigned_filteredin_nodes.push_back(node.Index()); + } + } + // If node has no assignment, it is included (available to any EP) + + if (include) { + sub_graph_holder->nodes.push_back(node.Index()); + } + } + return std::make_unique(graph, *sub_graph_holder); + } +#endif + return std::make_unique(graph); + }; + + // Helper to unassign nodes that were assigned to this EP but not claimed by updated capabilities. + auto reset_assignment_unclaimed_nodes = [&]() { +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + if (params.layering_index) { + auto rules_opt = params.layering_index->GetLayeringRulesForThisEp(ep_type); + if (rules_opt) { + const auto& ep_rules = rules_opt->get(); + InlinedHashSet claimed; + for (const auto& cap : capabilities) { + if (cap && cap->sub_graph) { + for (auto idx : cap->sub_graph->nodes) claimed.insert(idx); + } + } + + // Check if all assigned filtered-in nodes are claimed + // and if not make them available for subsequent EPs + for (auto& node_index : assigned_filteredin_nodes) { + if (claimed.count(node_index) == 0) { + auto rule_idx_opt = params.layering_index->GetNodeAssignment(graph, node_index); + if (rule_idx_opt && ep_rules.count(*rule_idx_opt) > 0) { + params.layering_index->MakeNodeUnassigned(graph, node_index); + } + } + } + assigned_filteredin_nodes.clear(); + } + } +#endif + }; + { - const GraphViewer graph_viewer(graph); - capabilities = get_capabilities(current_ep, graph_viewer, kernel_lookup, params.resource_accountant, + std::unique_ptr sub_graph_holder; + auto graph_viewer = create_graph_viewer(sub_graph_holder); + + capabilities = get_capabilities(current_ep, *graph_viewer, kernel_lookup, params.resource_accountant, graph_optimizer_registry); + + reset_assignment_unclaimed_nodes(); + if (params.check_load_cancellation_fn()) { return ORT_MAKE_STATUS(ONNXRUNTIME, MODEL_LOAD_CANCELED, "Graph partitioning was canceled by user request"); @@ -244,9 +312,25 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l capabilities.clear(); - const GraphViewer graph_viewer(graph); - capabilities = get_capabilities(current_ep, graph_viewer, kernel_lookup, params.resource_accountant, + if (params.layering_index && end_node > first_new_node) { + // We need to update the LayeringIndex with newly created nodes + // as the layout transformation may have created new nodes + // with inherited annotations + InlinedVector new_node_indices; + for (NodeIndex idx = first_new_node; idx < end_node; ++idx) { + new_node_indices.push_back(idx); + } + params.layering_index->Update(graph, new_node_indices); + } + + std::unique_ptr sub_graph_holder; + auto graph_viewer = create_graph_viewer(sub_graph_holder); + + capabilities = get_capabilities(current_ep, *graph_viewer, kernel_lookup, params.resource_accountant, graph_optimizer_registry); + + reset_assignment_unclaimed_nodes(); + if (params.check_load_cancellation_fn()) { return ORT_MAKE_STATUS(ONNXRUNTIME, MODEL_LOAD_CANCELED, "GetCapabilities was canceled by user request"); @@ -1150,6 +1234,7 @@ static Status PartitionOrtFormatModelImpl(const PartitionParams& partition_param // TODO: Could avoid the topological sort in the GraphViewer ctor by constructing from an existing // GraphViewer instance instead of the Graph (copying the topological order instead of recalculating). auto viewer = std::make_unique(graph, indexed_sub_graph); + compilation_entries.push_back(CompilationEntry{std::move(viewer), fused_node, *capability}); #else // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) return ORT_MAKE_STATUS(ONNXRUNTIME, FAIL, "Compiling capabilities is not supported in this build."); From 1626d5c0630f3083ccca0edc51785235b6daec17 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Fri, 6 Feb 2026 16:00:14 -0800 Subject: [PATCH 08/57] Clear annotations after partitioning --- include/onnxruntime/core/graph/graph.h | 5 + .../onnxruntime_session_options_config_keys.h | 5 +- .../core/framework/layering_annotations.cc | 16 +- .../core/framework/layering_annotations.h | 8 +- .../core/framework/resource_accountant.cc | 150 ++++++++---------- onnxruntime/core/graph/graph.cc | 14 ++ onnxruntime/core/session/inference_session.cc | 10 ++ 7 files changed, 108 insertions(+), 100 deletions(-) diff --git a/include/onnxruntime/core/graph/graph.h b/include/onnxruntime/core/graph/graph.h index 9b931b8658ff4..8591e43cf4c77 100644 --- a/include/onnxruntime/core/graph/graph.h +++ b/include/onnxruntime/core/graph/graph.h @@ -1576,6 +1576,11 @@ class Graph { // NOLINT(clang-analyzer-optin.performance.Padding): preserve exi // compiled model during partitioning, leaving them unused in the ORT Graph. To allow the memory to be freed // we need to manually run the cleanup that would usually happen as part of Graph::Resolve. Status RemovedUnusedInitializersOrtFormat(); + + // This examines all the nodes and removes any annotations that are only used for layering. + // This potentially saves memory. + Status RemoveAllLayeringAnnotations(); + #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) // This friendship relationship should only be used to call Graph::Graph and diff --git a/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h b/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h index 6701c19084c12..d5374af3b8cb3 100644 --- a/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h +++ b/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h @@ -325,8 +325,11 @@ static const char* const kOrtSessionOptionsCollectNodeMemoryStatsToFile = "sessi /// This is a composite CSV setting formatted as "memory limit in kb,file name for collected stats" /// "limit > 0": enables Capacity Aware Partitioning for Cuda EP. `limit` is optional and when absent /// the provider may attempt to figure out the memory available automatically. +/// The setting with no pre-recorded stats is expected to look like: "limit > 0,". +/// In this case, the EP will calculate memory using the initializers referenced by the node. +/// This enables an ad-hoc and flexible scenarios with no pre-recorded stats, but may be less accurate. /// The setting with no limit is expected to look like: ",file name for collected stats" -/// The EP will place nodes on device "file name" : +/// The EP will place nodes on device "file name" (currently only CUDA is supported) : /// this file is expected to be found at the same folder with the model. The file contains /// pre-recorded stats collected when running with kOrtSessionOptionsCollectNodeMemoryStatsToFile enforce (see above) static const char* const kOrtSessionOptionsResourceCudaPartitioningSettings = diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc index bd2ab7429a8a1..9eb7788266040 100644 --- a/onnxruntime/core/framework/layering_annotations.cc +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -393,7 +393,7 @@ std::optional EpLayeringMatcher::Match(const ExecutionProviders& pr return std::nullopt; } -LayeringIndex LayeringIndex::Create(Graph& graph, +LayeringIndex LayeringIndex::Create(const Graph& graph, EpNameToLayeringIndices ep_map, LayeringIndexToEpName rule_map, LayeringRules layering_rules) { @@ -406,7 +406,7 @@ LayeringIndex LayeringIndex::Create(Graph& graph, return index; } -Status LayeringIndex::Create(Graph& graph, +Status LayeringIndex::Create(const Graph& graph, const std::string& config_string, gsl::span ep_devices, const ExecutionProviders& ep_providers, @@ -467,7 +467,7 @@ Status LayeringIndex::Create(Graph& graph, } // Process top to bottom-up assign layering indices to nodes -void LayeringIndex::ProcessGraph(Graph& graph, std::optional parent_layer_id) { +void LayeringIndex::ProcessGraph(const Graph& graph, std::optional parent_layer_id) { // 3. Create entry for this graph instance bool was_updated = false; std::optional new_index; @@ -489,8 +489,6 @@ void LayeringIndex::ProcessGraph(Graph& graph, std::optional parent_laye if (!annotation.empty()) { // If it has an annotation try to match it matched_rule_idx = matcher_.Match(annotation); - // Save memory and clear the annotation since it's no longer needed - node.ClearLayeringAnnotation(); } // 5. If node has no annotation, inherit from subgraph parent node @@ -517,7 +515,7 @@ void LayeringIndex::ProcessGraph(Graph& graph, std::optional parent_laye // Recurse for subgraphs if (node.ContainsSubgraph()) { const std::optional subgraph_parent_assignment = matched_rule_idx; - for (auto& [attr_name, subgraph] : node.GetMutableMapOfAttributeNameToSubgraph()) { + for (auto& [attr_name, subgraph] : node.GetAttributeNameToSubgraphMap()) { ProcessGraph(*subgraph, subgraph_parent_assignment); } } @@ -527,7 +525,7 @@ void LayeringIndex::ProcessGraph(Graph& graph, std::optional parent_laye } } -void LayeringIndex::Update(Graph& graph, gsl::span nodes) { +void LayeringIndex::Update(const Graph& graph, gsl::span nodes) { // Ensure we have an entry for this graph (creating it if it doesn't exist, though typically it should) bool was_updated = false; std::optional new_index; @@ -544,7 +542,7 @@ void LayeringIndex::Update(Graph& graph, gsl::span nodes) { for (NodeIndex node_index : nodes) { // GetMutableNode because we want to ClearLayeringAnnotation if we use it - Node* node = graph.GetNode(node_index); + const Node* node = graph.GetNode(node_index); if (!node) { continue; } @@ -552,8 +550,6 @@ void LayeringIndex::Update(Graph& graph, gsl::span nodes) { const std::string& annotation = node->GetLayeringAnnotation(); if (!annotation.empty()) { auto matched_rule_idx = matcher_.Match(annotation); - // Consume the annotation - node->ClearLayeringAnnotation(); if (matched_rule_idx) { const size_t rule_idx = *matched_rule_idx; diff --git a/onnxruntime/core/framework/layering_annotations.h b/onnxruntime/core/framework/layering_annotations.h index 58565b34e0256..fd5bebafeda0c 100644 --- a/onnxruntime/core/framework/layering_annotations.h +++ b/onnxruntime/core/framework/layering_annotations.h @@ -136,7 +136,7 @@ class LayeringIndex { /// Pre-populated mapping of EP names to their applicable rule indices. /// Pre-populated mapping of rule indices to EP names. /// Matcher to resolve node annotations to rule indices. - static LayeringIndex Create(Graph& graph, + static LayeringIndex Create(const Graph& graph, EpNameToLayeringIndices ep_map, LayeringIndexToEpName rule_map, LayeringRules layering_rules); @@ -153,7 +153,7 @@ class LayeringIndex { /// Output parameter for the created LayeringIndex. Returns no index if /// no valid layering rules discovered. /// Status indicating success or failure. - static Status Create(Graph& graph, + static Status Create(const Graph& graph, const std::string& config_string, gsl::span ep_devices, const ExecutionProviders& ep_providers, @@ -218,7 +218,7 @@ class LayeringIndex { /// /// The graph containing the nodes. /// Pixels of nodes to check and update. - void Update(Graph& graph, gsl::span nodes); + void Update(const Graph& graph, gsl::span nodes); private: LayeringRules rules_; @@ -257,7 +257,7 @@ class LayeringIndex { // Graph and sub-graphs mapping to their indices InlinedHashMap graph_index_; - void ProcessGraph(Graph& graph, std::optional parent_layer_id); + void ProcessGraph(const Graph& graph, std::optional parent_layer_id); }; } // namespace onnxruntime diff --git a/onnxruntime/core/framework/resource_accountant.cc b/onnxruntime/core/framework/resource_accountant.cc index ce9a9d4d17358..7b1b7ac7fc866 100644 --- a/onnxruntime/core/framework/resource_accountant.cc +++ b/onnxruntime/core/framework/resource_accountant.cc @@ -17,6 +17,7 @@ #include "core/session/onnxruntime_session_options_config_keys.h" #include +#include namespace onnxruntime { @@ -32,6 +33,8 @@ class SizeBasedStatsAccountant : public IResourceAccountant { SizeBasedStatsAccountant(size_t threshold, InlinedHashMap&& node_stats) : IResourceAccountant(threshold), node_stats_(std::move(node_stats)) {} + explicit SizeBasedStatsAccountant(size_t threshold) : IResourceAccountant(threshold) {} + explicit SizeBasedStatsAccountant(InlinedHashMap&& node_stats) : IResourceAccountant(), node_stats_(std::move(node_stats)) {} @@ -52,78 +55,48 @@ class SizeBasedStatsAccountant : public IResourceAccountant { ResourceCount ComputeResourceCount(const Node& node) override { const auto node_name = MakeUniqueNodeName(node); - auto hit = node_stats_.find(node_name); - if (hit != node_stats_.end()) { - const auto& stats = hit->second; - return stats.input_sizes + stats.initializers_sizes + - stats.total_dynamic_sizes + stats.total_temp_allocations; - } - return static_cast(0U); - } - - private: - size_t consumed_amount_ = 0; - InlinedHashMap node_stats_; -}; - -// Use this accountant if your resource can be counted with size_t type -// This accountant calculates the resource consumption based on node consumed -// weights since those are the biggest consumers. It prevents double accounting. -// The accountant is used for ad-hoc partitioning when runtime consumables are not -// known (see SizeBasedStatsAccountant above for recording and replaying consumption -// based on real runs) but we want to run out of the box on as many environments -// as possible. -class WeightsSizeBasedAccountant : public IResourceAccountant { - public: - WeightsSizeBasedAccountant() = default; - ~WeightsSizeBasedAccountant() = default; - - ResourceCount GetConsumedAmount() const noexcept override { - return consumed_amount_; - } - - void AddConsumedAmount(const ResourceCount& amount) noexcept override { - if (std::holds_alternative(amount)) { - consumed_amount_ += std::get(amount); - } - } - void RemoveConsumedAmount(const ResourceCount& amount) noexcept override { - if (std::holds_alternative(amount)) { - consumed_amount_ -= std::get<0>(amount); - } - } - - ResourceCount ComputeResourceCount(const Node& node) override { - const auto* graph = node.GetContainingGraph(); - if (!graph) return static_cast(0); - - size_t total_size = 0; - for (const auto* input_def : node.InputDefs()) { - if (!input_def->Exists()) continue; - - const auto& name = input_def->Name(); - constexpr bool check_outer_scope = true; - const auto* tensor_proto = graph->GetInitializer(name, check_outer_scope); - - if (tensor_proto) { - if (accounted_weights_.find(name) != accounted_weights_.end()) { - continue; - } - - size_t size = 0; - auto status = utils::GetSizeInBytesFromTensorProto<0>(*tensor_proto, &size); - - if (status.IsOK()) { - total_size += size; - accounted_weights_.insert(name); + if (node_stats_) { + auto hit = node_stats_->find(node_name); + if (hit != node_stats_->end()) { + const auto& stats = hit->second; + return stats.input_sizes + stats.initializers_sizes + + stats.total_dynamic_sizes + stats.total_temp_allocations; + } + return static_cast(0U); + } else { + const auto* graph = node.GetContainingGraph(); + if (!graph) return static_cast(0); + + size_t total_size = 0; + for (const auto* input_def : node.InputDefs()) { + if (!input_def->Exists()) continue; + + const auto& name = input_def->Name(); + constexpr bool check_outer_scope = true; + const auto* tensor_proto = graph->GetInitializer(name, check_outer_scope); + + if (tensor_proto) { + // Already accounted for this initializer in another node, skip to avoid double counting. + if (accounted_weights_.find(name) != accounted_weights_.end()) { + continue; + } + + size_t size = 0; + auto status = utils::GetSizeInBytesFromTensorProto<0>(*tensor_proto, &size); + + if (status.IsOK()) { + total_size += size; + accounted_weights_.insert(name); + } } } + return total_size; } - return total_size; } private: size_t consumed_amount_ = 0; + std::optional> node_stats_; InlinedHashSet accounted_weights_; }; @@ -232,39 +205,46 @@ Status CreateAccountants( if (!resource_partitioning_settings.empty()) { auto splits = utils::SplitString(resource_partitioning_settings, ",", true); if (splits.size() == 2) { - if (splits[1].empty()) { - return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid resource partitioning settings"); + if (splits[0].empty() && splits[1].empty()) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid value for: ", + kOrtSessionOptionsResourceCudaPartitioningSettings, + " : at least one of the fields should be provided"); } - InlinedHashMap loaded_stats; - ORT_RETURN_IF_ERROR(LoadNodeAllocationStats(model_path, splits[1], loaded_stats)); - auto& map = result.emplace(); + std::optional cuda_memory_limit; if (!splits[0].empty()) { - size_t cuda_memory_limit = 0; - ORT_RETURN_IF_ERROR(ParseStringWithClassicLocale(std::string{splits[0]}, cuda_memory_limit)); - cuda_memory_limit = SafeInt(cuda_memory_limit) * 1024; // to bytes + cuda_memory_limit.emplace(0U); + ORT_RETURN_IF_ERROR(ParseStringWithClassicLocale(std::string{splits[0]}, *cuda_memory_limit)); + cuda_memory_limit = SafeInt(*cuda_memory_limit) * 1024; // to bytes + } + + std::optional> loaded_stats; + if (splits[1].empty()) { + loaded_stats.emplace(); + ORT_RETURN_IF_ERROR(LoadNodeAllocationStats(model_path, splits[1], *loaded_stats)); + } + + if (cuda_memory_limit && loaded_stats) { map.insert_or_assign(kCudaExecutionProvider, - std::make_unique(cuda_memory_limit, - std::move(loaded_stats))); - } else { + std::make_unique(*cuda_memory_limit, + std::move(*loaded_stats))); + } else if (cuda_memory_limit) { + map.insert_or_assign(kCudaExecutionProvider, + std::make_unique(*cuda_memory_limit)); + } else if (loaded_stats) { map.insert_or_assign(kCudaExecutionProvider, - std::make_unique(std::move(loaded_stats))); + std::make_unique(std::move(*loaded_stats))); + } else { + ORT_THROW("Invalid value for: ", kOrtSessionOptionsResourceCudaPartitioningSettings, + " : at least one of the fields should be provided"); } } else { return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid format for: ", kOrtSessionOptionsResourceCudaPartitioningSettings, " : expecting comma separated fields"); } - } else { - const std::string layer_assignments = - config_options.GetConfigOrDefault(kOrtSessionOptionsLayerAssignmentSettings, ""); - if (!layer_assignments.empty()) { - auto& map = result.emplace(); - map.insert_or_assign(kCudaExecutionProvider, - std::make_unique()); - } } acc_map = std::move(result); diff --git a/onnxruntime/core/graph/graph.cc b/onnxruntime/core/graph/graph.cc index 60b2e1f86115e..5c2dc93a13fd5 100644 --- a/onnxruntime/core/graph/graph.cc +++ b/onnxruntime/core/graph/graph.cc @@ -3932,6 +3932,20 @@ Status Graph::RemovedUnusedInitializersOrtFormat() { auto result = ForThisAndAllSubgraphs(all_subgraphs, cleanup_func); return result; } + +Status Graph::RemoveAllLayeringAnnotations() { + std::vector all_subgraphs; + FindAllSubgraphs(all_subgraphs); + auto cleanup_func = [](Graph& graph) { + for (auto& node : graph.Nodes()) { + node.ClearLayeringAnnotation(); + } + return Status::OK(); + }; + + return ForThisAndAllSubgraphs(all_subgraphs, cleanup_func); +} + #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) const std::string& Graph::Name() const noexcept { diff --git a/onnxruntime/core/session/inference_session.cc b/onnxruntime/core/session/inference_session.cc index 08549161de313..321d4ef8f2483 100644 --- a/onnxruntime/core/session/inference_session.cc +++ b/onnxruntime/core/session/inference_session.cc @@ -1511,6 +1511,16 @@ common::Status InferenceSession::TransformGraph(onnxruntime::Graph& graph, bool session_options_.config_options, *session_logger_, layering_index, mode, session_options_.GetEpContextGenerationOptions(), debug_graph_fn)); +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + if (layering_index) { + // Layering annotations maybe present even if index is not built although unlikely. + ORT_RETURN_IF_ERROR_SESSIONID_(graph.RemoveAllLayeringAnnotations()); + // We are currently not using it beyond this point. Clear it to free up memory. + layering_index = nullptr; + layering_index_storage.reset(); + } +#endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + // Get graph optimizations loop level from session config, if not present, set to default value of 1 as per // the definition of kOrtSessionOptionsGraphOptimizationsLoopLevel. unsigned int graph_optimizations_loop_level = static_cast(std::stoi( From b3ecb39f68dd8c2f9146d0590b183fd4922a554c Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Fri, 6 Feb 2026 18:13:45 -0800 Subject: [PATCH 09/57] Address accountant bug --- onnxruntime/core/framework/resource_accountant.cc | 2 +- onnxruntime/test/framework/session_state_test.cc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/onnxruntime/core/framework/resource_accountant.cc b/onnxruntime/core/framework/resource_accountant.cc index 7b1b7ac7fc866..3a7c0bd2665c8 100644 --- a/onnxruntime/core/framework/resource_accountant.cc +++ b/onnxruntime/core/framework/resource_accountant.cc @@ -221,7 +221,7 @@ Status CreateAccountants( } std::optional> loaded_stats; - if (splits[1].empty()) { + if (!splits[1].empty()) { loaded_stats.emplace(); ORT_RETURN_IF_ERROR(LoadNodeAllocationStats(model_path, splits[1], *loaded_stats)); } diff --git a/onnxruntime/test/framework/session_state_test.cc b/onnxruntime/test/framework/session_state_test.cc index b8131de7f6050..16798b95ceb4d 100644 --- a/onnxruntime/test/framework/session_state_test.cc +++ b/onnxruntime/test/framework/session_state_test.cc @@ -510,7 +510,7 @@ TEST(SessionStateTest, TestResourceAwarePartitioning_CPUOffloaded) { constexpr const ORTCHAR_T* model_path = ORT_TSTR("testdata/transformers/tiny_gpt2_beamsearch.onnx"); constexpr const char* limit_setting = "5000,tiny_gpt2_beamsearch_node_stats.txt"; - // Large limit, all nodes are still assigned + // Limit is smaller, we expect some nodes to be offloaded to CPU. SessionOptions sess_options; sess_options.enable_mem_pattern = false; sess_options.execution_mode = ExecutionMode::ORT_SEQUENTIAL; From b5ea1c6e152207ebec3cfa13588b74e90a42566f Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Mon, 9 Feb 2026 12:48:20 -0800 Subject: [PATCH 10/57] Annotate tiny_gpt2_beamsearch by layers and add SessionState partitioning test for layered execution. Add layering configuration file for tiny_gpt2_beamsearch and a script to annotate the model by layers. --- .../python/tools/layering/layer_annotate.py | 206 ++++++++++++++++++ .../test/framework/session_state_test.cc | 36 ++- .../tiny_gpt2_beamsearch_layering.onnx | Bin 0 -> 580131 bytes .../tiny_gpt2_beamsearch_layering.txt | 55 +++++ 4 files changed, 293 insertions(+), 4 deletions(-) create mode 100644 onnxruntime/python/tools/layering/layer_annotate.py create mode 100644 onnxruntime/test/testdata/layering/tiny_gpt2_beamsearch_layering.onnx create mode 100644 onnxruntime/test/testdata/layering/tiny_gpt2_beamsearch_layering.txt diff --git a/onnxruntime/python/tools/layering/layer_annotate.py b/onnxruntime/python/tools/layering/layer_annotate.py new file mode 100644 index 0000000000000..cf5568cf9b466 --- /dev/null +++ b/onnxruntime/python/tools/layering/layer_annotate.py @@ -0,0 +1,206 @@ +import pathlib +import onnx +import logging +import argparse +import concurrent.futures +import os +import threading + +def get_logger(name, level=logging.DEBUG): + logging.basicConfig(format="%(asctime)s %(name)s [%(levelname)s] - %(message)s") + logger = logging.getLogger(name) + logger.setLevel(level) + return logger + +def getargs(): + argparser = argparse.ArgumentParser( + description="Read a config file with a list of node annotations and apply them to an ONNX model.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + argparser.add_argument( + "--config_file_path", + type=pathlib.Path, + required=True, + help="Path to the configuration file with node annotations.", + ) + argparser.add_argument( + "--model_path", + type=pathlib.Path, + required=True, + help="Path to a single model to process.", + ) + argparser.add_argument( + "--annotated_model", + type=pathlib.Path, + required=True, + help="Path to write the annotated model to.", + ) + + return argparser.parse_args() + +def read_annotation_config(config_file_path): + """ + Reads a configuration file to map substrings to annotations. + + The file format is expected to be: + annotation_string: substring1, substring2, ... + + The same annotation string can appear multiple times. + The node names in the configuration are treated as substrings. + + Args: + config_file_path (str or Path): Path to the configuration file. + + Returns: + list: A list of tuples (substring, annotation_string). + """ + substring_annotations = [] + with open(config_file_path, "r") as f: + for line in f: + line = line.strip() + if not line: + continue + parts = line.split(":", 1) + if len(parts) < 2: + continue + annotation = parts[0].strip() + substrings = parts[1].split(",") + for substring in substrings: + substring = substring.strip() + if substring: + substring_annotations.append((substring, annotation)) + return substring_annotations + +def process_nodes(nodes, substring_annotations): + """ + Helper function to process a list of nodes sequentially. + """ + logger = get_logger("annotate_model") + logger.info(f"Thread {threading.get_ident()} processing {len(nodes)} nodes.") + + for node in nodes: + matched_annotation = None + for substring, annotation in substring_annotations: + if substring in node.name: + matched_annotation = annotation + + if matched_annotation: + # Check if annotation already exists + entry = None + for prop in node.metadata_props: + if prop.key == 'layer_ann': + entry = prop + break + + if entry: + entry.value = matched_annotation + else: + entry = node.metadata_props.add() + entry.key = 'layer_ann' + entry.value = matched_annotation + + # Recurse into subgraphs for control flow nodes + for attr in node.attribute: + if attr.type == onnx.AttributeProto.GRAPH: + annotate_graph(attr.g, substring_annotations, parallel=False) + elif attr.type == onnx.AttributeProto.GRAPHS: + for sub_graph in attr.graphs: + annotate_graph(sub_graph, substring_annotations, parallel=False) + +def annotate_graph(graph, substring_annotations, parallel=False): + """ + Recursively applies annotations to nodes where a configured substring appears in the node name. + + This function iterates over all nodes in the given graph. It checks if any + substring from the configuration appears in the node's name. If matched, + it adds or updates a metadata property with key 'layer_ann' containing + the annotation string. If multiple substrings match, the last one defined + in the configuration list applies. + + It also handles control flow nodes (like 'If' or 'Loop') by recursively + processing their subgraphs (attributes of type GRAPH or GRAPHS). + + Args: + graph (onnx.GraphProto): The ONNX graph to process. + substring_annotations (list): A list of tuples (substring, annotation_string). + parallel (bool): If True, process the graph's nodes in parallel chunks. + """ + if parallel: + logger = get_logger("annotate_model") + num_cores = os.cpu_count() or 1 + nodes = graph.node + total_nodes = len(nodes) + min_nodes_per_thread = 1000 + + if total_nodes > 0: + # Ensure each thread processes at least min_nodes_per_thread, if possible + max_workers = max(1, total_nodes // min_nodes_per_thread) + num_workers = min(num_cores, max_workers) + + logger.info(f"Parallel processing configuration: Total Nodes={total_nodes}, Cores={num_cores}. " + f"Calculated Workers={num_workers} (Min nodes per thread={min_nodes_per_thread}).") + + chunks = [] + start_index = 0 + base_chunk_size = total_nodes // num_workers + remainder = total_nodes % num_workers + + for i in range(num_workers): + # Distribute the remainder (extra nodes) across the first 'remainder' threads + # To avoid the last worker processing very small amount of nodes + current_chunk_size = base_chunk_size + (1 if i < remainder else 0) + end_index = start_index + current_chunk_size + chunks.append(nodes[start_index:end_index]) + start_index = end_index + + # Use current thread for one of the chunks to avoid idle main thread + if num_workers > 1: + # Execute num_workers - 1 chunks in background threads + # Execute the last chunk in the current (main) thread + background_chunks = chunks[:-1] + main_chunk = chunks[-1] + + logger.info(f"Dispatching {len(background_chunks)} chunks to thread pool and 1 chunk to main thread.") + + with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers - 1) as executor: + futures = [executor.submit(process_nodes, chunk, substring_annotations) for chunk in background_chunks] + + # Run last chunk here + process_nodes(main_chunk, substring_annotations) + + concurrent.futures.wait(futures) + else: + # Only 1 worker needed, run in current thread + logger.info("Using single thread (current) for processing.") + process_nodes(chunks[0], substring_annotations) + else: + process_nodes(graph.node, substring_annotations) + +def annotate_model(model, substring_annotations): + """ + Annotates an ONNX model with metadata based on a provided mapping. + + This function serves as the entry point to annotate the model's graph. + It delegates the work to `annotate_graph` enabling parallel processing for the main graph. + + Args: + model (onnx.ModelProto): The ONNX model to annotate. + substring_annotations (list): A list of tuples (substring, annotation_string). + """ + annotate_graph(model.graph, substring_annotations, parallel=True) + +if __name__ == "__main__": + args = getargs() + logger = get_logger("annotate_model") + + # Read the mapping from the configuration file + substring_annotations = read_annotation_config(args.config_file_path) + + logger.info(f"Loading model from {args.model_path}") + onnx_model = onnx.load(args.model_path, load_external_data=False) + + logger.info(f"Applying annotations from {args.config_file_path}") + annotate_model(onnx_model, substring_annotations) + + logger.info(f"Saving annotated model to {args.annotated_model}") + onnx.save_model(onnx_model, args.annotated_model) \ No newline at end of file diff --git a/onnxruntime/test/framework/session_state_test.cc b/onnxruntime/test/framework/session_state_test.cc index 16798b95ceb4d..3cfd13c67c81a 100644 --- a/onnxruntime/test/framework/session_state_test.cc +++ b/onnxruntime/test/framework/session_state_test.cc @@ -532,6 +532,36 @@ TEST(SessionStateTest, TestResourceAwarePartitioning_CPUOffloaded) { }); } +TEST(SessionStateTest, TestLayeringPartitioning) { + constexpr const ORTCHAR_T* model_path = ORT_TSTR("testdata/layering/tiny_gpt2_beamsearch_layering.onnx"); + constexpr const char* layering_setting = + "cpu(Embed,Decode);gpu(GptAttention0,GptAttention1,GptAttention2,GptAttention3,GptAttention4)"; + + // Set the session options for layering + SessionOptions sess_options; + sess_options.enable_mem_pattern = false; + sess_options.execution_mode = ExecutionMode::ORT_SEQUENTIAL; + sess_options.use_deterministic_compute = false; + sess_options.enable_mem_reuse = false; + ASSERT_STATUS_OK(sess_options.config_options.AddConfigEntry( + kOrtSessionOptionsLayerAssignmentSettings, layering_setting)); + + LoadWithResourceAwarePartitioning(model_path, sess_options, [](const Graph& graph) { + const auto& graph_nodes = graph.Nodes(); + for (const auto& node : graph_nodes) { + const std::string& name = node.Name(); + const bool expected_on_cpu = (name.find("EmbedLayer") == 0) || (name == "LayerNorm_10") || (name == "MatMul_1165"); + + const std::string& ep = node.GetExecutionProviderType(); + if (expected_on_cpu) { + EXPECT_EQ(ep, kCpuExecutionProvider) << "Node " << name << " expected on CPU but found on " << ep; + } else { + EXPECT_EQ(ep, kCudaExecutionProvider) << "Node " << name << " expected on CUDA but found on " << ep; + } + } + }); +} + #endif // USE_CUDA INSTANTIATE_TEST_SUITE_P(SessionStateTests, SessionStateTestP, testing::ValuesIn(param_list)); @@ -912,9 +942,8 @@ TEST_F(SessionStateTestSharedInitalizersWithPrePacking, test2) { OrtMemoryInfo mem_info(CPU, OrtDeviceAllocator); std::vector float_data(1, 1); auto value = std::make_unique(); - Tensor::InitOrtValue(DataTypeImpl::GetType(), - TensorShape(std::vector{1}), reinterpret_cast(float_data.data()), - mem_info, *value); + Tensor::InitOrtValue(DataTypeImpl::GetType(), TensorShape(std::vector{1}), + float_data.data(), mem_info, *value); ASSERT_STATUS_OK(sess_options.AddInitializer("node_0_input_1", value.get())); @@ -1382,6 +1411,5 @@ INSTANTIATE_TEST_SUITE_P(SessionStateTests, PrepackingTestParam{true, false}, PrepackingTestParam{true, true})); #endif - } // namespace test } // namespace onnxruntime diff --git a/onnxruntime/test/testdata/layering/tiny_gpt2_beamsearch_layering.onnx b/onnxruntime/test/testdata/layering/tiny_gpt2_beamsearch_layering.onnx new file mode 100644 index 0000000000000000000000000000000000000000..57efb4ebe11a3d241d1395417068e8fc9819a9ce GIT binary patch literal 580131 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%0D$EJLN)|?b@`!kExdS8w&6bvjc0CnRR_i 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%0Dtr9D-r-cX)#le&2WcX#@gugVW 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 TqV$iC@%K^xc~?^@h4KFbrCZZA literal 0 HcmV?d00001 diff --git a/onnxruntime/test/testdata/layering/tiny_gpt2_beamsearch_layering.txt b/onnxruntime/test/testdata/layering/tiny_gpt2_beamsearch_layering.txt new file mode 100644 index 0000000000000..5affbde73e5b3 --- /dev/null +++ b/onnxruntime/test/testdata/layering/tiny_gpt2_beamsearch_layering.txt @@ -0,0 +1,55 @@ +Embed:EmbedLayer +GptAttention0:GptAttention_0 +GptAttention0:Add_295 +GptAttention0:LayerNorm_1 +GptAttention0:FullyConnect_MatMul_0 +GptAttention0:FastGelu_AddBias_0 +GptAttention0:FullyConnect_MatMul_1 +GptAttention0:FullyConnect_Add_1 +GptAttention0:Add_360 +GptAttention1:LayerNorm_2 +GptAttention1:GptAttention_1 +GptAttention1:Add_492 +GptAttention1:FullyConnect_MatMul_2 +GptAttention1:FastGelu_AddBias_1 +GptAttention1:FullyConnect_MatMul_3 +GptAttention1:FullyConnect_Add_3 +GptAttention1:Add_557 +GptAttention2:LayerNorm_4 +GptAttention2:GptAttention_2 +GptAttention2:Add_689 +GptAttention2:LayerNorm_5 +GptAttention2:FullyConnect_MatMul_4 +GptAttention2:FastGelu_AddBias_2 +GptAttention2:FullyConnect_MatMul_5 +GptAttention2:FullyConnect_Add_5 +GptAttention2:Add_754 +GptAttention3:LayerNorm_6 +GptAttention3:GptAttention_3 +GptAttention3:Add_886 +GptAttention3:LayerNorm_7 +GptAttention3:FullyConnect_MatMul_6 +GptAttention3:FastGelu_AddBias_3 +GptAttention3:FullyConnect_MatMul_7 +GptAttention3:FullyConnect_Add_7 +GptAttention3:Add_951 +GptAttention4:LayerNorm_8 +GptAttention4:GptAttention_4 +GptAttention4:Add_1083 +GptAttention4:LayerNorm_9 +GptAttention4:FullyConnect_MatMul_8 +GptAttention4:FastGelu_AddBias_4 +GptAttention4:FullyConnect_MatMul_9 +GptAttention4:FullyConnect_Add_9 +GptAttention4:Add_1148 +Decode:LayerNorm_10 +Decode:MatMul_1165 + + + + + + + + + From 9a422bae31f6f3d85bb265c843dd7af38cbb5d6b Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Mon, 9 Feb 2026 18:49:18 -0800 Subject: [PATCH 11/57] Refactor Graph_GetGraphView to make it a utility --- onnxruntime/core/session/onnxruntime_c_api.cc | 214 ++++++------------ .../python/tools/layering/layer_annotate.py | 50 ++-- .../test/framework/session_state_test.cc | 79 ++++--- 3 files changed, 146 insertions(+), 197 deletions(-) diff --git a/onnxruntime/core/session/onnxruntime_c_api.cc b/onnxruntime/core/session/onnxruntime_c_api.cc index 7a027c8eafb81..ed25d9f02b439 100644 --- a/onnxruntime/core/session/onnxruntime_c_api.cc +++ b/onnxruntime/core/session/onnxruntime_c_api.cc @@ -3033,6 +3033,20 @@ ORT_API_STATUS_IMPL(OrtApis::Graph_GetGraphView, _In_ const OrtGraph* src_graph, const GraphViewer& graph_viewer = ep_graph->GetGraphViewer(); const Graph& graph = graph_viewer.GetGraph(); + // Create subgraph's node set and convert them to internal Node + InlinedHashSet node_set; + InlinedVector internal_nodes; + internal_nodes.reserve(num_nodes); + for (size_t i = 0; i < num_nodes; i++) { + const EpNode* ep_node = EpNode::ToInternal(nodes[i]); + if (ep_node != nullptr) { + const Node& node = ep_node->GetInternalNode(); + node_set.insert(node.Index()); + internal_nodes.push_back(&node); + } + // else XXX is ignoring null nodes OK? + } + // Create a GraphViewer with filtered info // TODO: Investigate whether utils::MakeComputeCapability can be extended and reused instead std::unique_ptr indexed_sub_graph = std::make_unique(); @@ -3040,178 +3054,90 @@ ORT_API_STATUS_IMPL(OrtApis::Graph_GetGraphView, _In_ const OrtGraph* src_graph, // Following data structures help determine the final inputs/outputs of the subgraph. // Note: The 'subgraph' here refers to a graph contains a subset of nodes in the 'src_graph'. - // Subgraph's node set - const std::unordered_set node_set = [&]() { - std::unordered_set node_set; - for (size_t i = 0; i < num_nodes; i++) { - const OrtNode* ort_node = nodes[i]; - const EpNode* ep_node = EpNode::ToInternal(ort_node); - if (ep_node != nullptr) { - node_set.insert(ep_node->GetInternalNode().Index()); - } + // Pre-pass: Identify all outputs produced by nodes within the subgraph. + // This allows O(1) checks to determine if an input is internal or from the boundary. + InlinedHashSet internal_outputs; + for (size_t i = 0, lim = internal_nodes.size(); i < lim; i++) { + const auto& node = *internal_nodes[i]; + for (const auto& output : node.OutputDefs()) { + internal_outputs.insert(output); } - - return node_set; - }(); + } // Source graph output names - std::unordered_set graph_output_names; + InlinedHashSet graph_output_names; for (const auto* output_arg : graph_viewer.GetOutputs()) { graph_output_names.insert(output_arg->Name()); } // These maps store the inputs and outputs of the subgraph. - // Please note that the inputs and outputs of the maps will be dynamically updated during node iteration - // to determine the final inputs and outputs of the subgraph. - std::unordered_map subgraph_inputs, subgraph_outputs; - - // This map stores the node's output that will be consumed by another node outside of this subgraph. - // So the node's output should be put into the subgraph's output list. - std::unordered_map subgraph_outputs_to_add; + // Value is order index to maintain deterministic order. + InlinedHashMap subgraph_inputs, subgraph_outputs; - // This map stores the node's output that is original graph's output. - // So the node's output should be put into the subgraph's output list. - std::unordered_map graph_outputs_to_add; - - std::unordered_set erased; - - // This is the relative ordering that ensures node's input or output being added to the 'subgraph_inputs', - // 'subgraph_outputs', 'subgraph_outputs_to_add' and 'graph_outputs_to_add' maps is associated with a relative order index. - // Items added earlier receive a smaller order index than items added later. - // When constructing the final subgraph's input or output lists, entries with smaller - // order indices will appear before those with larger indices. int input_order = 0; int output_order = 0; - // node arg to its consumer nodes. - // Note: graph.GetConsumerNodes() is not available in minimal build, in order to use unified implementation across - // all builds, this map is needed to determine if node arg is consumed by other nodes. - std::unordered_map> node_arg_to_consumer_nodes; + InlinedVector initializers; - std::vector initializers; - - // Add nodes - for (size_t i = 0; i < num_nodes; i++) { - const OrtNode* ort_node = nodes[i]; - const EpNode* ep_node = EpNode::ToInternal(ort_node); - if (ep_node == nullptr) { - return OrtApis::CreateStatus(OrtErrorCode::ORT_INVALID_ARGUMENT, - "node is a ModelEditorNode which doesn't support Graph_GetGraphView."); - } - const Node& node = ep_node->GetInternalNode(); + // Add nodes and identify boundary inputs/outputs + for (size_t i = 0, lim = internal_nodes.size(); i < lim; i++) { + const auto& node = *internal_nodes[i]; indexed_sub_graph->nodes.push_back(node.Index()); - for (const auto& input : node.InputDefs()) { - if (!input->Exists()) { - continue; - } + // Process Inputs: If an input is not produced internally, it's a subgraph input. + auto process_inputs = [&](gsl::span inputs) { + for (const auto& input : inputs) { + if (!input->Exists()) continue; - if (graph_viewer.IsConstantInitializer(input->Name(), true)) { - initializers.push_back(input->Name()); - continue; - } - const auto& it = subgraph_outputs.find(input); - if (it != subgraph_outputs.end()) { - subgraph_outputs.erase(it); - erased.insert(input); - } else if (erased.find(input) == erased.end()) { - // Only when input is neither in output list nor erased list, add the input to input list - subgraph_inputs.insert({input, input_order++}); - } - } + if (graph_viewer.IsConstantInitializer(input->Name(), true)) { + initializers.push_back(input->Name()); + continue; + } - for (const auto& input : node.ImplicitInputDefs()) { - if (!input->Exists()) { - continue; + // If not produced by this subgraph, it's a boundary input + if (internal_outputs.count(input) == 0) { + // Use insert to keep the first occurrence's order + subgraph_inputs.emplace(input, input_order++); + } } + }; - if (graph_viewer.IsConstantInitializer(input->Name(), true)) { - initializers.push_back(input->Name()); - continue; - } - const auto& it = subgraph_outputs.find(input); - if (it != subgraph_outputs.end()) { - subgraph_outputs.erase(it); - erased.insert(input); - } else if (erased.find(input) == erased.end()) { - // Only when input is neither in output list nor erased list, add the input to input list - subgraph_inputs.insert({input, input_order++}); - } - } + process_inputs(gsl::make_span(node.InputDefs().data(), node.InputDefs().size())); + process_inputs(gsl::make_span(node.ImplicitInputDefs().data(), node.ImplicitInputDefs().size())); - // For output searching, there are two special cases, - // One is, if subgraph's node output is parent graph's output. the node output should - // be also added to the subgraph's output list - // The other one is, if node's OutputEdges are more than its outputs, meaning certain output is used more than once, - // if the output is connected to nodes that don't belong to the subgraph, the output need to be added - // to the output list + // Process Outputs: If an output is graph output OR consumed externally, it's a subgraph output. for (const auto& output : node.OutputDefs()) { - if (!output->Exists()) { - continue; - } + if (!output->Exists()) continue; + + bool is_boundary_output = false; - const auto& it = subgraph_inputs.find(output); - if (it != subgraph_inputs.end()) { - subgraph_inputs.erase(it); - erased.insert(output); - } else if (erased.find(output) == erased.end()) { - auto has_consumer_nodes = [&](const std::string& node_arg_str) -> bool { - // Same implementation as Graph::PopulateNodeArgToProducerConsumerLookupsFromNodes() - if (node_arg_to_consumer_nodes.empty()) { - for (const auto& node : graph.Nodes()) { - node.ForEachDef([&](const NodeArg& node_arg, bool is_input) { - if (is_input) { - node_arg_to_consumer_nodes[node_arg.Name()].insert(node.Index()); - } - }); + // 1. Is it a graph output? + if (graph_output_names.count(output->Name()) > 0) { + is_boundary_output = true; + } else { + // 2. Is it consumed by any node outside the subgraph? + for (auto it = node.OutputEdgesBegin(), end = node.OutputEdgesEnd(); it != end; ++it) { + // Check if the edge uses this specific output + if (it->GetSrcArgIndex() < static_cast(node.OutputDefs().size()) && + node.OutputDefs()[it->GetSrcArgIndex()] == output) { + if (node_set.count(it->GetNode().Index()) == 0) { + is_boundary_output = true; + break; } } - return node_arg_to_consumer_nodes.find(node_arg_str) != node_arg_to_consumer_nodes.end(); - }; - - if (has_consumer_nodes(output->Name())) { - // Only when output is neither in input list nor erased list, - // and the output is consumed by another node, add the output to output list - subgraph_outputs.insert({output, output_order++}); } } - if (graph_output_names.find(output->Name()) != graph_output_names.end()) { - // This output is the graph's output. - // So the output should be put into the subgraph's output list. - graph_outputs_to_add.insert({output, output_order++}); - } - } - - if (node.GetOutputEdgesCount() > node.OutputDefs().size()) { - for (auto it = node.OutputEdgesBegin(), end = node.OutputEdgesEnd(); it != end; ++it) { - const auto& node_idx = it->GetNode().Index(); - - if (node_set.find(node_idx) == node_set.end()) { - // This output will be consumed by another node outside of this subgraph. - // So the output should be put into the subgraph's output list. - const NodeArg* output = nullptr; - - // The dst_arg_index from GetDstArgIndex() could be the index for explicit/implicit input defs of the node. - // We need to get the correct input index accordingly. (See Graph::BuildConnections() in graph.cc for more details) - if (it->GetDstArgIndex() < static_cast(it->GetNode().InputDefs().size())) { - output = (it->GetNode()).InputDefs()[it->GetDstArgIndex()]; - } else { - output = (it->GetNode()).ImplicitInputDefs()[it->GetDstArgIndex() - it->GetNode().InputDefs().size()]; - } - subgraph_outputs_to_add.insert({output, output_order++}); - } + if (is_boundary_output) { + subgraph_outputs.insert({output, output_order++}); } } } - subgraph_outputs.insert(subgraph_outputs_to_add.begin(), subgraph_outputs_to_add.end()); - subgraph_outputs.insert(graph_outputs_to_add.begin(), graph_outputs_to_add.end()); - std::multimap inputs, outputs; // Get the input order of the original graph - std::unordered_map original_inputs; + InlinedHashMap original_inputs; int order = 0; for (const auto* input : graph_viewer.GetInputs()) { original_inputs[input] = order++; @@ -3222,19 +3148,19 @@ ORT_API_STATUS_IMPL(OrtApis::Graph_GetGraphView, _In_ const OrtGraph* src_graph, const auto& original_input_it = original_inputs.find(node_arg); if (original_input_it != original_inputs.end()) { - inputs.insert(std::make_pair( + inputs.emplace( original_input_it->second, // input order from original graph - node_arg)); + node_arg); } else { - inputs.insert(std::make_pair( + inputs.emplace( subgraph_input_order, // input order from subgraph - node_arg)); + node_arg); } } // Sort outputs by the order they were added - for (auto it = subgraph_outputs.begin(), end = subgraph_outputs.end(); it != end; ++it) { - outputs.insert(std::pair(it->second, it->first)); + for (const auto& [node_arg, subgraph_output_order] : subgraph_outputs) { + outputs.emplace(subgraph_output_order, node_arg); } std::unique_ptr meta_def = std::make_unique(); diff --git a/onnxruntime/python/tools/layering/layer_annotate.py b/onnxruntime/python/tools/layering/layer_annotate.py index cf5568cf9b466..310658e742bbe 100644 --- a/onnxruntime/python/tools/layering/layer_annotate.py +++ b/onnxruntime/python/tools/layering/layer_annotate.py @@ -1,17 +1,20 @@ -import pathlib -import onnx -import logging import argparse import concurrent.futures +import logging import os +import pathlib import threading +import onnx + + def get_logger(name, level=logging.DEBUG): logging.basicConfig(format="%(asctime)s %(name)s [%(levelname)s] - %(message)s") logger = logging.getLogger(name) logger.setLevel(level) return logger + def getargs(): argparser = argparse.ArgumentParser( description="Read a config file with a list of node annotations and apply them to an ONNX model.", @@ -38,6 +41,7 @@ def getargs(): return argparser.parse_args() + def read_annotation_config(config_file_path): """ Reads a configuration file to map substrings to annotations. @@ -55,7 +59,7 @@ def read_annotation_config(config_file_path): list: A list of tuples (substring, annotation_string). """ substring_annotations = [] - with open(config_file_path, "r") as f: + with open(config_file_path) as f: for line in f: line = line.strip() if not line: @@ -71,6 +75,7 @@ def read_annotation_config(config_file_path): substring_annotations.append((substring, annotation)) return substring_annotations + def process_nodes(nodes, substring_annotations): """ Helper function to process a list of nodes sequentially. @@ -83,20 +88,20 @@ def process_nodes(nodes, substring_annotations): for substring, annotation in substring_annotations: if substring in node.name: matched_annotation = annotation - + if matched_annotation: # Check if annotation already exists entry = None for prop in node.metadata_props: - if prop.key == 'layer_ann': + if prop.key == "layer_ann": entry = prop break - + if entry: entry.value = matched_annotation else: entry = node.metadata_props.add() - entry.key = 'layer_ann' + entry.key = "layer_ann" entry.value = matched_annotation # Recurse into subgraphs for control flow nodes @@ -107,17 +112,18 @@ def process_nodes(nodes, substring_annotations): for sub_graph in attr.graphs: annotate_graph(sub_graph, substring_annotations, parallel=False) + def annotate_graph(graph, substring_annotations, parallel=False): """ Recursively applies annotations to nodes where a configured substring appears in the node name. This function iterates over all nodes in the given graph. It checks if any substring from the configuration appears in the node's name. If matched, - it adds or updates a metadata property with key 'layer_ann' containing + it adds or updates a metadata property with key 'layer_ann' containing the annotation string. If multiple substrings match, the last one defined in the configuration list applies. - - It also handles control flow nodes (like 'If' or 'Loop') by recursively + + It also handles control flow nodes (like 'If' or 'Loop') by recursively processing their subgraphs (attributes of type GRAPH or GRAPHS). Args: @@ -137,8 +143,10 @@ def annotate_graph(graph, substring_annotations, parallel=False): max_workers = max(1, total_nodes // min_nodes_per_thread) num_workers = min(num_cores, max_workers) - logger.info(f"Parallel processing configuration: Total Nodes={total_nodes}, Cores={num_cores}. " - f"Calculated Workers={num_workers} (Min nodes per thread={min_nodes_per_thread}).") + logger.info( + f"Parallel processing configuration: Total Nodes={total_nodes}, Cores={num_cores}. " + f"Calculated Workers={num_workers} (Min nodes per thread={min_nodes_per_thread})." + ) chunks = [] start_index = 0 @@ -152,22 +160,24 @@ def annotate_graph(graph, substring_annotations, parallel=False): end_index = start_index + current_chunk_size chunks.append(nodes[start_index:end_index]) start_index = end_index - + # Use current thread for one of the chunks to avoid idle main thread if num_workers > 1: # Execute num_workers - 1 chunks in background threads # Execute the last chunk in the current (main) thread background_chunks = chunks[:-1] main_chunk = chunks[-1] - + logger.info(f"Dispatching {len(background_chunks)} chunks to thread pool and 1 chunk to main thread.") with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers - 1) as executor: - futures = [executor.submit(process_nodes, chunk, substring_annotations) for chunk in background_chunks] - + futures = [ + executor.submit(process_nodes, chunk, substring_annotations) for chunk in background_chunks + ] + # Run last chunk here process_nodes(main_chunk, substring_annotations) - + concurrent.futures.wait(futures) else: # Only 1 worker needed, run in current thread @@ -176,6 +186,7 @@ def annotate_graph(graph, substring_annotations, parallel=False): else: process_nodes(graph.node, substring_annotations) + def annotate_model(model, substring_annotations): """ Annotates an ONNX model with metadata based on a provided mapping. @@ -189,6 +200,7 @@ def annotate_model(model, substring_annotations): """ annotate_graph(model.graph, substring_annotations, parallel=True) + if __name__ == "__main__": args = getargs() logger = get_logger("annotate_model") @@ -203,4 +215,4 @@ def annotate_model(model, substring_annotations): annotate_model(onnx_model, substring_annotations) logger.info(f"Saving annotated model to {args.annotated_model}") - onnx.save_model(onnx_model, args.annotated_model) \ No newline at end of file + onnx.save_model(onnx_model, args.annotated_model) diff --git a/onnxruntime/test/framework/session_state_test.cc b/onnxruntime/test/framework/session_state_test.cc index 3cfd13c67c81a..ea458407fe020 100644 --- a/onnxruntime/test/framework/session_state_test.cc +++ b/onnxruntime/test/framework/session_state_test.cc @@ -418,7 +418,8 @@ using ParitionVerifierFn = std::function; void LoadWithResourceAwarePartitioning(const ORTCHAR_T* model_path, const SessionOptions& sess_options, - const ParitionVerifierFn& verifier_fn) { + const ParitionVerifierFn& verifier_fn, + const std::string& layering_config = std::string()) { const auto& log_manager = DefaultLoggingManager(); log_manager.SetDefaultLoggerSeverity(onnxruntime::logging::Severity::kVERBOSE); const auto& default_logger = log_manager.DefaultLogger(); @@ -433,9 +434,12 @@ void LoadWithResourceAwarePartitioning(const ORTCHAR_T* model_path, auto tp = concurrency::CreateThreadPool(&onnxruntime::Env::Default(), to, concurrency::ThreadPoolType::INTRA_OP); ExecutionProviders execution_providers; - auto tmp_cpu_execution_provider = DefaultCudaExecutionProvider(); - tmp_cpu_execution_provider->SetLogger(&default_logger); - ASSERT_STATUS_OK(execution_providers.Add(kCudaExecutionProvider, std::move(tmp_cpu_execution_provider))); + auto tmp_execution_provider = DefaultCudaExecutionProvider(); + tmp_execution_provider->SetLogger(&default_logger); + ASSERT_STATUS_OK(execution_providers.Add(kCudaExecutionProvider, std::move(tmp_execution_provider))); + tmp_execution_provider = DefaultCpuExecutionProvider(); + tmp_execution_provider->SetLogger(&default_logger); + ASSERT_STATUS_OK(execution_providers.Add(kCpuExecutionProvider, std::move(tmp_execution_provider))); KernelRegistryManager krm; ASSERT_STATUS_OK(krm.RegisterKernels(execution_providers)); @@ -447,6 +451,14 @@ void LoadWithResourceAwarePartitioning(const ORTCHAR_T* model_path, SessionState session_state(model->MainGraph(), execution_providers, tp.get(), nullptr, dtm, edlm, default_logger, profiler, sess_options); + LayeringIndex* layering_index = nullptr; + static std::optional layering_index_storage; + if (!layering_config.empty()) { + ASSERT_STATUS_OK(LayeringIndex::Create(graph, layering_config, {}, execution_providers, + default_logger, layering_index_storage)); + layering_index = &layering_index_storage.value(); + } + // Create GraphOptimizerRegistry instance for providing predefined graph optimizers and selection functions for EPs to lookup auto graph_optimizer_registry = std::make_unique(&sess_options, execution_providers.Get(onnxruntime::kCpuExecutionProvider), @@ -457,7 +469,7 @@ void LoadWithResourceAwarePartitioning(const ORTCHAR_T* model_path, layout_transformation::DebugGraphFn debug_graph_fn; ASSERT_STATUS_OK( partitioner.Partition(graph, session_state.GetMutableFuncMgr(), transform_layout_fn, - sess_options.config_options, default_logger, nullptr /*layering_index*/, + sess_options.config_options, default_logger, layering_index, GraphPartitioner::Mode::kNormal, epctx::ModelGenOptions{}, debug_graph_fn)); @@ -532,35 +544,34 @@ TEST(SessionStateTest, TestResourceAwarePartitioning_CPUOffloaded) { }); } -TEST(SessionStateTest, TestLayeringPartitioning) { - constexpr const ORTCHAR_T* model_path = ORT_TSTR("testdata/layering/tiny_gpt2_beamsearch_layering.onnx"); - constexpr const char* layering_setting = - "cpu(Embed,Decode);gpu(GptAttention0,GptAttention1,GptAttention2,GptAttention3,GptAttention4)"; - - // Set the session options for layering - SessionOptions sess_options; - sess_options.enable_mem_pattern = false; - sess_options.execution_mode = ExecutionMode::ORT_SEQUENTIAL; - sess_options.use_deterministic_compute = false; - sess_options.enable_mem_reuse = false; - ASSERT_STATUS_OK(sess_options.config_options.AddConfigEntry( - kOrtSessionOptionsLayerAssignmentSettings, layering_setting)); - - LoadWithResourceAwarePartitioning(model_path, sess_options, [](const Graph& graph) { - const auto& graph_nodes = graph.Nodes(); - for (const auto& node : graph_nodes) { - const std::string& name = node.Name(); - const bool expected_on_cpu = (name.find("EmbedLayer") == 0) || (name == "LayerNorm_10") || (name == "MatMul_1165"); - - const std::string& ep = node.GetExecutionProviderType(); - if (expected_on_cpu) { - EXPECT_EQ(ep, kCpuExecutionProvider) << "Node " << name << " expected on CPU but found on " << ep; - } else { - EXPECT_EQ(ep, kCudaExecutionProvider) << "Node " << name << " expected on CUDA but found on " << ep; - } - } - }); -} +// TEST(SessionStateTest, TestLayeringPartitioning) { +// constexpr const ORTCHAR_T* model_path = ORT_TSTR("testdata/layering/tiny_gpt2_beamsearch_layering.onnx"); +// constexpr const char* layering_setting = +// "cpu(Embed,Decode);gpu(GptAttention0,GptAttention1,GptAttention2,GptAttention3,GptAttention4)"; +// +// // Set the session options for layering +// SessionOptions sess_options; +// sess_options.enable_mem_pattern = false; +// sess_options.execution_mode = ExecutionMode::ORT_SEQUENTIAL; +// sess_options.use_deterministic_compute = false; +// sess_options.enable_mem_reuse = false; +// ASSERT_STATUS_OK(sess_options.config_options.AddConfigEntry( +// kOrtSessionOptionsLayerAssignmentSettings, layering_setting)); +// +// LoadWithResourceAwarePartitioning(model_path, sess_options, [](const Graph& graph) { +// const auto& graph_nodes = graph.Nodes(); +// for (const auto& node : graph_nodes) { +// const std::string& name = node.Name(); +// const bool expected_on_cpu = (name.find("EmbedLayer") == 0) || (name == "LayerNorm_10") || (name == "MatMul_1165"); +// +// const std::string& ep = node.GetExecutionProviderType(); +// if (expected_on_cpu) { +// EXPECT_EQ(ep, kCpuExecutionProvider) << "Node " << name << " expected on CPU but found on " << ep; +// } else { +// EXPECT_EQ(ep, kCudaExecutionProvider) << "Node " << name << " expected on CUDA but found on " << ep; +// } +// } }, layering_setting); +// } #endif // USE_CUDA From a1caf934c3d7ce9d7a532b72baf0f7316f44085d Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Tue, 10 Feb 2026 14:13:31 -0800 Subject: [PATCH 12/57] Introduce a graph utility to create an IndexedSubgraph instance based on a set of nodes. This is used by the graph partitioner to create a filtered graph viewer. Adjust implementation of the Graph_GetViewer. --- .../core/framework/graph_partitioner.cc | 33 ++-- onnxruntime/core/graph/graph_utils.cc | 145 ++++++++++++++++++ onnxruntime/core/graph/graph_utils.h | 12 ++ onnxruntime/core/session/onnxruntime_c_api.cc | 6 +- .../test/framework/session_state_test.cc | 56 +++---- 5 files changed, 212 insertions(+), 40 deletions(-) diff --git a/onnxruntime/core/framework/graph_partitioner.cc b/onnxruntime/core/framework/graph_partitioner.cc index 6456f5ac3914a..49a7642a3a0ef 100644 --- a/onnxruntime/core/framework/graph_partitioner.cc +++ b/onnxruntime/core/framework/graph_partitioner.cc @@ -196,15 +196,23 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l auto& capabilities = params.capabilities.get(); const auto& graph_optimizer_registry = params.graph_optimizer_registry.get(); +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) InlinedVector assigned_filteredin_nodes; + InlinedVector filteredin_nodes; +#endif // Helper to create a GraphViewer that filters nodes based on layering_index if present. - auto create_graph_viewer = [&](std::unique_ptr& sub_graph_holder) -> std::unique_ptr { + auto create_graph_viewer = [&](std::unique_ptr& sub_graph_holder, + std::unique_ptr& viewer) -> Status { #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) if (params.layering_index) { - sub_graph_holder = std::make_unique(); - sub_graph_holder->nodes.reserve(graph.NumberOfNodes()); + assigned_filteredin_nodes.clear(); + filteredin_nodes.clear(); + filteredin_nodes.reserve(graph.NumberOfNodes()); auto rules_opt = params.layering_index->GetLayeringRulesForThisEp(ep_type); + if (rules_opt) { + assigned_filteredin_nodes.reserve(rules_opt->get().size()); + } for (auto& node : graph.Nodes()) { auto rule_idx_opt = params.layering_index->GetNodeAssignment(graph, node.Index()); @@ -220,16 +228,19 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l // If node has no assignment, it is included (available to any EP) if (include) { - sub_graph_holder->nodes.push_back(node.Index()); + filteredin_nodes.push_back(&node); } } - return std::make_unique(graph, *sub_graph_holder); + ORT_RETURN_IF_ERROR(graph_utils::CreateFilteredIndexedGraph(filteredin_nodes, graph, sub_graph_holder)); + viewer = std::make_unique(graph, *sub_graph_holder); + return Status::OK(); } #endif - return std::make_unique(graph); + viewer = std::make_unique(graph); + return Status::OK(); }; - // Helper to unassign nodes that were assigned to this EP but not claimed by updated capabilities. + // Helper to un-assign nodes that were assigned to this EP but not claimed by updated capabilities. auto reset_assignment_unclaimed_nodes = [&]() { #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) if (params.layering_index) { @@ -261,7 +272,8 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l { std::unique_ptr sub_graph_holder; - auto graph_viewer = create_graph_viewer(sub_graph_holder); + std::unique_ptr graph_viewer; + ORT_RETURN_IF_ERROR(create_graph_viewer(sub_graph_holder, graph_viewer)); capabilities = get_capabilities(current_ep, *graph_viewer, kernel_lookup, params.resource_accountant, graph_optimizer_registry); @@ -312,6 +324,7 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l capabilities.clear(); +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) if (params.layering_index && end_node > first_new_node) { // We need to update the LayeringIndex with newly created nodes // as the layout transformation may have created new nodes @@ -322,9 +335,11 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l } params.layering_index->Update(graph, new_node_indices); } +#endif std::unique_ptr sub_graph_holder; - auto graph_viewer = create_graph_viewer(sub_graph_holder); + std::unique_ptr graph_viewer; + ORT_RETURN_IF_ERROR(create_graph_viewer(sub_graph_holder, graph_viewer)); capabilities = get_capabilities(current_ep, *graph_viewer, kernel_lookup, params.resource_accountant, graph_optimizer_registry); diff --git a/onnxruntime/core/graph/graph_utils.cc b/onnxruntime/core/graph/graph_utils.cc index 0480263befdd1..a6cdda7da5e91 100644 --- a/onnxruntime/core/graph/graph_utils.cc +++ b/onnxruntime/core/graph/graph_utils.cc @@ -1009,6 +1009,151 @@ NodeArg& CreateNodeArg(Graph& graph, const NodeArg& base_arg) { return graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(base_arg.Name()), base_arg.TypeAsProto()); } +Status CreateFilteredIndexedGraph(gsl::span nodes, const Graph& graph, + std::unique_ptr& result) { + // Following data structures help determine the final inputs/outputs of the subgraph. + // Note: The 'subgraph' here refers to a graph contains a subset of nodes in the 'src_graph'. + + // Pre-pass: Identify all outputs produced by nodes within the subgraph. + // This allows O(1) checks to determine if an input is internal or from the boundary. + InlinedHashSet node_set; + InlinedHashSet internal_outputs; + for (size_t i = 0, lim = nodes.size(); i < lim; i++) { + const auto& node = *nodes[i]; + node_set.insert(node.Index()); + for (const auto& output : node.OutputDefs()) { + internal_outputs.insert(output); + } + } + + // Source graph output names + InlinedHashSet graph_output_names; + for (const auto* output_arg : graph.GetOutputs()) { + graph_output_names.insert(output_arg->Name()); + } + + // These maps store the inputs and outputs of the subgraph. + // Value is order index to maintain deterministic order. + InlinedHashMap subgraph_inputs, subgraph_outputs; + + int input_order = 0; + int output_order = 0; + + std::unique_ptr indexed_sub_graph = std::make_unique(); + InlinedVector initializers; + + // Add nodes and identify boundary inputs/outputs + for (size_t i = 0, lim = nodes.size(); i < lim; i++) { + const auto& node = *nodes[i]; + indexed_sub_graph->nodes.push_back(node.Index()); + + // Process Inputs: If an input is not produced internally, it's a subgraph input. + auto process_inputs = [&](gsl::span inputs) { + for (const auto& input : inputs) { + if (!input->Exists()) continue; + + const auto* tensor_proto = graph.GetConstantInitializer(input->Name(), true); + if (tensor_proto != nullptr) { + initializers.push_back(input->Name()); + continue; + } + + // If not produced by this subgraph, it's a boundary input + if (internal_outputs.count(input) == 0) { + // Use insert to keep the first occurrence's order + subgraph_inputs.emplace(input, input_order++); + } + } + }; + + process_inputs(gsl::make_span(node.InputDefs().data(), node.InputDefs().size())); + process_inputs(gsl::make_span(node.ImplicitInputDefs().data(), node.ImplicitInputDefs().size())); + + // Process Outputs: If an output is graph output OR consumed externally, it's a subgraph output. + for (const auto& output : node.OutputDefs()) { + if (!output->Exists()) continue; + + bool is_boundary_output = false; + + // 1. Is it a graph output? + if (graph_output_names.count(output->Name()) > 0) { + is_boundary_output = true; + } else { + // 2. Is it consumed by any node outside the subgraph? + for (auto it = node.OutputEdgesBegin(), end = node.OutputEdgesEnd(); it != end; ++it) { + // Check if the edge uses this specific output + if (it->GetSrcArgIndex() < static_cast(node.OutputDefs().size()) && + node.OutputDefs()[it->GetSrcArgIndex()] == output) { + if (node_set.count(it->GetNode().Index()) == 0) { + is_boundary_output = true; + break; + } + } + } + } + + if (is_boundary_output) { + subgraph_outputs.insert({output, output_order++}); + } + } + } + + std::multimap inputs, outputs; + + // Get the input order of the original graph + InlinedHashMap original_inputs; + int order = 0; + for (const auto* input : graph.GetInputs()) { + original_inputs[input] = order++; + } + + // input order needs to be consistent with original graph's input order + for (const auto& [node_arg, subgraph_input_order] : subgraph_inputs) { + const auto original_input_it = original_inputs.find(node_arg); + + if (original_input_it != original_inputs.end()) { + inputs.emplace( + original_input_it->second, // input order from original graph + node_arg); + } else { + inputs.emplace( + subgraph_input_order, // input order from subgraph + node_arg); + } + } + + // Sort outputs by the order they were added + for (const auto& [node_arg, subgraph_output_order] : subgraph_outputs) { + outputs.emplace(subgraph_output_order, node_arg); + } + + std::unique_ptr meta_def = std::make_unique(); + meta_def->name = "sub_graph"; + meta_def->since_version = 1; + + // Assign inputs and outputs to subgraph's meta_def + for (const auto& input : inputs) { + if (input.second->Exists()) { + meta_def->inputs.push_back(input.second->Name()); + } + } + + for (const auto& initializer : initializers) { + meta_def->constant_initializers.push_back(initializer); + } + + for (const auto& output : outputs) { + if (output.second->Exists()) { + meta_def->outputs.push_back(output.second->Name()); + } + } + + indexed_sub_graph->SetMetaDef(std::move(meta_def)); + result = std::move(indexed_sub_graph); + + return Status::OK(); +} + #endif // !defined(ORT_MINIMAL_BUILD) } // namespace graph_utils diff --git a/onnxruntime/core/graph/graph_utils.h b/onnxruntime/core/graph/graph_utils.h index 256a6fc81495d..3ca32bdff2778 100644 --- a/onnxruntime/core/graph/graph_utils.h +++ b/onnxruntime/core/graph/graph_utils.h @@ -473,6 +473,18 @@ bool RemoveNodesWithOneOutputBottomUp(Graph& graph, const Node& node); */ NodeArg& CreateNodeArg(Graph& graph, const NodeArg& base_arg); +///

+/// This function creates an indexed subgraph from a collection of nodes +/// using the graph instance. The IndexedSubgraph can then we used to create +/// a filtered GraphViewer instance that only contains the nodes in the collection. +/// +/// +/// +/// +/// +Status CreateFilteredIndexedGraph(gsl::span nodes, const Graph& graph, + std::unique_ptr& indexed_subgraph); + #endif // !defined(ORT_MINIMAL_BUILD) } // namespace graph_utils diff --git a/onnxruntime/core/session/onnxruntime_c_api.cc b/onnxruntime/core/session/onnxruntime_c_api.cc index ed25d9f02b439..237afa782690c 100644 --- a/onnxruntime/core/session/onnxruntime_c_api.cc +++ b/onnxruntime/core/session/onnxruntime_c_api.cc @@ -3031,7 +3031,6 @@ ORT_API_STATUS_IMPL(OrtApis::Graph_GetGraphView, _In_ const OrtGraph* src_graph, "src_graph is a ModelEditorGraph which doesn't support Graph_GetGraphView."); } const GraphViewer& graph_viewer = ep_graph->GetGraphViewer(); - const Graph& graph = graph_viewer.GetGraph(); // Create subgraph's node set and convert them to internal Node InlinedHashSet node_set; @@ -3145,7 +3144,7 @@ ORT_API_STATUS_IMPL(OrtApis::Graph_GetGraphView, _In_ const OrtGraph* src_graph, // input order needs to be consistent with original graph's input order for (const auto& [node_arg, subgraph_input_order] : subgraph_inputs) { - const auto& original_input_it = original_inputs.find(node_arg); + const auto original_input_it = original_inputs.find(node_arg); if (original_input_it != original_inputs.end()) { inputs.emplace( @@ -3185,7 +3184,8 @@ ORT_API_STATUS_IMPL(OrtApis::Graph_GetGraphView, _In_ const OrtGraph* src_graph, } indexed_sub_graph->SetMetaDef(std::move(meta_def)); - auto new_graph_viewer = std::make_unique(graph, *indexed_sub_graph.get()); + const Graph& graph = graph_viewer.GetGraph(); + auto new_graph_viewer = std::make_unique(graph, *indexed_sub_graph); std::unique_ptr result; ORT_API_RETURN_IF_STATUS_NOT_OK(EpGraph::Create(std::move(new_graph_viewer), std::move(indexed_sub_graph), result)); diff --git a/onnxruntime/test/framework/session_state_test.cc b/onnxruntime/test/framework/session_state_test.cc index ea458407fe020..3e566952f5dc5 100644 --- a/onnxruntime/test/framework/session_state_test.cc +++ b/onnxruntime/test/framework/session_state_test.cc @@ -544,34 +544,34 @@ TEST(SessionStateTest, TestResourceAwarePartitioning_CPUOffloaded) { }); } -// TEST(SessionStateTest, TestLayeringPartitioning) { -// constexpr const ORTCHAR_T* model_path = ORT_TSTR("testdata/layering/tiny_gpt2_beamsearch_layering.onnx"); -// constexpr const char* layering_setting = -// "cpu(Embed,Decode);gpu(GptAttention0,GptAttention1,GptAttention2,GptAttention3,GptAttention4)"; -// -// // Set the session options for layering -// SessionOptions sess_options; -// sess_options.enable_mem_pattern = false; -// sess_options.execution_mode = ExecutionMode::ORT_SEQUENTIAL; -// sess_options.use_deterministic_compute = false; -// sess_options.enable_mem_reuse = false; -// ASSERT_STATUS_OK(sess_options.config_options.AddConfigEntry( -// kOrtSessionOptionsLayerAssignmentSettings, layering_setting)); -// -// LoadWithResourceAwarePartitioning(model_path, sess_options, [](const Graph& graph) { -// const auto& graph_nodes = graph.Nodes(); -// for (const auto& node : graph_nodes) { -// const std::string& name = node.Name(); -// const bool expected_on_cpu = (name.find("EmbedLayer") == 0) || (name == "LayerNorm_10") || (name == "MatMul_1165"); -// -// const std::string& ep = node.GetExecutionProviderType(); -// if (expected_on_cpu) { -// EXPECT_EQ(ep, kCpuExecutionProvider) << "Node " << name << " expected on CPU but found on " << ep; -// } else { -// EXPECT_EQ(ep, kCudaExecutionProvider) << "Node " << name << " expected on CUDA but found on " << ep; -// } -// } }, layering_setting); -// } +TEST(SessionStateTest, TestLayeringPartitioning) { + constexpr const ORTCHAR_T* model_path = ORT_TSTR("testdata/layering/tiny_gpt2_beamsearch_layering.onnx"); + constexpr const char* layering_setting = + "cpu(Embed,Decode);gpu(GptAttention0,GptAttention1,GptAttention2,GptAttention3,GptAttention4)"; + + // Set the session options for layering + SessionOptions sess_options; + sess_options.enable_mem_pattern = false; + sess_options.execution_mode = ExecutionMode::ORT_SEQUENTIAL; + sess_options.use_deterministic_compute = false; + sess_options.enable_mem_reuse = false; + ASSERT_STATUS_OK(sess_options.config_options.AddConfigEntry( + kOrtSessionOptionsLayerAssignmentSettings, layering_setting)); + + LoadWithResourceAwarePartitioning(model_path, sess_options, [](const Graph& graph) { + const auto& graph_nodes = graph.Nodes(); + for (const auto& node : graph_nodes) { + const std::string& name = node.Name(); + const bool expected_on_cpu = (name.find("EmbedLayer") == 0) || (name == "LayerNorm_10") || (name == "MatMul_1165"); + + const std::string& ep = node.GetExecutionProviderType(); + if (expected_on_cpu) { + EXPECT_EQ(ep, kCpuExecutionProvider) << "Node " << name << " expected on CPU but found on " << ep; + } else { + EXPECT_EQ(ep, kCudaExecutionProvider) << "Node " << name << " expected on CUDA but found on " << ep; + } + } }, layering_setting); +} #endif // USE_CUDA From acec4024109f7e002933a611b32ef8b36d078f5c Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Wed, 11 Feb 2026 14:12:40 -0800 Subject: [PATCH 13/57] Fix lint in python script. Add a no-threashold and no-stat option for the accountant. --- .../onnxruntime_session_options_config_keys.h | 1 + .../core/framework/resource_accountant.cc | 17 ++++++----------- .../python/tools/layering/layer_annotate.py | 8 ++++---- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h b/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h index d5374af3b8cb3..2261de511b88a 100644 --- a/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h +++ b/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h @@ -329,6 +329,7 @@ static const char* const kOrtSessionOptionsCollectNodeMemoryStatsToFile = "sessi /// In this case, the EP will calculate memory using the initializers referenced by the node. /// This enables an ad-hoc and flexible scenarios with no pre-recorded stats, but may be less accurate. /// The setting with no limit is expected to look like: ",file name for collected stats" +/// Finally a setting with both limit and pre-recorded stats absent can contain a single comma: ",". /// The EP will place nodes on device "file name" (currently only CUDA is supported) : /// this file is expected to be found at the same folder with the model. The file contains /// pre-recorded stats collected when running with kOrtSessionOptionsCollectNodeMemoryStatsToFile enforce (see above) diff --git a/onnxruntime/core/framework/resource_accountant.cc b/onnxruntime/core/framework/resource_accountant.cc index 3a7c0bd2665c8..9c83e9ffac6ae 100644 --- a/onnxruntime/core/framework/resource_accountant.cc +++ b/onnxruntime/core/framework/resource_accountant.cc @@ -54,8 +54,8 @@ class SizeBasedStatsAccountant : public IResourceAccountant { } ResourceCount ComputeResourceCount(const Node& node) override { - const auto node_name = MakeUniqueNodeName(node); if (node_stats_) { + const auto node_name = MakeUniqueNodeName(node); auto hit = node_stats_->find(node_name); if (hit != node_stats_->end()) { const auto& stats = hit->second; @@ -67,7 +67,9 @@ class SizeBasedStatsAccountant : public IResourceAccountant { const auto* graph = node.GetContainingGraph(); if (!graph) return static_cast(0); - size_t total_size = 0; + /// XXX: Consider accounting for intermediate tensors as well + /// if they have shape. + SafeInt total_size = 0; for (const auto* input_def : node.InputDefs()) { if (!input_def->Exists()) continue; @@ -90,7 +92,7 @@ class SizeBasedStatsAccountant : public IResourceAccountant { } } } - return total_size; + return static_cast(total_size); } } @@ -205,12 +207,6 @@ Status CreateAccountants( if (!resource_partitioning_settings.empty()) { auto splits = utils::SplitString(resource_partitioning_settings, ",", true); if (splits.size() == 2) { - if (splits[0].empty() && splits[1].empty()) { - return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid value for: ", - kOrtSessionOptionsResourceCudaPartitioningSettings, - " : at least one of the fields should be provided"); - } - auto& map = result.emplace(); std::optional cuda_memory_limit; @@ -237,8 +233,7 @@ Status CreateAccountants( map.insert_or_assign(kCudaExecutionProvider, std::make_unique(std::move(*loaded_stats))); } else { - ORT_THROW("Invalid value for: ", kOrtSessionOptionsResourceCudaPartitioningSettings, - " : at least one of the fields should be provided"); + map.insert_or_assign(kCudaExecutionProvider, std::make_unique()); } } else { return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, "Invalid format for: ", diff --git a/onnxruntime/python/tools/layering/layer_annotate.py b/onnxruntime/python/tools/layering/layer_annotate.py index 310658e742bbe..7ae32dbffb8b8 100644 --- a/onnxruntime/python/tools/layering/layer_annotate.py +++ b/onnxruntime/python/tools/layering/layer_annotate.py @@ -60,8 +60,8 @@ def read_annotation_config(config_file_path): """ substring_annotations = [] with open(config_file_path) as f: - for line in f: - line = line.strip() + for unstripped_line in f: + line = unstripped_line.strip() if not line: continue parts = line.split(":", 1) @@ -69,8 +69,8 @@ def read_annotation_config(config_file_path): continue annotation = parts[0].strip() substrings = parts[1].split(",") - for substring in substrings: - substring = substring.strip() + for substr in substrings: + substring = substr.strip() if substring: substring_annotations.append((substring, annotation)) return substring_annotations From e445b6083450a3781966ef24f069108c5ed9cd12 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Mon, 9 Mar 2026 16:29:51 -0700 Subject: [PATCH 14/57] Fix build errors and address Copilt comments --- include/onnxruntime/core/graph/graph.h | 12 +- .../onnxruntime_session_options_config_keys.h | 2 +- .../core/framework/graph_partitioner.cc | 25 +- .../core/framework/layering_annotations.cc | 39 ++- .../core/framework/layering_annotations.h | 59 ++-- onnxruntime/core/graph/graph_utils.cc | 291 +++++++++--------- onnxruntime/core/graph/graph_utils.h | 6 +- onnxruntime/core/session/onnxruntime_c_api.cc | 5 +- .../framework/layering_annotations_test.cc | 4 +- .../test/framework/session_state_test.cc | 2 +- .../test/framework/tensorutils_test.cc | 2 - 11 files changed, 245 insertions(+), 202 deletions(-) diff --git a/include/onnxruntime/core/graph/graph.h b/include/onnxruntime/core/graph/graph.h index 8c61550c63046..dc47c076b73cd 100644 --- a/include/onnxruntime/core/graph/graph.h +++ b/include/onnxruntime/core/graph/graph.h @@ -180,11 +180,6 @@ class Node { #if !defined(ORT_MINIMAL_BUILD) - void ClearLayeringAnnotation() { - std::string t; - layering_annotation_.swap(t); - } - /** Gets the Node's OpSchema. @remarks The graph containing this node must be resolved, otherwise nullptr will be returned. */ const ONNX_NAMESPACE::OpSchema* Op() const noexcept { return op_; } @@ -266,6 +261,13 @@ class Node { #endif // !defined(ORT_MINIMAL_BUILD) #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + + // Make sure that the annotation does not occupy memory after partitioning is done. + void ClearLayeringAnnotation() { + std::string t; + layering_annotation_.swap(t); + } + /** Gets a modifiable count of arguments for each of the Node's explicit inputs. @todo This should be removed in favor of a method that updates the input args and the count. Currently these operations are separate which is not a good setup. */ diff --git a/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h b/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h index 9361c96156e3a..2ac4d7f649008 100644 --- a/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h +++ b/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h @@ -330,7 +330,7 @@ static const char* const kOrtSessionOptionsCollectNodeMemoryStatsToFile = "sessi /// This enables an ad-hoc and flexible scenarios with no pre-recorded stats, but may be less accurate. /// The setting with no limit is expected to look like: ",file name for collected stats" /// Finally a setting with both limit and pre-recorded stats absent can contain a single comma: ",". -/// The EP will place nodes on device "file name" (currently only CUDA is supported) : +/// The EP will attempt to place nodes on device (currently only CUDA is supported) : /// this file is expected to be found at the same folder with the model. The file contains /// pre-recorded stats collected when running with kOrtSessionOptionsCollectNodeMemoryStatsToFile enforce (see above) static const char* const kOrtSessionOptionsResourceCudaPartitioningSettings = diff --git a/onnxruntime/core/framework/graph_partitioner.cc b/onnxruntime/core/framework/graph_partitioner.cc index 49a7642a3a0ef..c30c064d27ffa 100644 --- a/onnxruntime/core/framework/graph_partitioner.cc +++ b/onnxruntime/core/framework/graph_partitioner.cc @@ -197,21 +197,21 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l const auto& graph_optimizer_registry = params.graph_optimizer_registry.get(); #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) - InlinedVector assigned_filteredin_nodes; - InlinedVector filteredin_nodes; + InlinedVector assigned_filtered_in_nodes; + InlinedVector filtered_in_nodes; #endif // Helper to create a GraphViewer that filters nodes based on layering_index if present. auto create_graph_viewer = [&](std::unique_ptr& sub_graph_holder, std::unique_ptr& viewer) -> Status { #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) if (params.layering_index) { - assigned_filteredin_nodes.clear(); - filteredin_nodes.clear(); - filteredin_nodes.reserve(graph.NumberOfNodes()); + assigned_filtered_in_nodes.clear(); + filtered_in_nodes.clear(); + filtered_in_nodes.reserve(graph.NumberOfNodes()); auto rules_opt = params.layering_index->GetLayeringRulesForThisEp(ep_type); if (rules_opt) { - assigned_filteredin_nodes.reserve(rules_opt->get().size()); + assigned_filtered_in_nodes.reserve(rules_opt->get().size()); } for (auto& node : graph.Nodes()) { @@ -222,16 +222,16 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l if (!rules_opt || rules_opt->get().count(*rule_idx_opt) == 0) { include = false; } else { - assigned_filteredin_nodes.push_back(node.Index()); + assigned_filtered_in_nodes.push_back(node.Index()); } } // If node has no assignment, it is included (available to any EP) if (include) { - filteredin_nodes.push_back(&node); + filtered_in_nodes.push_back(&node); } } - ORT_RETURN_IF_ERROR(graph_utils::CreateFilteredIndexedGraph(filteredin_nodes, graph, sub_graph_holder)); + ORT_RETURN_IF_ERROR(graph_utils::CreateFilteredIndexedGraph(filtered_in_nodes, graph, sub_graph_holder)); viewer = std::make_unique(graph, *sub_graph_holder); return Status::OK(); } @@ -256,7 +256,7 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l // Check if all assigned filtered-in nodes are claimed // and if not make them available for subsequent EPs - for (auto& node_index : assigned_filteredin_nodes) { + for (auto& node_index : assigned_filtered_in_nodes) { if (claimed.count(node_index) == 0) { auto rule_idx_opt = params.layering_index->GetNodeAssignment(graph, node_index); if (rule_idx_opt && ep_rules.count(*rule_idx_opt) > 0) { @@ -264,7 +264,7 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l } } } - assigned_filteredin_nodes.clear(); + assigned_filtered_in_nodes.clear(); } } #endif @@ -1207,7 +1207,8 @@ static Status PartitionOrtFormatModelImpl(const PartitionParams& partition_param #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) nullptr, std::ref(graph_optimizer_registry), - partition_params.check_load_cancellation_fn + partition_params.check_load_cancellation_fn, + partition_params.layering_index }; // clang-format on diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc index 9eb7788266040..3dbf5d36c7ffc 100644 --- a/onnxruntime/core/framework/layering_annotations.cc +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -12,6 +12,7 @@ #include "core/framework/execution_providers.h" #include "core/graph/graph.h" +#include #include namespace onnxruntime { @@ -128,10 +129,14 @@ std::optional LayeringRuleMatcher::Match(const std::string& node_annotat namespace { bool CaseInsensitiveCompare(std::string_view a, std::string_view b) { return std::equal(a.begin(), a.end(), b.begin(), b.end(), - [](char c1, char c2) { return std::tolower(c1) == std::tolower(c2); }); + [](char c1, char c2) { + return std::tolower(static_cast(c1)) == + std::tolower(static_cast(c2)); + }); } bool TryParseIndex(const std::string& str, uint32_t& index) { + if (str.empty() || str[0] == '-') return false; char* end = nullptr; const char* ptr = str.c_str(); errno = 0; @@ -139,6 +144,9 @@ bool TryParseIndex(const std::string& str, uint32_t& index) { if (errno != 0 || end != ptr + str.size()) { return false; } + if (val > std::numeric_limits::max()) { + return false; + } index = narrow(val); return true; } @@ -576,6 +584,35 @@ void LayeringIndex::Update(const Graph& graph, gsl::span nodes) graph_index_.emplace(&graph, std::move(*new_index)); } } + +void LayeringRuleMatcher::AddExactRule(const std::string& annotation, size_t index) { + // Only store the first occurrence (lowest index) + exact_match_rules_.insert({annotation, index}); +} + +void LayeringRuleMatcher::AddPrefixRule(const std::string& annotation, size_t index) { + TrieNode* current = &root_; + for (char c : annotation) { + auto p = current->children.insert({c, nullptr}); + if (p.second) { + p.first->second = std::make_unique(); + } + current = p.first->second.get(); + } + + // Only store if strictly better (lower index) or not set + // Since we iterate rules 0..N, if a rule index is already set for this node, + // it corresponds to a higher priority rule, so we skip overwriting it. + if (!current->rule_index) { + current->rule_index = index; + } +} + +void LayeringRuleMatcher::UpdateBestMatch(std::optional& current_best, size_t candidate) const { + if (!current_best || candidate < *current_best) { + current_best = candidate; + } +} } // namespace onnxruntime #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) diff --git a/onnxruntime/core/framework/layering_annotations.h b/onnxruntime/core/framework/layering_annotations.h index fd5bebafeda0c..b31d3be33705e 100644 --- a/onnxruntime/core/framework/layering_annotations.h +++ b/onnxruntime/core/framework/layering_annotations.h @@ -38,6 +38,13 @@ struct LayeringRules { std::vector rules; /// /// Parses the layering rules from the given configuration string. + /// The configuration string is in the following format.: + /// 'cpu(L1,L2); gpu(L3,=L4)' where cpu or gpu denote the target EP. + /// L1, L2, L3 are annotations that can be matched to node annotations in the graph. The '=' prefix denotes + /// exact match. The position of the annotation (L1, L2, L3) in the list denotes its priority in matching (left to right). + /// However, the prefix annotations will always have higher priority than the exact match annotations regardless + /// of their position in the list. In the above example, L1 has the highest priority, followed by L2, + /// then L3 and finally L4. The rules are separated by ';' and there can be multiple rules for different EPs. /// /// The configuration string to parse. /// Output parameter where the parsed rules will be stored. @@ -69,34 +76,11 @@ class LayeringRuleMatcher { TrieNode root_; InlinedHashMap exact_match_rules_; - void AddExactRule(const std::string& annotation, size_t index) { - // Only store the first occurrence (lowest index) - exact_match_rules_.insert({annotation, index}); - } + void AddExactRule(const std::string& annotation, size_t index); - void AddPrefixRule(const std::string& annotation, size_t index) { - TrieNode* current = &root_; - for (char c : annotation) { - auto p = current->children.insert({c, nullptr}); - if (p.second) { - p.first->second = std::make_unique(); - } - current = p.first->second.get(); - } + void AddPrefixRule(const std::string& annotation, size_t index); - // Only store if strictly better (lower index) or not set - // Since we iterate rules 0..N, if a rule index is already set for this node, - // it corresponds to a higher priority rule, so we skip overwriting it. - if (!current->rule_index) { - current->rule_index = index; - } - } - - void UpdateBestMatch(std::optional& current_best, size_t candidate) const { - if (!current_best || candidate < *current_best) { - current_best = candidate; - } - } + void UpdateBestMatch(std::optional& current_best, size_t candidate) const; }; namespace EpLayeringMatcher { @@ -118,7 +102,7 @@ std::optional Match(gsl::span ep_devices, /// The rule containing the device designator. /// Optional containing the matched EP type, nullopt otherwise. std::optional Match(const ExecutionProviders& providers, const LayerAnnotation& rule); -}; // namespace EpLayeringMatcher +} // namespace EpLayeringMatcher // This class contains indexing information about the entire graph // per sub-graph info is stored in graph_index_ @@ -207,6 +191,21 @@ class LayeringIndex { auto layer_to_nodes_hit = graph_layering_index.layer_to_node_ids_.find(*layer_idx); if (layer_to_nodes_hit != graph_layering_index.layer_to_node_ids_.end()) { layer_to_nodes_hit->second.erase(node_id); + // If the layer has no more nodes assigned across this graph, + // remove the layer index from the EP mapping so subsequent + // partitioning passes no longer reserve this layer for the EP. + if (layer_to_nodes_hit->second.empty()) { + graph_layering_index.layer_to_node_ids_.erase(layer_to_nodes_hit); + // Update ep_name_to_layering_indices_ to remove this layer index + // from the EP that owned it, making it available for other EPs. + auto rule_to_ep_hit = layering_index_to_ep_name_.find(*layer_idx); + if (rule_to_ep_hit != layering_index_to_ep_name_.end()) { + auto ep_hit = ep_name_to_layering_indices_.find(rule_to_ep_hit->second); + if (ep_hit != ep_name_to_layering_indices_.end()) { + ep_hit->second.erase(*layer_idx); + } + } + } } } } @@ -217,7 +216,7 @@ class LayeringIndex { /// and updates the assignment. /// /// The graph containing the nodes. - /// Pixels of nodes to check and update. + /// Indices of nodes to check and update. void Update(const Graph& graph, gsl::span nodes); private: @@ -242,12 +241,10 @@ class LayeringIndex { // If the node is not in this map, it is unassigned NodeIndexToLayeringIndex node_to_layering_index_; // This map contains mapping of LayeringRule index to the list of node ids - // Revers from the above 1:M + // Reverse from the above 1:M LayerIndexToNodes layer_to_node_ids_; }; - LayeringIndex() = default; - LayeringIndex(LayeringRules layering_rules, EpNameToLayeringIndices ep_name_to_layering_indices, LayeringIndexToEpName layering_index_to_ep_name) : rules_(std::move(layering_rules)), matcher_(rules_), diff --git a/onnxruntime/core/graph/graph_utils.cc b/onnxruntime/core/graph/graph_utils.cc index a6cdda7da5e91..00243574dffdf 100644 --- a/onnxruntime/core/graph/graph_utils.cc +++ b/onnxruntime/core/graph/graph_utils.cc @@ -32,6 +32,151 @@ static int GetIndexFromName(const Node& node, const std::string& name, bool is_i return static_cast(index); } +Status CreateFilteredIndexedGraph(gsl::span nodes, const Graph& graph, + std::unique_ptr& result) { + // Following data structures help determine the final inputs/outputs of the subgraph. + // Note: The 'subgraph' here refers to a graph that contains a subset of nodes in the 'src_graph'. + + // Pre-pass: Identify all outputs produced by nodes within the subgraph. + // This allows O(1) checks to determine if an input is internal or from the boundary. + InlinedHashSet node_set; + InlinedHashSet internal_outputs; + for (size_t i = 0, lim = nodes.size(); i < lim; i++) { + const auto& node = *nodes[i]; + node_set.insert(node.Index()); + for (const auto& output : node.OutputDefs()) { + internal_outputs.insert(output); + } + } + + // Source graph output names + InlinedHashSet graph_output_names; + for (const auto* output_arg : graph.GetOutputs()) { + graph_output_names.insert(output_arg->Name()); + } + + // These maps store the inputs and outputs of the subgraph. + // Value is order index to maintain deterministic order. + InlinedHashMap subgraph_inputs, subgraph_outputs; + + int input_order = 0; + int output_order = 0; + + std::unique_ptr indexed_sub_graph = std::make_unique(); + InlinedVector initializers; + + // Add nodes and identify boundary inputs/outputs + for (size_t i = 0, lim = nodes.size(); i < lim; i++) { + const auto& node = *nodes[i]; + indexed_sub_graph->nodes.push_back(node.Index()); + + // Process Inputs: If an input is not produced internally, it's a subgraph input. + auto process_inputs = [&](gsl::span inputs) { + for (const auto& input : inputs) { + if (!input->Exists()) continue; + + const auto* tensor_proto = graph.GetConstantInitializer(input->Name(), true); + if (tensor_proto != nullptr) { + initializers.push_back(input->Name()); + continue; + } + + // If not produced by this subgraph, it's a boundary input + if (internal_outputs.count(input) == 0) { + // Use insert to keep the first occurrence's order + subgraph_inputs.emplace(input, input_order++); + } + } + }; + + process_inputs(gsl::make_span(node.InputDefs().data(), node.InputDefs().size())); + process_inputs(gsl::make_span(node.ImplicitInputDefs().data(), node.ImplicitInputDefs().size())); + + // Process Outputs: If an output is graph output OR consumed externally, it's a subgraph output. + for (const auto& output : node.OutputDefs()) { + if (!output->Exists()) continue; + + bool is_boundary_output = false; + + // 1. Is it a graph output? + if (graph_output_names.count(output->Name()) > 0) { + is_boundary_output = true; + } else { + // 2. Is it consumed by any node outside the subgraph? + for (auto it = node.OutputEdgesBegin(), end = node.OutputEdgesEnd(); it != end; ++it) { + // Check if the edge uses this specific output + if (it->GetSrcArgIndex() < static_cast(node.OutputDefs().size()) && + node.OutputDefs()[it->GetSrcArgIndex()] == output) { + if (node_set.count(it->GetNode().Index()) == 0) { + is_boundary_output = true; + break; + } + } + } + } + + if (is_boundary_output) { + subgraph_outputs.insert({output, output_order++}); + } + } + } + + std::multimap inputs, outputs; + + // Get the input order of the original graph + InlinedHashMap original_inputs; + int order = 0; + for (const auto* input : graph.GetInputs()) { + original_inputs[input] = order++; + } + + // input order needs to be consistent with original graph's input order + for (const auto& [node_arg, subgraph_input_order] : subgraph_inputs) { + const auto original_input_it = original_inputs.find(node_arg); + + if (original_input_it != original_inputs.end()) { + inputs.emplace( + original_input_it->second, // input order from original graph + node_arg); + } else { + inputs.emplace( + subgraph_input_order, // input order from subgraph + node_arg); + } + } + + // Sort outputs by the order they were added + for (const auto& [node_arg, subgraph_output_order] : subgraph_outputs) { + outputs.emplace(subgraph_output_order, node_arg); + } + + std::unique_ptr meta_def = std::make_unique(); + meta_def->name = "sub_graph"; + meta_def->since_version = 1; + + // Assign inputs and outputs to subgraph's meta_def + for (const auto& input : inputs) { + if (input.second->Exists()) { + meta_def->inputs.push_back(input.second->Name()); + } + } + + for (const auto& initializer : initializers) { + meta_def->constant_initializers.push_back(initializer); + } + + for (const auto& output : outputs) { + if (output.second->Exists()) { + meta_def->outputs.push_back(output.second->Name()); + } + } + + indexed_sub_graph->SetMetaDef(std::move(meta_def)); + result = std::move(indexed_sub_graph); + + return Status::OK(); +} + #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) #if !defined(ORT_MINIMAL_BUILD) @@ -1009,152 +1154,6 @@ NodeArg& CreateNodeArg(Graph& graph, const NodeArg& base_arg) { return graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(base_arg.Name()), base_arg.TypeAsProto()); } -Status CreateFilteredIndexedGraph(gsl::span nodes, const Graph& graph, - std::unique_ptr& result) { - // Following data structures help determine the final inputs/outputs of the subgraph. - // Note: The 'subgraph' here refers to a graph contains a subset of nodes in the 'src_graph'. - - // Pre-pass: Identify all outputs produced by nodes within the subgraph. - // This allows O(1) checks to determine if an input is internal or from the boundary. - InlinedHashSet node_set; - InlinedHashSet internal_outputs; - for (size_t i = 0, lim = nodes.size(); i < lim; i++) { - const auto& node = *nodes[i]; - node_set.insert(node.Index()); - for (const auto& output : node.OutputDefs()) { - internal_outputs.insert(output); - } - } - - // Source graph output names - InlinedHashSet graph_output_names; - for (const auto* output_arg : graph.GetOutputs()) { - graph_output_names.insert(output_arg->Name()); - } - - // These maps store the inputs and outputs of the subgraph. - // Value is order index to maintain deterministic order. - InlinedHashMap subgraph_inputs, subgraph_outputs; - - int input_order = 0; - int output_order = 0; - - std::unique_ptr indexed_sub_graph = std::make_unique(); - InlinedVector initializers; - - // Add nodes and identify boundary inputs/outputs - for (size_t i = 0, lim = nodes.size(); i < lim; i++) { - const auto& node = *nodes[i]; - indexed_sub_graph->nodes.push_back(node.Index()); - - // Process Inputs: If an input is not produced internally, it's a subgraph input. - auto process_inputs = [&](gsl::span inputs) { - for (const auto& input : inputs) { - if (!input->Exists()) continue; - - const auto* tensor_proto = graph.GetConstantInitializer(input->Name(), true); - if (tensor_proto != nullptr) { - initializers.push_back(input->Name()); - continue; - } - - // If not produced by this subgraph, it's a boundary input - if (internal_outputs.count(input) == 0) { - // Use insert to keep the first occurrence's order - subgraph_inputs.emplace(input, input_order++); - } - } - }; - - process_inputs(gsl::make_span(node.InputDefs().data(), node.InputDefs().size())); - process_inputs(gsl::make_span(node.ImplicitInputDefs().data(), node.ImplicitInputDefs().size())); - - // Process Outputs: If an output is graph output OR consumed externally, it's a subgraph output. - for (const auto& output : node.OutputDefs()) { - if (!output->Exists()) continue; - - bool is_boundary_output = false; - - // 1. Is it a graph output? - if (graph_output_names.count(output->Name()) > 0) { - is_boundary_output = true; - } else { - // 2. Is it consumed by any node outside the subgraph? - for (auto it = node.OutputEdgesBegin(), end = node.OutputEdgesEnd(); it != end; ++it) { - // Check if the edge uses this specific output - if (it->GetSrcArgIndex() < static_cast(node.OutputDefs().size()) && - node.OutputDefs()[it->GetSrcArgIndex()] == output) { - if (node_set.count(it->GetNode().Index()) == 0) { - is_boundary_output = true; - break; - } - } - } - } - - if (is_boundary_output) { - subgraph_outputs.insert({output, output_order++}); - } - } - } - - std::multimap inputs, outputs; - - // Get the input order of the original graph - InlinedHashMap original_inputs; - int order = 0; - for (const auto* input : graph.GetInputs()) { - original_inputs[input] = order++; - } - - // input order needs to be consistent with original graph's input order - for (const auto& [node_arg, subgraph_input_order] : subgraph_inputs) { - const auto original_input_it = original_inputs.find(node_arg); - - if (original_input_it != original_inputs.end()) { - inputs.emplace( - original_input_it->second, // input order from original graph - node_arg); - } else { - inputs.emplace( - subgraph_input_order, // input order from subgraph - node_arg); - } - } - - // Sort outputs by the order they were added - for (const auto& [node_arg, subgraph_output_order] : subgraph_outputs) { - outputs.emplace(subgraph_output_order, node_arg); - } - - std::unique_ptr meta_def = std::make_unique(); - meta_def->name = "sub_graph"; - meta_def->since_version = 1; - - // Assign inputs and outputs to subgraph's meta_def - for (const auto& input : inputs) { - if (input.second->Exists()) { - meta_def->inputs.push_back(input.second->Name()); - } - } - - for (const auto& initializer : initializers) { - meta_def->constant_initializers.push_back(initializer); - } - - for (const auto& output : outputs) { - if (output.second->Exists()) { - meta_def->outputs.push_back(output.second->Name()); - } - } - - indexed_sub_graph->SetMetaDef(std::move(meta_def)); - result = std::move(indexed_sub_graph); - - return Status::OK(); -} - #endif // !defined(ORT_MINIMAL_BUILD) - } // namespace graph_utils } // namespace onnxruntime diff --git a/onnxruntime/core/graph/graph_utils.h b/onnxruntime/core/graph/graph_utils.h index 3ca32bdff2778..8152821192795 100644 --- a/onnxruntime/core/graph/graph_utils.h +++ b/onnxruntime/core/graph/graph_utils.h @@ -473,6 +473,10 @@ bool RemoveNodesWithOneOutputBottomUp(Graph& graph, const Node& node); */ NodeArg& CreateNodeArg(Graph& graph, const NodeArg& base_arg); +#endif // !defined(ORT_MINIMAL_BUILD) + +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) + /// /// This function creates an indexed subgraph from a collection of nodes /// using the graph instance. The IndexedSubgraph can then we used to create @@ -485,7 +489,7 @@ NodeArg& CreateNodeArg(Graph& graph, const NodeArg& base_arg); Status CreateFilteredIndexedGraph(gsl::span nodes, const Graph& graph, std::unique_ptr& indexed_subgraph); -#endif // !defined(ORT_MINIMAL_BUILD) +#endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) } // namespace graph_utils } // namespace onnxruntime diff --git a/onnxruntime/core/session/onnxruntime_c_api.cc b/onnxruntime/core/session/onnxruntime_c_api.cc index 237afa782690c..6838ed5743286 100644 --- a/onnxruntime/core/session/onnxruntime_c_api.cc +++ b/onnxruntime/core/session/onnxruntime_c_api.cc @@ -3042,8 +3042,11 @@ ORT_API_STATUS_IMPL(OrtApis::Graph_GetGraphView, _In_ const OrtGraph* src_graph, const Node& node = ep_node->GetInternalNode(); node_set.insert(node.Index()); internal_nodes.push_back(&node); + } else { + std::ostringstream oss; + oss << "node indexed [" << i << "] appears to be a ModelEditorGraph"; + return OrtApis::CreateStatus(OrtErrorCode::ORT_INVALID_ARGUMENT, oss.str().c_str()); } - // else XXX is ignoring null nodes OK? } // Create a GraphViewer with filtered info diff --git a/onnxruntime/test/framework/layering_annotations_test.cc b/onnxruntime/test/framework/layering_annotations_test.cc index ecbdd0ae15beb..32951311b8059 100644 --- a/onnxruntime/test/framework/layering_annotations_test.cc +++ b/onnxruntime/test/framework/layering_annotations_test.cc @@ -3,6 +3,7 @@ #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) +#include "core/framework/execution_providers.h" #include "core/framework/ortmemoryinfo.h" #include "core/framework/layering_annotations.h" #include "core/session/abi_devices.h" @@ -11,7 +12,8 @@ #include "core/graph/constants.h" #include "core/graph/model.h" // For Model, Graph #include "gtest/gtest.h" -#include "core/framework/execution_providers.h" + +#include "test/util/include/asserts.h" namespace onnxruntime { namespace test { diff --git a/onnxruntime/test/framework/session_state_test.cc b/onnxruntime/test/framework/session_state_test.cc index 3e566952f5dc5..5095e0f219d5c 100644 --- a/onnxruntime/test/framework/session_state_test.cc +++ b/onnxruntime/test/framework/session_state_test.cc @@ -452,7 +452,7 @@ void LoadWithResourceAwarePartitioning(const ORTCHAR_T* model_path, default_logger, profiler, sess_options); LayeringIndex* layering_index = nullptr; - static std::optional layering_index_storage; + std::optional layering_index_storage; if (!layering_config.empty()) { ASSERT_STATUS_OK(LayeringIndex::Create(graph, layering_config, {}, execution_providers, default_logger, layering_index_storage)); diff --git a/onnxruntime/test/framework/tensorutils_test.cc b/onnxruntime/test/framework/tensorutils_test.cc index 680a91d30ec16..2e7bed9c73f49 100644 --- a/onnxruntime/test/framework/tensorutils_test.cc +++ b/onnxruntime/test/framework/tensorutils_test.cc @@ -664,7 +664,6 @@ TEST(TensorProtoUtilsTest, GetNodeProtoLayeringAnnotation) { } } - // Tests for ValidateEmbeddedTensorProtoDataSizeAndShape and embedded initializer size limits TEST(TensorProtoDataSizeShapeValidationTest, ValidTensorProtoWithRawData) { @@ -902,6 +901,5 @@ TEST(TensorProtoDataSizeShapeValidationTest, ExternalDataValidFileSizeSucceeds) } #endif // !defined(__wasm__) - } // namespace test } // namespace onnxruntime From 358f7df6102750200bb93e9edf765ba13b35859f Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Mon, 9 Mar 2026 16:43:01 -0700 Subject: [PATCH 15/57] Reject duplicate rules --- .../core/framework/layering_annotations.cc | 14 +++++++++++ .../framework/layering_annotations_test.cc | 23 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc index 3dbf5d36c7ffc..0b8eb719fc807 100644 --- a/onnxruntime/core/framework/layering_annotations.cc +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -23,6 +23,11 @@ common::Status LayeringRules::FromConfigString(const std::string& config_value, return common::Status::OK(); } + // Track seen annotations to reject duplicates. + // Separate sets for exact and prefix match annotations. + InlinedHashSet seen_exact_annotations; + InlinedHashSet seen_prefix_annotations; + auto entries = utils::SplitString(config_value, ";"); for (const auto& e : entries) { auto entry = utils::TrimString(e); @@ -69,6 +74,15 @@ common::Status LayeringRules::FromConfigString(const std::string& config_value, continue; } + // Check for duplicate annotation (same annotation string and match type) + auto& seen_set = prefix_match ? seen_prefix_annotations : seen_exact_annotations; + auto [it, inserted] = seen_set.insert(ann); + if (!inserted) { + return ORT_MAKE_STATUS(ONNXRUNTIME, INVALID_ARGUMENT, + "Invalid layering config: Duplicate ", (prefix_match ? "prefix" : "exact"), + " match annotation '", ann, "' found in entry: ", entry); + } + rules.rules.push_back({device, std::move(ann), prefix_match}); } } diff --git a/onnxruntime/test/framework/layering_annotations_test.cc b/onnxruntime/test/framework/layering_annotations_test.cc index 32951311b8059..2a04b6291315f 100644 --- a/onnxruntime/test/framework/layering_annotations_test.cc +++ b/onnxruntime/test/framework/layering_annotations_test.cc @@ -996,7 +996,28 @@ TEST(LayeringRulesTest, FromConfigString_IgnoresEmptyEntries) { EXPECT_TRUE(rules.rules.empty()); } +TEST(LayeringRulesTest, FromConfigString_RejectsDuplicateAnnotations) { + LayeringRules rules; + + // Duplicate exact annotation within the same device + EXPECT_FALSE(LayeringRules::FromConfigString("EP1(Ann1, Ann1)", rules).IsOK()); + + // Duplicate exact annotation across different devices + EXPECT_FALSE(LayeringRules::FromConfigString("EP1(Ann1); EP2(Ann1)", rules).IsOK()); + + // Duplicate prefix annotation within the same device + EXPECT_FALSE(LayeringRules::FromConfigString("EP1(=Ann1, =Ann1)", rules).IsOK()); + + // Duplicate prefix annotation across different devices + EXPECT_FALSE(LayeringRules::FromConfigString("EP1(=Ann1); EP2(=Ann1)", rules).IsOK()); + + // Same annotation but different match types (exact vs prefix) should be OK + ASSERT_STATUS_OK(LayeringRules::FromConfigString("EP1(Ann1, =Ann1)", rules)); + ASSERT_EQ(rules.rules.size(), 2u); + EXPECT_FALSE(rules.rules[0].prefix_match); + EXPECT_TRUE(rules.rules[1].prefix_match); +} } // namespace test } // namespace onnxruntime -#endif // !defined(ORT_MINIMAL_BUILD) && defined(ORT_EXTENDED_MINIMAL_BUILD) +#endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) \ No newline at end of file From 653fb8b11a8fa2938a797f3c10a8bc295ee0339c Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Mon, 9 Mar 2026 16:51:06 -0700 Subject: [PATCH 16/57] Move methods to .cc --- .../core/framework/layering_annotations.cc | 63 ++++++++++++++++++- .../core/framework/layering_annotations.h | 62 +----------------- 2 files changed, 65 insertions(+), 60 deletions(-) diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc index 0b8eb719fc807..6863efb88f50a 100644 --- a/onnxruntime/core/framework/layering_annotations.cc +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -529,7 +529,6 @@ void LayeringIndex::ProcessGraph(const Graph& graph, std::optional paren was_updated = true; } else { // reset since no valid EP mapping - // so it does not propagate to sub-graphs if any matched_rule_idx = std::nullopt; } } @@ -627,6 +626,68 @@ void LayeringRuleMatcher::UpdateBestMatch(std::optional& current_best, s current_best = candidate; } } + +std::optional>> +LayeringIndex::GetLayeringRulesForThisEp(const std::string& ep_type) const { + auto hit = ep_name_to_layering_indices_.find(ep_type); + if (hit == ep_name_to_layering_indices_.end()) { + return {}; + } + return hit->second; +} + +std::optional LayeringIndex::GetNodeAssignment(const Graph& graph, NodeIndex node_id) const { + auto hit = graph_index_.find(&graph); + if (hit == graph_index_.end()) { + return {}; + } + + // Nodes in subgraph that were not annotated has already inherited their + // annotation if any from the parent node of the subgraph + const auto& graph_layering_index = hit->second; + auto layer_hit = graph_layering_index.node_to_layering_index_.find(node_id); + if (layer_hit != graph_layering_index.node_to_layering_index_.end()) { + return layer_hit->second; + } + return {}; +} + +void LayeringIndex::MakeNodeUnassigned(const Graph& graph, NodeIndex node_id) { + auto hit = graph_index_.find(&graph); + if (hit == graph_index_.end()) { + return; + } + auto& graph_layering_index = hit->second; + auto node_to_layer_hit = graph_layering_index.node_to_layering_index_.find(node_id); + std::optional layer_idx; + if (node_to_layer_hit != graph_layering_index.node_to_layering_index_.end()) { + // Get the layer index + layer_idx = node_to_layer_hit->second; + graph_layering_index.node_to_layering_index_.erase(node_to_layer_hit); + } + // Remove node from layer collection + if (layer_idx) { + auto layer_to_nodes_hit = graph_layering_index.layer_to_node_ids_.find(*layer_idx); + if (layer_to_nodes_hit != graph_layering_index.layer_to_node_ids_.end()) { + layer_to_nodes_hit->second.erase(node_id); + // If the layer has no more nodes assigned across this graph, + // remove the layer index from the EP mapping so subsequent + // partitioning passes no longer reserve this layer for the EP. + if (layer_to_nodes_hit->second.empty()) { + graph_layering_index.layer_to_node_ids_.erase(layer_to_nodes_hit); + // Update ep_name_to_layering_indices_ to remove this layer index + // from the EP that owned it, making it available for other EPs. + auto rule_to_ep_hit = layering_index_to_ep_name_.find(*layer_idx); + if (rule_to_ep_hit != layering_index_to_ep_name_.end()) { + auto ep_hit = ep_name_to_layering_indices_.find(rule_to_ep_hit->second); + if (ep_hit != ep_name_to_layering_indices_.end()) { + ep_hit->second.erase(*layer_idx); + } + } + } + } + } +} } // namespace onnxruntime #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) diff --git a/onnxruntime/core/framework/layering_annotations.h b/onnxruntime/core/framework/layering_annotations.h index b31d3be33705e..6c6a00c996b94 100644 --- a/onnxruntime/core/framework/layering_annotations.h +++ b/onnxruntime/core/framework/layering_annotations.h @@ -146,70 +146,14 @@ class LayeringIndex { // Returns the Layering Rule indices mapped to the EP if any std::optional>> - GetLayeringRulesForThisEp(const std::string& ep_type) const { - auto hit = ep_name_to_layering_indices_.find(ep_type); - if (hit == ep_name_to_layering_indices_.end()) { - return {}; - } - return hit->second; - } + GetLayeringRulesForThisEp(const std::string& ep_type) const; // This function returns an index for the Layering rule the node is assigned to if any - std::optional GetNodeAssignment(const Graph& graph, NodeIndex node_id) const { - auto hit = graph_index_.find(&graph); - if (hit == graph_index_.end()) { - return {}; - } - - // Nodes in subgraph that were not annotated has already inherited their - // annotation if any from the parent node of the subgraph - const auto& graph_layering_index = hit->second; - auto layer_hit = graph_layering_index.node_to_layering_index_.find(node_id); - if (layer_hit != graph_layering_index.node_to_layering_index_.end()) { - return layer_hit->second; - } - return {}; - } + std::optional GetNodeAssignment(const Graph& graph, NodeIndex node_id) const; // This is used when an EP fails to claim a node during partitioning so we make it // available for other EPs - void MakeNodeUnassigned(const Graph& graph, NodeIndex node_id) { - auto hit = graph_index_.find(&graph); - if (hit == graph_index_.end()) { - return; - } - auto& graph_layering_index = hit->second; - auto node_to_layer_hit = graph_layering_index.node_to_layering_index_.find(node_id); - std::optional layer_idx; - if (node_to_layer_hit != graph_layering_index.node_to_layering_index_.end()) { - // Get the layer index - layer_idx = node_to_layer_hit->second; - graph_layering_index.node_to_layering_index_.erase(node_to_layer_hit); - } - // Remove node from layer collection - if (layer_idx) { - auto layer_to_nodes_hit = graph_layering_index.layer_to_node_ids_.find(*layer_idx); - if (layer_to_nodes_hit != graph_layering_index.layer_to_node_ids_.end()) { - layer_to_nodes_hit->second.erase(node_id); - // If the layer has no more nodes assigned across this graph, - // remove the layer index from the EP mapping so subsequent - // partitioning passes no longer reserve this layer for the EP. - if (layer_to_nodes_hit->second.empty()) { - graph_layering_index.layer_to_node_ids_.erase(layer_to_nodes_hit); - // Update ep_name_to_layering_indices_ to remove this layer index - // from the EP that owned it, making it available for other EPs. - auto rule_to_ep_hit = layering_index_to_ep_name_.find(*layer_idx); - if (rule_to_ep_hit != layering_index_to_ep_name_.end()) { - auto ep_hit = ep_name_to_layering_indices_.find(rule_to_ep_hit->second); - if (ep_hit != ep_name_to_layering_indices_.end()) { - ep_hit->second.erase(*layer_idx); - } - } - } - } - } - } - + void MakeNodeUnassigned(const Graph& graph, NodeIndex node_id); /// /// Updates the layering index for a specific set of nodes in a graph. /// This checks if the nodes have annotations, and if so, matches them against the rules From 23a8ecfa3e1534ae6fb6af067d73e0b0c2d85cc3 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Tue, 10 Mar 2026 11:26:10 -0700 Subject: [PATCH 17/57] Remove code duplication --- .../core/framework/layering_annotations.cc | 354 +++++++----------- 1 file changed, 130 insertions(+), 224 deletions(-) diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc index 6863efb88f50a..3f2c7b1edcf64 100644 --- a/onnxruntime/core/framework/layering_annotations.cc +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -164,254 +164,159 @@ bool TryParseIndex(const std::string& str, uint32_t& index) { index = narrow(val); return true; } + +// Normalized view of an EP's device properties used by the matching logic. +// All fields are non-owning references or value types. +struct EpDeviceView { + std::string_view ep_name; + OrtDevice::DeviceType device_type; // OrtDevice::CPU, GPU, NPU, FPGA, etc. + uint32_t vendor_id; + OrtDevice::DeviceId device_id; + std::string_view vendor_string; // from OrtHardwareDevice::vendor (empty if unavailable) + bool has_hardware_info; // true if hardware device info was available +}; + +bool MatchEpDevice(const EpDeviceView& ep, + std::string_view target_type_str, + std::string_view target_specifier, + std::string_view target_full) { + // "cpu" + if (CaseInsensitiveCompare(target_type_str, "cpu")) { + return ep.ep_name == kCpuExecutionProvider || + ep.device_type == OrtDevice::CPU; + } + // "gpu" + if (CaseInsensitiveCompare(target_type_str, "gpu")) { + if (target_specifier.empty()) { + if (ep.device_type == OrtDevice::GPU) return true; + // Heuristic fallback for common GPU EPs if hardware info is missing + return ep.ep_name == kCudaExecutionProvider || ep.ep_name == kDmlExecutionProvider; + } + // "gpu:" or "gpu:" + if (ep.device_type == OrtDevice::GPU) { + uint32_t index = std::numeric_limits::max(); + if (TryParseIndex(std::string(target_specifier), index)) { + return ep.device_id == static_cast(index); + } + // gpu: + if (!ep.vendor_string.empty() && CaseInsensitiveCompare(ep.vendor_string, target_specifier)) { + return true; + } + if (CaseInsensitiveCompare(target_specifier, "nvidia") && + ep.vendor_id == OrtDevice::VendorIds::NVIDIA) return true; + if (CaseInsensitiveCompare(target_specifier, "amd") && + ep.vendor_id == OrtDevice::VendorIds::AMD) return true; + if (CaseInsensitiveCompare(target_specifier, "intel") && + ep.vendor_id == OrtDevice::VendorIds::INTEL) return true; + // Heuristic: gpu:nvidia -> CUDA + if (CaseInsensitiveCompare(target_specifier, "nvidia") && + ep.ep_name == kCudaExecutionProvider) return true; + } + return false; + } + // "accelerator" + if (CaseInsensitiveCompare(target_type_str, "accelerator")) { + return ep.ep_name != kCpuExecutionProvider && ep.device_type != OrtDevice::CPU; + } + // "npu" + if (CaseInsensitiveCompare(target_type_str, "npu")) { + if (ep.device_type == OrtDevice::NPU) return true; + return ep.ep_name == kQnnExecutionProvider || ep.ep_name == kVitisAIExecutionProvider; + } + // "fpga" + if (CaseInsensitiveCompare(target_type_str, "fpga")) { + return ep.device_type == OrtDevice::FPGA; + } + // "cuda" + if (CaseInsensitiveCompare(target_type_str, "cuda")) { + return ep.ep_name == kCudaExecutionProvider; + } + // "dml" + if (CaseInsensitiveCompare(target_type_str, "dml")) { + return ep.ep_name == kDmlExecutionProvider; + } + // Fallback: exact EP name match + return ep.ep_name == target_full; +} + +void ParseDeviceTarget(const std::string& target_full, + std::string& target_type_str, + std::string& target_specifier) { + const auto colon_pos = target_full.find(':'); + target_type_str = (colon_pos == std::string::npos) ? target_full : target_full.substr(0, colon_pos); + target_specifier = (colon_pos != std::string::npos) ? target_full.substr(colon_pos + 1) : std::string(); +} + } // namespace std::optional EpLayeringMatcher::Match(gsl::span ep_devices, const LayerAnnotation& rule) { - const std::string& target_full = rule.device; - const auto colon_pos = target_full.find(':'); - const std::string target_type_str = (colon_pos == std::string::npos) ? target_full : target_full.substr(0, colon_pos); - // vendor or index or uuid, if present - std::string target_specifier; - if (colon_pos != std::string::npos) { - target_specifier = target_full.substr(colon_pos + 1); - } + std::string target_type_str, target_specifier; + ParseDeviceTarget(rule.device, target_type_str, target_specifier); for (const auto* ep_device_ptr : ep_devices) { - if (!ep_device_ptr) { - continue; - } + if (!ep_device_ptr) continue; const OrtEpDevice& ep_device = *ep_device_ptr; - bool matched = false; - - // Helper to check device type from MemInfo if Hardware device logic fails/is absent - auto check_mem_device_type = [&](OrtDevice::DeviceType type) -> bool { - if (ep_device.device_memory_info) { - return ep_device.device_memory_info->device.Type() == type; - } - return false; - }; - - // 1. Exact Name / Alias match - // "cpu" - if (CaseInsensitiveCompare(target_type_str, "cpu")) { - if (ep_device.ep_name == kCpuExecutionProvider) { - matched = true; - } else if (ep_device.device && ep_device.device->type == OrtHardwareDeviceType_CPU) { - matched = true; - } else if (check_mem_device_type(OrtDevice::CPU)) { - matched = true; - } - } // "gpu" - else if (CaseInsensitiveCompare(target_type_str, "gpu")) { - // If simple "gpu" - if (target_specifier.empty()) { - if (ep_device.device && ep_device.device->type == OrtHardwareDeviceType_GPU) { - matched = true; - } else if (check_mem_device_type(OrtDevice::GPU)) { - matched = true; - } // Heuristic fallback for common GPU EPs if hardware info is missing. Should we also check for TRT here? - else if (ep_device.ep_name == kCudaExecutionProvider || ep_device.ep_name == kDmlExecutionProvider) { - matched = true; - } - } else { - // "gpu:" or "gpu:" - if (ep_device.device && ep_device.device->type == OrtHardwareDeviceType_GPU) { - uint32_t index = std::numeric_limits::max(); - if (TryParseIndex(target_specifier, index)) { - // gpu: - if (ep_device.device->device_id == index) { - matched = true; - } - } else { - // gpu: - if (CaseInsensitiveCompare(ep_device.device->vendor, target_specifier)) { - matched = true; - } - // Check against vendor ID - else if (CaseInsensitiveCompare(target_specifier, "nvidia") && - ep_device.device->vendor_id == OrtDevice::VendorIds::NVIDIA) { - matched = true; - } else if (CaseInsensitiveCompare(target_specifier, "amd") && - ep_device.device->vendor_id == OrtDevice::VendorIds::AMD) { - matched = true; - } else if (CaseInsensitiveCompare(target_specifier, "intel") && - ep_device.device->vendor_id == OrtDevice::VendorIds::INTEL) { - matched = true; - } - // Special shortcuts heuristics: gpu:nvidia -> CUDA - else if (CaseInsensitiveCompare(target_specifier, "nvidia") && - ep_device.ep_name == kCudaExecutionProvider) { - matched = true; - } - } - } - } - } - // "accelerator" (not cpu) - else if (CaseInsensitiveCompare(target_type_str, "accelerator")) { - if (ep_device.ep_name != kCpuExecutionProvider) { - // If we don't have HW info, assuming non-CPU EP is an accelerator. - // If we do have HW info, check it's not CPU. - const bool is_cpu_hw = (ep_device.device && ep_device.device->type == OrtHardwareDeviceType_CPU); - const bool is_cpu_mem = check_mem_device_type(OrtDevice::CPU); - - if (!is_cpu_hw && !is_cpu_mem) { - matched = true; - } - } - } // "npu" - else if (CaseInsensitiveCompare(target_type_str, "npu")) { - if (ep_device.device && ep_device.device->type == OrtHardwareDeviceType_NPU) { - matched = true; - } else if (ep_device.ep_name == kQnnExecutionProvider || ep_device.ep_name == kVitisAIExecutionProvider) { - // Heuristic for known NPU providers if HW device info is missing - // XXX: These can run on CPU as well, need to see if there any check that is missing. - matched = true; - } - } - // "fpga" - else if (CaseInsensitiveCompare(target_type_str, "fpga")) { - // No OrtHardwareDeviceType_FPGA currently, rely on OrtDevice::FPGA - if (check_mem_device_type(OrtDevice::FPGA)) { - matched = true; - } - } - // "cuda" - else if (CaseInsensitiveCompare(target_type_str, "cuda")) { - if (ep_device.ep_name == kCudaExecutionProvider) { - matched = true; - } - } - // "dml" - else if (CaseInsensitiveCompare(target_type_str, "dml")) { - if (ep_device.ep_name == kDmlExecutionProvider) { - matched = true; + // Build normalized view from OrtEpDevice + // For the OrtEpDevice overload, device type comes from either the hardware device + // or the memory info, with hardware device taking priority. + OrtDevice::DeviceType device_type = OrtDevice::CPU; // default + bool has_hw = ep_device.device != nullptr; + if (has_hw) { + // Map OrtHardwareDeviceType to OrtDevice::DeviceType + switch (ep_device.device->type) { + case OrtHardwareDeviceType_GPU: + device_type = OrtDevice::GPU; + break; + case OrtHardwareDeviceType_NPU: + device_type = OrtDevice::NPU; + break; + default: + device_type = OrtDevice::CPU; + break; } + } else if (ep_device.device_memory_info) { + device_type = ep_device.device_memory_info->device.Type(); } - // Fallback: Exact EP name string match (e.g. "MyCustomEP") - else if (ep_device.ep_name == target_full) { - matched = true; - } - if (matched) { - return ep_device.ep_name; + EpDeviceView view{ + ep_device.ep_name, + device_type, + has_hw ? ep_device.device->vendor_id : 0u, + has_hw ? static_cast(ep_device.device->device_id) : OrtDevice::DeviceId{}, + has_hw ? std::string_view(ep_device.device->vendor) : std::string_view{}, + has_hw}; + + if (MatchEpDevice(view, target_type_str, target_specifier, rule.device)) { + return std::string(ep_device.ep_name); } } - return std::nullopt; } -std::optional EpLayeringMatcher::Match(const ExecutionProviders& providers, const LayerAnnotation& rule) { - const std::string& target_full = rule.device; - const auto colon_pos = target_full.find(':'); - const std::string target_type_str = (colon_pos == std::string::npos) ? target_full : target_full.substr(0, colon_pos); - std::string target_specifier; - if (colon_pos != std::string::npos) { - target_specifier = target_full.substr(colon_pos + 1); - } +std::optional EpLayeringMatcher::Match(const ExecutionProviders& providers, + const LayerAnnotation& rule) { + std::string target_type_str, target_specifier; + ParseDeviceTarget(rule.device, target_type_str, target_specifier); for (const auto& ep_shared_ptr : providers) { - if (!ep_shared_ptr) { - continue; - } + if (!ep_shared_ptr) continue; const IExecutionProvider& ep = *ep_shared_ptr; - const std::string& ep_name = ep.Type(); const OrtDevice& device = ep.GetDevice(); - bool matched = false; + EpDeviceView view{ + ep.Type(), + device.Type(), + device.Vendor(), + device.Id(), + {}, // no vendor string available from IExecutionProvider + true}; // OrtDevice always available - // 1. Exact Name / Alias match - // "cpu" - if (CaseInsensitiveCompare(target_type_str, "cpu")) { - if (ep_name == kCpuExecutionProvider) { - matched = true; - } else if (device.Type() == OrtDevice::CPU) { - matched = true; - } - } // "gpu" - else if (CaseInsensitiveCompare(target_type_str, "gpu")) { - // If simple "gpu" - if (target_specifier.empty()) { - if (device.Type() == OrtDevice::GPU) { - matched = true; - } // Heuristics, XXX: Should we also check for TRT here? - else if (ep_name == kCudaExecutionProvider || ep_name == kDmlExecutionProvider) { - matched = true; - } - } else { - // "gpu:" or "gpu:" - if (device.Type() == OrtDevice::GPU) { - uint32_t index = std::numeric_limits::max(); - if (TryParseIndex(target_specifier, index)) { - // gpu: - if (device.Id() == static_cast(index)) { - matched = true; - } - } else { - // gpu: checking against Vendor ID - if (CaseInsensitiveCompare(target_specifier, "nvidia") && - device.Vendor() == OrtDevice::VendorIds::NVIDIA) { - matched = true; - } else if (CaseInsensitiveCompare(target_specifier, "amd") && - device.Vendor() == OrtDevice::VendorIds::AMD) { - matched = true; - } else if (CaseInsensitiveCompare(target_specifier, "intel") && - device.Vendor() == OrtDevice::VendorIds::INTEL) { - matched = true; - } - // Special shortcuts heuristics: gpu:nvidia -> CUDA - else if (CaseInsensitiveCompare(target_specifier, "nvidia") && ep_name == kCudaExecutionProvider) { - matched = true; - } - } - } - } - } - // "accelerator" (not cpu) - else if (CaseInsensitiveCompare(target_type_str, "accelerator")) { - if (ep_name != kCpuExecutionProvider) { - if (device.Type() != OrtDevice::CPU) { - matched = true; - } - } - } // "npu" - else if (CaseInsensitiveCompare(target_type_str, "npu")) { - if (device.Type() == OrtDevice::NPU) { - matched = true; - } else if (ep_name == kQnnExecutionProvider || ep_name == kVitisAIExecutionProvider) { - matched = true; - } - } - // "fpga" - else if (CaseInsensitiveCompare(target_type_str, "fpga")) { - if (device.Type() == OrtDevice::FPGA) { - matched = true; - } - } - // "cuda" - else if (CaseInsensitiveCompare(target_type_str, "cuda")) { - if (ep_name == kCudaExecutionProvider) { - matched = true; - } - } - // "dml" - else if (CaseInsensitiveCompare(target_type_str, "dml")) { - if (ep_name == kDmlExecutionProvider) { - matched = true; - } - } - // Fallback: Exact EP name string match (e.g. "MyCustomEP") - else if (ep_name == target_full) { - matched = true; - } - - if (matched) { - return ep_name; + if (MatchEpDevice(view, target_type_str, target_specifier, rule.device)) { + return std::string(ep.Type()); } } - return std::nullopt; } @@ -688,6 +593,7 @@ void LayeringIndex::MakeNodeUnassigned(const Graph& graph, NodeIndex node_id) { } } } + } // namespace onnxruntime #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) From ef1227e5d7866409081d8079e70eb115047a788f Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Tue, 10 Mar 2026 11:29:50 -0700 Subject: [PATCH 18/57] Add missing include --- onnxruntime/test/framework/layering_annotations_test.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/onnxruntime/test/framework/layering_annotations_test.cc b/onnxruntime/test/framework/layering_annotations_test.cc index 2a04b6291315f..ce20f0c70473c 100644 --- a/onnxruntime/test/framework/layering_annotations_test.cc +++ b/onnxruntime/test/framework/layering_annotations_test.cc @@ -14,6 +14,7 @@ #include "gtest/gtest.h" #include "test/util/include/asserts.h" +#include "test/util/include/test_environment.h" namespace onnxruntime { namespace test { From b0b23966cf83cefbf3fb846c39e6f937a75162a3 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Tue, 10 Mar 2026 12:33:48 -0700 Subject: [PATCH 19/57] Fix matching bug --- .../core/framework/layering_annotations.cc | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc index 3f2c7b1edcf64..8311c199ef14e 100644 --- a/onnxruntime/core/framework/layering_annotations.cc +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -165,15 +165,19 @@ bool TryParseIndex(const std::string& str, uint32_t& index) { return true; } +// Sentinel value representing an unknown/unavailable device type. +// Used when an OrtEpDevice has neither hardware info nor memory info, +// so we cannot determine the actual device type. +constexpr OrtDevice::DeviceType kDeviceTypeUnknown = static_cast(-1); + // Normalized view of an EP's device properties used by the matching logic. // All fields are non-owning references or value types. struct EpDeviceView { std::string_view ep_name; - OrtDevice::DeviceType device_type; // OrtDevice::CPU, GPU, NPU, FPGA, etc. + OrtDevice::DeviceType device_type; // OrtDevice::CPU, GPU, NPU, FPGA, or kDeviceTypeUnknown uint32_t vendor_id; OrtDevice::DeviceId device_id; std::string_view vendor_string; // from OrtHardwareDevice::vendor (empty if unavailable) - bool has_hardware_info; // true if hardware device info was available }; bool MatchEpDevice(const EpDeviceView& ep, @@ -214,8 +218,11 @@ bool MatchEpDevice(const EpDeviceView& ep, } return false; } - // "accelerator" + // "accelerator" (not cpu) if (CaseInsensitiveCompare(target_type_str, "accelerator")) { + // Match if the EP is not a known CPU provider and its device type + // is not definitively CPU. Unknown device type (no HW/mem info) + // is treated as a potential accelerator. return ep.ep_name != kCpuExecutionProvider && ep.device_type != OrtDevice::CPU; } // "npu" @@ -258,10 +265,11 @@ std::optional EpLayeringMatcher::Match(gsl::span EpLayeringMatcher::Match(gsl::spandevice.Type(); @@ -285,8 +296,7 @@ std::optional EpLayeringMatcher::Match(gsl::spanvendor_id : 0u, has_hw ? static_cast(ep_device.device->device_id) : OrtDevice::DeviceId{}, - has_hw ? std::string_view(ep_device.device->vendor) : std::string_view{}, - has_hw}; + has_hw ? std::string_view(ep_device.device->vendor) : std::string_view{}}; if (MatchEpDevice(view, target_type_str, target_specifier, rule.device)) { return std::string(ep_device.ep_name); @@ -310,8 +320,7 @@ std::optional EpLayeringMatcher::Match(const ExecutionProviders& pr device.Type(), device.Vendor(), device.Id(), - {}, // no vendor string available from IExecutionProvider - true}; // OrtDevice always available + {}}; // no vendor string available from IExecutionProvider if (MatchEpDevice(view, target_type_str, target_specifier, rule.device)) { return std::string(ep.Type()); From b9e13cfa93cd0def4f9aa3f3b883c6a148a0ee75 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Tue, 10 Mar 2026 12:35:10 -0700 Subject: [PATCH 20/57] Change index parsing --- .../core/framework/layering_annotations.cc | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc index 8311c199ef14e..5ed1e3009c5fa 100644 --- a/onnxruntime/core/framework/layering_annotations.cc +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -5,6 +5,7 @@ #include "core/graph/constants.h" #include "core/common/narrow.h" +#include "core/common/parse_string.h" #include "core/common/string_utils.h" #include "core/framework/layering_annotations.h" #include "core/framework/ortmemoryinfo.h" @@ -12,7 +13,6 @@ #include "core/framework/execution_providers.h" #include "core/graph/graph.h" -#include #include namespace onnxruntime { @@ -150,19 +150,8 @@ bool CaseInsensitiveCompare(std::string_view a, std::string_view b) { } bool TryParseIndex(const std::string& str, uint32_t& index) { - if (str.empty() || str[0] == '-') return false; - char* end = nullptr; - const char* ptr = str.c_str(); - errno = 0; - unsigned long val = std::strtoul(ptr, &end, 10); - if (errno != 0 || end != ptr + str.size()) { - return false; - } - if (val > std::numeric_limits::max()) { - return false; - } - index = narrow(val); - return true; + if (str.empty()) return false; + return TryParseStringWithClassicLocale(str, index); } // Sentinel value representing an unknown/unavailable device type. From add0227e2e3d57994f306b7d77434c329374d46d Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Tue, 10 Mar 2026 12:37:39 -0700 Subject: [PATCH 21/57] Remove wrong comment --- onnxruntime/core/framework/layering_annotations.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc index 5ed1e3009c5fa..08c8bb0783b28 100644 --- a/onnxruntime/core/framework/layering_annotations.cc +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -391,7 +391,6 @@ Status LayeringIndex::Create(const Graph& graph, return Status::OK(); } -// Process top to bottom-up assign layering indices to nodes void LayeringIndex::ProcessGraph(const Graph& graph, std::optional parent_layer_id) { // 3. Create entry for this graph instance bool was_updated = false; From 17e35254326e9408a2b9545fa0a59c8761d7a451 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Tue, 10 Mar 2026 13:58:10 -0700 Subject: [PATCH 22/57] Address minimal build issues --- onnxruntime/core/framework/graph_partitioner.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onnxruntime/core/framework/graph_partitioner.cc b/onnxruntime/core/framework/graph_partitioner.cc index c30c064d27ffa..2456033760c40 100644 --- a/onnxruntime/core/framework/graph_partitioner.cc +++ b/onnxruntime/core/framework/graph_partitioner.cc @@ -1413,7 +1413,7 @@ Status GraphPartitioner::Partition(Graph& graph, FuncManager& func_mgr, std::ref(graph), std::cref(check_load_cancellation_fn), std::cref(on_partition_assignment_fn_), - }; + layering_index}; #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) From 1b1a7dbb00626271c51c8ae3c02acdef96d5cc92 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Tue, 10 Mar 2026 14:13:08 -0700 Subject: [PATCH 23/57] Fix unused arg --- onnxruntime/core/framework/graph_partitioner.cc | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/onnxruntime/core/framework/graph_partitioner.cc b/onnxruntime/core/framework/graph_partitioner.cc index 2456033760c40..e99ef29697697 100644 --- a/onnxruntime/core/framework/graph_partitioner.cc +++ b/onnxruntime/core/framework/graph_partitioner.cc @@ -201,8 +201,8 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l InlinedVector filtered_in_nodes; #endif // Helper to create a GraphViewer that filters nodes based on layering_index if present. - auto create_graph_viewer = [&](std::unique_ptr& sub_graph_holder, - std::unique_ptr& viewer) -> Status { + auto create_graph_viewer = [&](std::unique_ptr& out_sub_graph, + std::unique_ptr& out_viewer) -> Status { #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) if (params.layering_index) { assigned_filtered_in_nodes.clear(); @@ -231,15 +231,16 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l filtered_in_nodes.push_back(&node); } } - ORT_RETURN_IF_ERROR(graph_utils::CreateFilteredIndexedGraph(filtered_in_nodes, graph, sub_graph_holder)); - viewer = std::make_unique(graph, *sub_graph_holder); + ORT_RETURN_IF_ERROR(graph_utils::CreateFilteredIndexedGraph(filtered_in_nodes, graph, out_sub_graph)); + out_viewer = std::make_unique(graph, *out_sub_graph); return Status::OK(); } +#else + ORT_UNUSED_PARAMETER(out_sub_graph); #endif - viewer = std::make_unique(graph); + out_viewer = std::make_unique(graph); return Status::OK(); }; - // Helper to un-assign nodes that were assigned to this EP but not claimed by updated capabilities. auto reset_assignment_unclaimed_nodes = [&]() { #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) From 88c2c47991b83ec02663a522c3feb1f318175678 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Wed, 18 Mar 2026 14:55:13 -0700 Subject: [PATCH 24/57] Add logging --- onnxruntime/core/providers/cuda/cuda_execution_provider.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/onnxruntime/core/providers/cuda/cuda_execution_provider.cc b/onnxruntime/core/providers/cuda/cuda_execution_provider.cc index 1c290c46603cc..1dfcf46ae8b17 100644 --- a/onnxruntime/core/providers/cuda/cuda_execution_provider.cc +++ b/onnxruntime/core/providers/cuda/cuda_execution_provider.cc @@ -2965,6 +2965,8 @@ CUDAExecutionProvider::GetCapability(const onnxruntime::GraphViewer& graph, << "CUDA_EP failed to get available GPU memory info. Using info_.gpu_mem_limit instead: " << info_.gpu_mem_limit; } else { memory_threshold = std::min(free_memory, info_.gpu_mem_limit); + LOGS(logger, WARNING) + << "CUDA_EP Using threshold: " << memory_threshold << " Free memory reported: " << free_memory; } } else { memory_threshold = std::get<0>(*threshold); From 9b0b52981bc4a16d54d7b0703e223ff66e374fbf Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 19 Mar 2026 14:11:05 -0700 Subject: [PATCH 25/57] Make sure the annotation is copied on node copy --- onnxruntime/core/graph/graph.cc | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/onnxruntime/core/graph/graph.cc b/onnxruntime/core/graph/graph.cc index 0c3115b9f4207..4cceba77a67e8 100644 --- a/onnxruntime/core/graph/graph.cc +++ b/onnxruntime/core/graph/graph.cc @@ -4382,6 +4382,13 @@ Node& Graph::AddNode(const Node& other) { &other.GetAttributes(), other.Domain()); + // Preserve layering annotation from the source node so that graph transformers + // that reconstruct nodes (or function inlining) retain the EP assignment hint. + const auto& annotation = other.GetLayeringAnnotation(); + if (!annotation.empty()) { + new_node.SetLayeringAnnotation(annotation); + } + return new_node; } @@ -4407,12 +4414,12 @@ Node& Graph::AddNode(const NodeProto& node_proto, &attributes, node_proto.domain()); -#ifndef ORT_MINIMAL_BUILD +#if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) auto maybe_annotation = utils::GetNodeProtoLayeringAnnotation(node_proto); if (maybe_annotation) { new_node.SetLayeringAnnotation(std::move(*maybe_annotation)); } -#endif +#endif // // Perf optimization: temporarily set NodeProto in Node so we don't need to call Node::ToProto prior to // calling onnx::check_node From dab76bc09784a9f4693593c14bacca4f9c5ab1fc Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 19 Mar 2026 14:15:56 -0700 Subject: [PATCH 26/57] Adjust error message --- onnxruntime/core/session/onnxruntime_c_api.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onnxruntime/core/session/onnxruntime_c_api.cc b/onnxruntime/core/session/onnxruntime_c_api.cc index 6838ed5743286..06510a93777ce 100644 --- a/onnxruntime/core/session/onnxruntime_c_api.cc +++ b/onnxruntime/core/session/onnxruntime_c_api.cc @@ -3044,7 +3044,7 @@ ORT_API_STATUS_IMPL(OrtApis::Graph_GetGraphView, _In_ const OrtGraph* src_graph, internal_nodes.push_back(&node); } else { std::ostringstream oss; - oss << "node indexed [" << i << "] appears to be a ModelEditorGraph"; + oss << "node indexed [" << i << "] appears to be a ModelEditorNode"; return OrtApis::CreateStatus(OrtErrorCode::ORT_INVALID_ARGUMENT, oss.str().c_str()); } } From b39a487da29e877c8005afc623af5a3a96dd8b70 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 19 Mar 2026 16:48:30 -0700 Subject: [PATCH 27/57] Copy Annotations when copying nodes and inlining functions --- onnxruntime/core/graph/graph.cc | 20 ++- onnxruntime/test/framework/function_test.cc | 156 ++++++++++++++++++++ 2 files changed, 175 insertions(+), 1 deletion(-) diff --git a/onnxruntime/core/graph/graph.cc b/onnxruntime/core/graph/graph.cc index 4cceba77a67e8..06313191a1278 100644 --- a/onnxruntime/core/graph/graph.cc +++ b/onnxruntime/core/graph/graph.cc @@ -6156,6 +6156,13 @@ Status Graph::InlineFunction(Node& callnode) { base_uniq_identifier.append(callnode.OpType()); const auto uniq_identifier = GenerateNodeName(base_uniq_identifier); + // Capture the parent function node's layering annotation before inlining. + // Inlined nodes that don't already have their own annotation will inherit this. + const std::string parent_annotation = callnode.GetLayeringAnnotation(); + + // Record the current max node index so we can identify newly inlined nodes afterward. + const int max_node_index_before_inline = MaxNodeIndex(); + // Replace a (function-call) node by an inlined graph. if (!callnode.GetFunctionBody()) { // This is the normal use-case: inlining a FunctionProto (representing @@ -6217,13 +6224,24 @@ Status Graph::InlineFunction(Node& callnode) { } } + // Propagate the parent function node's layering annotation to all newly inlined nodes + // that don't already have their own annotation. + if (!parent_annotation.empty()) { + const int max_node_index_after_inline = MaxNodeIndex(); + for (int i = max_node_index_before_inline; i < max_node_index_after_inline; ++i) { + Node* node = GetNode(static_cast(i)); + if (node != nullptr && node->GetLayeringAnnotation().empty()) { + node->SetLayeringAnnotation(parent_annotation); + } + } + } + RemoveNode(callnode.Index()); // std::cout << "Graph after inlining\n\n" << *this << std::endl << std::flush; return Status::OK(); } - void Graph::SetInputs(gsl::span inputs) { graph_inputs_including_initializers_.clear(); graph_inputs_excluding_initializers_.clear(); diff --git a/onnxruntime/test/framework/function_test.cc b/onnxruntime/test/framework/function_test.cc index 699d1b1a2c27a..9e28882b9a65d 100644 --- a/onnxruntime/test/framework/function_test.cc +++ b/onnxruntime/test/framework/function_test.cc @@ -662,5 +662,161 @@ TEST(FunctionTest, Test_GH_issue_16438) { status = session_object.Initialize(); ASSERT_TRUE(status.IsOK()) << status.ErrorMessage(); } + +// Verify that when a function node with a layering annotation is inlined, +// the inlined nodes inherit the parent function node's annotation. +TEST(FunctionTest, InlinedNodesInheritLayeringAnnotation) { + // Parse and build a Model with a local function (multi-node body: Constant + Mul). + ONNX_NAMESPACE::OnnxParser parser(basic_code); + ONNX_NAMESPACE::ModelProto model_proto; + auto parse_status = parser.Parse(model_proto); + ASSERT_TRUE(parse_status.IsOK()) << parse_status.ErrorMessage(); + ASSERT_TRUE(parser.EndOfInput()) << "Extra unparsed input unexpected."; + + auto& logger = DefaultLoggingManager().DefaultLogger(); + std::shared_ptr model; + ASSERT_STATUS_OK(Model::Load(std::move(model_proto), model, nullptr, logger)); + + Graph& graph = model->MainGraph(); + ASSERT_STATUS_OK(graph.Resolve()); + + // Find the function call node (local.myfun) and annotate it. + Node* func_node = nullptr; + for (auto& node : graph.Nodes()) { + if (node.OpType() == "myfun") { + func_node = &node; + break; + } + } + ASSERT_NE(func_node, nullptr) << "Could not find function call node 'myfun'"; + ASSERT_TRUE(func_node->CanBeInlined()); + + const std::string annotation = "TestLayerAnnotation"; + func_node->SetLayeringAnnotation(annotation); + + // Inline the function node. + ASSERT_STATUS_OK(graph.InlineFunction(*func_node)); + ASSERT_STATUS_OK(graph.Resolve()); + + // After inlining, the original function call node is removed and replaced + // by the function body nodes (a Mul node; the Constant becomes an initializer). + // Verify every remaining node inherited the annotation. + int node_count = 0; + for (const auto& node : graph.Nodes()) { + ++node_count; + EXPECT_EQ(node.GetLayeringAnnotation(), annotation) + << "Node '" << node.Name() << "' (op: " << node.OpType() + << ") did not inherit the parent function's layering annotation."; + } + EXPECT_GT(node_count, 0) << "Expected at least one inlined node in the graph."; +} + +// Verify that when a function node with no layering annotation is inlined, +// the inlined nodes remain unannotated. +TEST(FunctionTest, InlinedNodesNoAnnotationWhenParentUnannotated) { + ONNX_NAMESPACE::OnnxParser parser(basic_code); + ONNX_NAMESPACE::ModelProto model_proto; + auto parse_status = parser.Parse(model_proto); + ASSERT_TRUE(parse_status.IsOK()) << parse_status.ErrorMessage(); + ASSERT_TRUE(parser.EndOfInput()) << "Extra unparsed input unexpected."; + + auto& logger = DefaultLoggingManager().DefaultLogger(); + std::shared_ptr model; + ASSERT_STATUS_OK(Model::Load(std::move(model_proto), model, nullptr, logger)); + + Graph& graph = model->MainGraph(); + ASSERT_STATUS_OK(graph.Resolve()); + + Node* func_node = nullptr; + for (auto& node : graph.Nodes()) { + if (node.OpType() == "myfun") { + func_node = &node; + break; + } + } + ASSERT_NE(func_node, nullptr); + // Do NOT set any annotation on the function node. + ASSERT_TRUE(func_node->GetLayeringAnnotation().empty()); + + ASSERT_STATUS_OK(graph.InlineFunction(*func_node)); + ASSERT_STATUS_OK(graph.Resolve()); + + for (const auto& node : graph.Nodes()) { + EXPECT_TRUE(node.GetLayeringAnnotation().empty()) + << "Node '" << node.Name() << "' should not have a layering annotation " + << "when the parent function node was unannotated."; + } +} + +// Verify annotation inheritance with two calls to the same function, +// where each call has a different annotation. +TEST(FunctionTest, InlinedNodesInheritDistinctAnnotationsPerCallSite) { + const char* code = R"( + < + ir_version: 8, + opset_import: [ "" : 16, "local" : 1 ] + > + agraph (float[N] x) => (float[N] y) + { + y1 = local.myfun (x) + y = local.myfun (y1) + } + + < + opset_import: [ "" : 16 ], + domain: "local" + > + myfun (lx) => (ly) { + two = Constant () + ly = Mul (lx, two) + } + )"; + + ONNX_NAMESPACE::OnnxParser parser(code); + ONNX_NAMESPACE::ModelProto model_proto; + auto parse_status = parser.Parse(model_proto); + ASSERT_TRUE(parse_status.IsOK()) << parse_status.ErrorMessage(); + ASSERT_TRUE(parser.EndOfInput()); + + auto& logger = DefaultLoggingManager().DefaultLogger(); + std::shared_ptr model; + ASSERT_STATUS_OK(Model::Load(std::move(model_proto), model, nullptr, logger)); + + Graph& graph = model->MainGraph(); + ASSERT_STATUS_OK(graph.Resolve()); + + // Collect the two function call nodes in graph order. + std::vector func_nodes; + for (auto& node : graph.Nodes()) { + if (node.OpType() == "myfun") { + func_nodes.push_back(&node); + } + } + ASSERT_EQ(func_nodes.size(), 2u); + + // Annotate each call site differently. + func_nodes[0]->SetLayeringAnnotation("AnnotationA"); + func_nodes[1]->SetLayeringAnnotation("AnnotationB"); + + // Inline the first call, then the second. + ASSERT_STATUS_OK(graph.InlineFunction(*func_nodes[0])); + ASSERT_STATUS_OK(graph.InlineFunction(*func_nodes[1])); + ASSERT_STATUS_OK(graph.Resolve()); + + // After inlining both calls, the graph should have nodes from both expansions. + // Each group should carry its respective annotation. + bool found_a = false; + bool found_b = false; + for (const auto& node : graph.Nodes()) { + const auto& ann = node.GetLayeringAnnotation(); + EXPECT_TRUE(ann == "AnnotationA" || ann == "AnnotationB") + << "Node '" << node.Name() << "' has unexpected annotation: '" << ann << "'"; + if (ann == "AnnotationA") found_a = true; + if (ann == "AnnotationB") found_b = true; + } + EXPECT_TRUE(found_a) << "No node found with AnnotationA"; + EXPECT_TRUE(found_b) << "No node found with AnnotationB"; +} + } // namespace test } // namespace onnxruntime From 4e260bce4eeb0880b68d460e707af32f11d8ff77 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 19 Mar 2026 18:13:29 -0700 Subject: [PATCH 28/57] Update LayeringIndex after function inlining --- .../core/framework/graph_partitioner.cc | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/onnxruntime/core/framework/graph_partitioner.cc b/onnxruntime/core/framework/graph_partitioner.cc index e99ef29697697..dec8aea6450d2 100644 --- a/onnxruntime/core/framework/graph_partitioner.cc +++ b/onnxruntime/core/framework/graph_partitioner.cc @@ -760,12 +760,12 @@ static Status PartitionOnnxFormatModelImpl(Graph& graph, FuncManager& func_mgr, } // expand any nodes that have an ONNX function definition but no matching ORT kernel -static Status InlineNodes(Graph& graph, bool& modified_graph) { +static Status InlineNodes(Graph& graph, bool& modified_graph, LayeringIndex* layering_index) { // recurse into nested graphs first so we process from bottom up for (auto& node : graph.Nodes()) { for (auto& entry : node.GetAttributeNameToMutableSubgraphMap()) { Graph* subgraph = entry.second; - ORT_RETURN_IF_ERROR(InlineNodes(*subgraph, modified_graph)); + ORT_RETURN_IF_ERROR(InlineNodes(*subgraph, modified_graph, layering_index)); } } @@ -780,14 +780,37 @@ static Status InlineNodes(Graph& graph, bool& modified_graph) { } } + // Collect new node indices for nodes inlined from annotated parents so we can + // update the LayeringIndex in one batch. + InlinedVector new_node_indices; + for (auto* node : nodes_to_inline) { + // Only track new nodes when the parent has an annotation that will be inherited. + const bool has_annotation = layering_index != nullptr && + !node->GetLayeringAnnotation().empty(); + const int max_before = has_annotation ? graph.MaxNodeIndex() : 0; + ORT_RETURN_IF_ERROR(graph.InlineFunction(*node)); modified_graph = true; + + if (has_annotation) { + const int max_after = graph.MaxNodeIndex(); + for (int i = max_before; i < max_after; ++i) { + if (graph.GetNode(static_cast(i)) != nullptr) { + new_node_indices.push_back(static_cast(i)); + } + } + } + } + + // Update the LayeringIndex so the next partitioning round filters correctly + // for the newly inlined nodes that inherited their parent's annotation. + if (layering_index != nullptr && !new_node_indices.empty()) { + layering_index->Update(graph, new_node_indices); } return Status::OK(); } - static Status InlineFunctionsAOTImpl(const ExecutionProviders& execution_providers, const KernelRegistryManager& kernel_registry_mgr, Graph& graph, @@ -1158,7 +1181,7 @@ static Status PartitionOnnxFormatModel(const PartitionParams& partition_params, // expand any nodes that have an ONNX function definition but no matching ORT kernel. modified_graph = false; - ORT_RETURN_IF_ERROR(InlineNodes(graph, modified_graph)); + ORT_RETURN_IF_ERROR(InlineNodes(graph, modified_graph, partition_params.layering_index)); // Resolve and rerun graph partitioning and inlining if there was a change if (modified_graph) { From 52143507422f214380eac22789456609cbc3beb4 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Mon, 23 Mar 2026 13:50:27 -0700 Subject: [PATCH 29/57] Add intermediate buffers accounting + temp coefficient --- .../core/framework/resource_accountant.cc | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/onnxruntime/core/framework/resource_accountant.cc b/onnxruntime/core/framework/resource_accountant.cc index 9c83e9ffac6ae..b948a3f6ab2ee 100644 --- a/onnxruntime/core/framework/resource_accountant.cc +++ b/onnxruntime/core/framework/resource_accountant.cc @@ -92,7 +92,27 @@ class SizeBasedStatsAccountant : public IResourceAccountant { } } } - return static_cast(total_size); + + // Account for intermediate output tensors when shape info is available. + // GetSizeInBytesFromTensorTypeProto will only succeed when all dims are known + // (static shape) and a valid element type is present, so dynamic outputs are + // naturally skipped. + SafeInt output_size = 0; + for (const auto* output_def : node.OutputDefs()) { + if (!output_def->Exists() || !output_def->HasTensorOrScalarShape()) continue; + const auto* type_proto = output_def->TypeAsProto(); + if (!type_proto || !utils::HasTensorType(*type_proto)) continue; + + size_t size = 0; + if (utils::GetSizeInBytesFromTensorTypeProto<0>(type_proto->tensor_type(), &size).IsOK()) { + output_size += size; + } + } + + // Apply a safety multiplier for workspace/temp allocations we can't see + constexpr size_t kAdHocSafetyMultiplierPercent = 150; // 1.5x + SafeInt estimated = total_size + output_size; + return static_cast(estimated * kAdHocSafetyMultiplierPercent / 100); } } From cd73b56d0080012ce51940d7e1231f9c04a627cb Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Wed, 25 Mar 2026 11:20:39 -0700 Subject: [PATCH 30/57] Address MakeNodeUnassigned feedback --- .../core/framework/layering_annotations.cc | 9 -- .../framework/layering_annotations_test.cc | 118 ++++++++++++++++++ 2 files changed, 118 insertions(+), 9 deletions(-) diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc index 08c8bb0783b28..32343412e8759 100644 --- a/onnxruntime/core/framework/layering_annotations.cc +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -577,15 +577,6 @@ void LayeringIndex::MakeNodeUnassigned(const Graph& graph, NodeIndex node_id) { // partitioning passes no longer reserve this layer for the EP. if (layer_to_nodes_hit->second.empty()) { graph_layering_index.layer_to_node_ids_.erase(layer_to_nodes_hit); - // Update ep_name_to_layering_indices_ to remove this layer index - // from the EP that owned it, making it available for other EPs. - auto rule_to_ep_hit = layering_index_to_ep_name_.find(*layer_idx); - if (rule_to_ep_hit != layering_index_to_ep_name_.end()) { - auto ep_hit = ep_name_to_layering_indices_.find(rule_to_ep_hit->second); - if (ep_hit != ep_name_to_layering_indices_.end()) { - ep_hit->second.erase(*layer_idx); - } - } } } } diff --git a/onnxruntime/test/framework/layering_annotations_test.cc b/onnxruntime/test/framework/layering_annotations_test.cc index ce20f0c70473c..fdde8d060fe7a 100644 --- a/onnxruntime/test/framework/layering_annotations_test.cc +++ b/onnxruntime/test/framework/layering_annotations_test.cc @@ -1018,6 +1018,124 @@ TEST(LayeringRulesTest, FromConfigString_RejectsDuplicateAnnotations) { EXPECT_FALSE(rules.rules[0].prefix_match); EXPECT_TRUE(rules.rules[1].prefix_match); } + +TEST(LayeringIndexTest, MakeNodeUnassigned_PreservesEpRuleMapping) { + // Scenario: All nodes for a rule are unassigned in one graph. + // ep_name_to_layering_indices_ must still contain the rule so that + // sibling subgraphs (or the same graph on a subsequent pass) can still + // use it for filtering. + + // 1. Setup Graph with two nodes, both annotated with the same rule + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* input_arg = &graph.GetOrCreateNodeArg("input", &type_proto); + NodeArg* mid_arg = &graph.GetOrCreateNodeArg("mid", &type_proto); + NodeArg* output_arg = &graph.GetOrCreateNodeArg("output", &type_proto); + + Node& node0 = graph.AddNode("node0", "Abs", "Node 0", {input_arg}, {mid_arg}); + node0.SetLayeringAnnotation("RuleA"); + Node& node1 = graph.AddNode("node1", "Abs", "Node 1", {mid_arg}, {output_arg}); + node1.SetLayeringAnnotation("RuleA"); + + ASSERT_STATUS_OK(graph.Resolve()); + + // 2. Setup Rules: RuleA -> DeviceA + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); // Index 0 + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + + // Both nodes should be assigned + ASSERT_TRUE(index.GetNodeAssignment(graph, node0.Index()).has_value()); + ASSERT_TRUE(index.GetNodeAssignment(graph, node1.Index()).has_value()); + + // 3. Unassign both nodes (simulating EP failing to claim them) + index.MakeNodeUnassigned(graph, node0.Index()); + index.MakeNodeUnassigned(graph, node1.Index()); + + // Nodes should be unassigned + EXPECT_FALSE(index.GetNodeAssignment(graph, node0.Index()).has_value()); + EXPECT_FALSE(index.GetNodeAssignment(graph, node1.Index()).has_value()); + + // 4. CRITICAL: ep_name_to_layering_indices_ must still map DeviceA -> {0} + // so that other graphs/passes can still use this rule for filtering. + auto rules_opt = index.GetLayeringRulesForThisEp("DeviceA"); + ASSERT_TRUE(rules_opt.has_value()) << "EP-to-rule mapping should not be erased when nodes are unassigned"; + EXPECT_EQ(rules_opt->get().count(0), 1u); +} + +TEST(LayeringIndexTest, UpdateAfterFullUnassignment_RestoresVisibility) { + // Scenario: All nodes for a rule are unassigned, then Update() adds + // a new node matching the same rule. The new node must be visible + // to the EP via GetLayeringRulesForThisEp. + + // 1. Setup Graph with one annotated node + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* input_arg = &graph.GetOrCreateNodeArg("input", &type_proto); + NodeArg* output_arg = &graph.GetOrCreateNodeArg("output", &type_proto); + + Node& node0 = graph.AddNode("node0", "Abs", "Node 0", {input_arg}, {output_arg}); + node0.SetLayeringAnnotation("RuleA"); + + ASSERT_STATUS_OK(graph.Resolve()); + + // 2. Setup Rules: RuleA -> DeviceA + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); // Index 0 + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + ASSERT_TRUE(index.GetNodeAssignment(graph, node0.Index()).has_value()); + + // 3. Unassign the only node + index.MakeNodeUnassigned(graph, node0.Index()); + EXPECT_FALSE(index.GetNodeAssignment(graph, node0.Index()).has_value()); + + // 4. Simulate layout transform adding a new node with inherited annotation + NodeArg* new_output_arg = &graph.GetOrCreateNodeArg("new_output", &type_proto); + Node& new_node = graph.AddNode("new_node", "Abs", "New Node", {output_arg}, {new_output_arg}); + new_node.SetLayeringAnnotation("RuleA"); + ASSERT_STATUS_OK(graph.Resolve()); + + // 5. Update index with the new node + std::vector new_nodes = {new_node.Index()}; + index.Update(graph, new_nodes); + + // 6. New node should be assigned to rule 0 + auto assign = index.GetNodeAssignment(graph, new_node.Index()); + ASSERT_TRUE(assign.has_value()); + EXPECT_EQ(*assign, 0u); + + // 7. CRITICAL: The rule must still be visible for DeviceA + auto rules_opt = index.GetLayeringRulesForThisEp("DeviceA"); + ASSERT_TRUE(rules_opt.has_value()) << "EP-to-rule mapping must be intact for Update to be effective"; + EXPECT_EQ(rules_opt->get().count(0), 1u); +} + } // namespace test } // namespace onnxruntime From dfe4d1396e648bd45c881bfc5892a7c1a2bbf538 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Wed, 25 Mar 2026 13:25:19 -0700 Subject: [PATCH 31/57] Address InlineNodes feedback --- .../core/framework/graph_partitioner.cc | 29 +- .../core/framework/layering_annotations.h | 3 + .../framework/layering_annotations_test.cc | 608 +++++++++++++++++- 3 files changed, 633 insertions(+), 7 deletions(-) diff --git a/onnxruntime/core/framework/graph_partitioner.cc b/onnxruntime/core/framework/graph_partitioner.cc index dec8aea6450d2..c8558cd44913a 100644 --- a/onnxruntime/core/framework/graph_partitioner.cc +++ b/onnxruntime/core/framework/graph_partitioner.cc @@ -759,6 +759,7 @@ static Status PartitionOnnxFormatModelImpl(Graph& graph, FuncManager& func_mgr, return Status::OK(); } +// expand any nodes that have an ONNX function definition but no matching ORT kernel // expand any nodes that have an ONNX function definition but no matching ORT kernel static Status InlineNodes(Graph& graph, bool& modified_graph, LayeringIndex* layering_index) { // recurse into nested graphs first so we process from bottom up @@ -785,15 +786,32 @@ static Status InlineNodes(Graph& graph, bool& modified_graph, LayeringIndex* lay InlinedVector new_node_indices; for (auto* node : nodes_to_inline) { - // Only track new nodes when the parent has an annotation that will be inherited. - const bool has_annotation = layering_index != nullptr && - !node->GetLayeringAnnotation().empty(); - const int max_before = has_annotation ? graph.MaxNodeIndex() : 0; + // Check for an effective layering assignment: either from an explicit annotation + // on the node, or from an inherited assignment via the LayeringIndex (e.g., a function + // call node inside an annotated If/Loop subgraph that inherited its parent's rule). + const bool has_explicit_annotation = !node->GetLayeringAnnotation().empty(); + bool has_effective_assignment = has_explicit_annotation; + + if (layering_index != nullptr && !has_explicit_annotation) { + // The node may have an inherited-only assignment with no stored annotation string. + // Materialize the annotation on the node so Graph::InlineFunction propagates it + // to the newly created inlined nodes. + auto rule_idx = layering_index->GetNodeAssignment(graph, node->Index()); + if (rule_idx) { + has_effective_assignment = true; + const auto& rules = layering_index->GetRules(); + if (*rule_idx < rules.rules.size()) { + node->SetLayeringAnnotation(rules.rules[*rule_idx].annotation); + } + } + } + + const int max_before = has_effective_assignment ? graph.MaxNodeIndex() : 0; ORT_RETURN_IF_ERROR(graph.InlineFunction(*node)); modified_graph = true; - if (has_annotation) { + if (has_effective_assignment) { const int max_after = graph.MaxNodeIndex(); for (int i = max_before; i < max_after; ++i) { if (graph.GetNode(static_cast(i)) != nullptr) { @@ -811,6 +829,7 @@ static Status InlineNodes(Graph& graph, bool& modified_graph, LayeringIndex* lay return Status::OK(); } + static Status InlineFunctionsAOTImpl(const ExecutionProviders& execution_providers, const KernelRegistryManager& kernel_registry_mgr, Graph& graph, diff --git a/onnxruntime/core/framework/layering_annotations.h b/onnxruntime/core/framework/layering_annotations.h index 6c6a00c996b94..5d58e9ace2471 100644 --- a/onnxruntime/core/framework/layering_annotations.h +++ b/onnxruntime/core/framework/layering_annotations.h @@ -148,6 +148,9 @@ class LayeringIndex { std::optional>> GetLayeringRulesForThisEp(const std::string& ep_type) const; + // Returns the parsed layering rules + const LayeringRules& GetRules() const noexcept { return rules_; } + // This function returns an index for the Layering rule the node is assigned to if any std::optional GetNodeAssignment(const Graph& graph, NodeIndex node_id) const; diff --git a/onnxruntime/test/framework/layering_annotations_test.cc b/onnxruntime/test/framework/layering_annotations_test.cc index fdde8d060fe7a..f7d91629a08b8 100644 --- a/onnxruntime/test/framework/layering_annotations_test.cc +++ b/onnxruntime/test/framework/layering_annotations_test.cc @@ -1033,6 +1033,8 @@ TEST(LayeringIndexTest, MakeNodeUnassigned_PreservesEpRuleMapping) { DefaultLoggingManager().DefaultLogger()); Graph& graph = model.MainGraph(); + // Create nodes + // Node 0: "AnnotatedNode" -> Annotated with "RuleA" ONNX_NAMESPACE::TypeProto type_proto; type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); NodeArg* input_arg = &graph.GetOrCreateNodeArg("input", &type_proto); @@ -1118,11 +1120,14 @@ TEST(LayeringIndexTest, UpdateAfterFullUnassignment_RestoresVisibility) { // 4. Simulate layout transform adding a new node with inherited annotation NodeArg* new_output_arg = &graph.GetOrCreateNodeArg("new_output", &type_proto); Node& new_node = graph.AddNode("new_node", "Abs", "New Node", {output_arg}, {new_output_arg}); - new_node.SetLayeringAnnotation("RuleA"); + new_node.SetLayeringAnnotation("RuleA"); // Inherits parent's annotation ASSERT_STATUS_OK(graph.Resolve()); + // Record the new node index + NodeIndex new_node_index = new_node.Index(); + // 5. Update index with the new node - std::vector new_nodes = {new_node.Index()}; + std::vector new_nodes = {new_node_index}; index.Update(graph, new_nodes); // 6. New node should be assigned to rule 0 @@ -1136,6 +1141,605 @@ TEST(LayeringIndexTest, UpdateAfterFullUnassignment_RestoresVisibility) { EXPECT_EQ(rules_opt->get().count(0), 1u); } +// ============================================================================ +// Tests for graph_partitioner.cc LayeringIndex integration +// These tests exercise behaviors from GetCapabilityForEP, InlineNodes, and +// the partitioning pipeline when a LayeringIndex is present. +// ============================================================================ + +// Helper to create a simple linear graph: input -> node0 -> node1 -> ... -> output +namespace { + +struct SimpleGraphHelper { + std::unique_ptr model; + Graph* graph = nullptr; + std::vector node_indices; + + static SimpleGraphHelper Create(int num_nodes, const std::string& op_type = "Abs") { + SimpleGraphHelper h; + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + h.model = std::make_unique("test_model", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + h.graph = &h.model->MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + + NodeArg* prev_arg = &h.graph->GetOrCreateNodeArg("input", &type_proto); + + for (int i = 0; i < num_nodes; ++i) { + std::string out_name = (i == num_nodes - 1) ? "output" : "mid_" + std::to_string(i); + NodeArg* out_arg = &h.graph->GetOrCreateNodeArg(out_name, &type_proto); + Node& node = h.graph->AddNode("node_" + std::to_string(i), op_type, + "Node " + std::to_string(i), {prev_arg}, {out_arg}); + h.node_indices.push_back(node.Index()); + prev_arg = out_arg; + } + return h; + } +}; + +LayeringIndex CreateTwoEpIndex(const Graph& graph, + const std::string& ep_a, const std::string& annotation_a, + const std::string& ep_b, const std::string& annotation_b) { + LayeringRules rules; + rules.rules.push_back({ep_a, annotation_a, false}); // Index 0 + rules.rules.push_back({ep_b, annotation_b, false}); // Index 1 + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map[ep_a].insert(0); + ep_map[ep_b].insert(1); + + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = ep_a; + rule_map[1] = ep_b; + + return LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); +} + +} // namespace + +TEST(LayeringIndexPartitionerTest, FilteredGraphViewerExcludesOtherEpNodes) { + // Validates the filtering logic in create_graph_viewer (GetCapabilityForEP): + // When layering_index is present, nodes assigned to other EPs should be excluded + // from the GraphViewer presented to the current EP. + + // Setup: 3-node chain, node0 -> RuleA (DeviceA), node1 -> unannotated, node2 -> RuleB (DeviceB) + auto h = SimpleGraphHelper::Create(3); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + auto* node2 = h.graph->GetNode(h.node_indices[2]); + node0->SetLayeringAnnotation("RuleA"); + node2->SetLayeringAnnotation("RuleB"); + ASSERT_STATUS_OK(h.graph->Resolve()); + + auto index = CreateTwoEpIndex(*h.graph, "DeviceA", "RuleA", "DeviceB", "RuleB"); + + // Verify: From DeviceA's perspective, node2 should be excluded + auto rules_a = index.GetLayeringRulesForThisEp("DeviceA"); + ASSERT_TRUE(rules_a.has_value()); + + // node0 should be assigned to rule 0 (DeviceA) + auto assign0 = index.GetNodeAssignment(*h.graph, h.node_indices[0]); + ASSERT_TRUE(assign0.has_value()); + EXPECT_EQ(*assign0, 0u); + + // node1 should be unassigned (available to any EP) + auto assign1 = index.GetNodeAssignment(*h.graph, h.node_indices[1]); + EXPECT_FALSE(assign1.has_value()); + + // node2 should be assigned to rule 1 (DeviceB) + auto assign2 = index.GetNodeAssignment(*h.graph, h.node_indices[2]); + ASSERT_TRUE(assign2.has_value()); + EXPECT_EQ(*assign2, 1u); + + // Simulate the filtering logic from create_graph_viewer: + // For DeviceA: include nodes with no assignment OR assignment in DeviceA's rules + InlinedVector filtered_for_device_a; + for (auto& node : h.graph->Nodes()) { + auto rule_idx_opt = index.GetNodeAssignment(*h.graph, node.Index()); + bool include = true; + if (rule_idx_opt) { + // Node has assignment - include only if it belongs to DeviceA + if (rules_a->get().count(*rule_idx_opt) == 0) { + include = false; + } + } + if (include) { + filtered_for_device_a.push_back(&node); + } + } + + // DeviceA should see node0 (assigned to it) and node1 (unassigned), but NOT node2 + EXPECT_EQ(filtered_for_device_a.size(), 2u); + bool found_node0 = false, found_node1 = false, found_node2 = false; + for (const auto* n : filtered_for_device_a) { + if (n->Index() == h.node_indices[0]) found_node0 = true; + if (n->Index() == h.node_indices[1]) found_node1 = true; + if (n->Index() == h.node_indices[2]) found_node2 = true; + } + EXPECT_TRUE(found_node0) << "DeviceA's assigned node should be included"; + EXPECT_TRUE(found_node1) << "Unassigned node should be included for any EP"; + EXPECT_FALSE(found_node2) << "DeviceB's assigned node should be excluded from DeviceA's view"; +} + +TEST(LayeringIndexPartitionerTest, FilteredGraphViewerForDeviceBExcludesDeviceANodes) { + // Mirror of the above test but from DeviceB's perspective. + + auto h = SimpleGraphHelper::Create(3); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + auto* node2 = h.graph->GetNode(h.node_indices[2]); + node0->SetLayeringAnnotation("RuleA"); + node2->SetLayeringAnnotation("RuleB"); + ASSERT_STATUS_OK(h.graph->Resolve()); + + auto index = CreateTwoEpIndex(*h.graph, "DeviceA", "RuleA", "DeviceB", "RuleB"); + + auto rules_b = index.GetLayeringRulesForThisEp("DeviceB"); + ASSERT_TRUE(rules_b.has_value()); + + // Simulate filtering for DeviceB + InlinedVector filtered_for_device_b; + for (auto& node : h.graph->Nodes()) { + auto rule_idx_opt = index.GetNodeAssignment(*h.graph, node.Index()); + bool include = true; + if (rule_idx_opt) { + if (rules_b->get().count(*rule_idx_opt) == 0) { + include = false; + } + } + if (include) { + filtered_for_device_b.push_back(&node); + } + } + + // DeviceB should see node1 (unassigned) and node2 (assigned to it), but NOT node0 + EXPECT_EQ(filtered_for_device_b.size(), 2u); + bool found_node0 = false, found_node1 = false, found_node2 = false; + for (const auto* n : filtered_for_device_b) { + if (n->Index() == h.node_indices[0]) found_node0 = true; + if (n->Index() == h.node_indices[1]) found_node1 = true; + if (n->Index() == h.node_indices[2]) found_node2 = true; + } + EXPECT_FALSE(found_node0) << "DeviceA's assigned node should be excluded from DeviceB's view"; + EXPECT_TRUE(found_node1) << "Unassigned node should be included for any EP"; + EXPECT_TRUE(found_node2) << "DeviceB's assigned node should be included"; +} + +TEST(LayeringIndexPartitionerTest, ResetUnclaimedNodesRemovesAssignment) { + // Validates the reset_assignment_unclaimed_nodes logic: + // Nodes that were pre-assigned to an EP via layering but NOT claimed in capabilities + // should be unassigned so subsequent EPs can pick them up. + + auto h = SimpleGraphHelper::Create(3); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + auto* node1 = h.graph->GetNode(h.node_indices[1]); + auto* node2 = h.graph->GetNode(h.node_indices[2]); + node0->SetLayeringAnnotation("RuleA"); + node1->SetLayeringAnnotation("RuleA"); + node2->SetLayeringAnnotation("RuleA"); + ASSERT_STATUS_OK(h.graph->Resolve()); + + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); // Index 0 + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + + auto index = LayeringIndex::Create(*h.graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + + // All 3 nodes should be assigned initially + ASSERT_TRUE(index.GetNodeAssignment(*h.graph, h.node_indices[0]).has_value()); + ASSERT_TRUE(index.GetNodeAssignment(*h.graph, h.node_indices[1]).has_value()); + ASSERT_TRUE(index.GetNodeAssignment(*h.graph, h.node_indices[2]).has_value()); + + // Simulate: EP only claims node0 and node2 (not node1) + InlinedHashSet claimed; + claimed.insert(h.node_indices[0]); + claimed.insert(h.node_indices[2]); + + auto ep_rules_opt = index.GetLayeringRulesForThisEp("DeviceA"); + ASSERT_TRUE(ep_rules_opt.has_value()); + const auto& ep_rules = ep_rules_opt->get(); + + // Replicate reset_assignment_unclaimed_nodes logic: + // For each assigned-filtered-in node, if not claimed, unassign it + std::vector assigned_filtered_in = {h.node_indices[0], h.node_indices[1], h.node_indices[2]}; + for (auto node_index : assigned_filtered_in) { + if (claimed.count(node_index) == 0) { + auto rule_idx_opt = index.GetNodeAssignment(*h.graph, node_index); + if (rule_idx_opt && ep_rules.count(*rule_idx_opt) > 0) { + index.MakeNodeUnassigned(*h.graph, node_index); + } + } + } + + // node0 and node2 should still be assigned + EXPECT_TRUE(index.GetNodeAssignment(*h.graph, h.node_indices[0]).has_value()); + EXPECT_TRUE(index.GetNodeAssignment(*h.graph, h.node_indices[2]).has_value()); + // node1 should be unassigned (not claimed by EP) + EXPECT_FALSE(index.GetNodeAssignment(*h.graph, h.node_indices[1]).has_value()); +} + +TEST(LayeringIndexPartitionerTest, UpdateAfterLayoutTransformAddsNewNodes) { + // Validates the LayeringIndex update after layout transformation creates new nodes. + // In GetCapabilityForEP, after layout transform, new nodes with inherited annotations + // are added and the index is updated. + + auto h = SimpleGraphHelper::Create(2); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + node0->SetLayeringAnnotation("RuleA"); + ASSERT_STATUS_OK(h.graph->Resolve()); + + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + + auto index = LayeringIndex::Create(*h.graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + + // Record the max node index before "layout transformation" + const NodeIndex first_new_node = h.graph->MaxNodeIndex(); + + // Simulate layout transformation adding new nodes with inherited annotation + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* extra_out = &h.graph->GetOrCreateNodeArg("extra_output", &type_proto); + NodeArg* output_arg = &h.graph->GetOrCreateNodeArg("output", nullptr); // reuse existing + Node& new_node = h.graph->AddNode("layout_inserted_node", "Abs", "Layout inserted", + {output_arg}, {extra_out}); + new_node.SetLayeringAnnotation("RuleA"); // Inherits parent's annotation + ASSERT_STATUS_OK(h.graph->Resolve()); + + const NodeIndex end_node = h.graph->MaxNodeIndex(); + + // Collect new node indices (as done in graph_partitioner.cc) + InlinedVector new_node_indices; + for (NodeIndex idx = first_new_node; idx < end_node; ++idx) { + if (h.graph->GetNode(idx) != nullptr) { + new_node_indices.push_back(idx); + } + } + + // Update index + ASSERT_FALSE(new_node_indices.empty()); + index.Update(*h.graph, new_node_indices); + + // New node should now be assigned to rule 0 (DeviceA) + auto assign = index.GetNodeAssignment(*h.graph, new_node.Index()); + ASSERT_TRUE(assign.has_value()); + EXPECT_EQ(*assign, 0u); +} + +TEST(LayeringIndexPartitionerTest, UpdateWithUnannotatedNewNodeRemainsUnassigned) { + // New nodes created by layout transform that do NOT have annotations + // should remain unassigned after Update. + + auto h = SimpleGraphHelper::Create(1); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + node0->SetLayeringAnnotation("RuleA"); + ASSERT_STATUS_OK(h.graph->Resolve()); + + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + + auto index = LayeringIndex::Create(*h.graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + + // Add a new node WITHOUT annotation + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* extra_out = &h.graph->GetOrCreateNodeArg("extra_output", &type_proto); + NodeArg* output_arg = &h.graph->GetOrCreateNodeArg("output", nullptr); + Node& new_node = h.graph->AddNode("unannotated_node", "Abs", "No annotation", + {output_arg}, {extra_out}); + // Deliberately NOT setting annotation + ASSERT_STATUS_OK(h.graph->Resolve()); + + std::vector new_nodes = {new_node.Index()}; + index.Update(*h.graph, new_nodes); + + // New node should remain unassigned + auto assign = index.GetNodeAssignment(*h.graph, new_node.Index()); + EXPECT_FALSE(assign.has_value()); +} + +TEST(LayeringIndexPartitionerTest, InlineAnnotationMaterialization) { + // Validates the InlineNodes logic where a node has an inherited-only assignment + // (no explicit annotation string) and the annotation is materialized before inlining. + // This tests the code path: + // if (layering_index != nullptr && !has_explicit_annotation) { + // auto rule_idx = layering_index->GetNodeAssignment(graph, node->Index()); + // if (rule_idx) { ... node->SetLayeringAnnotation(rules.rules[*rule_idx].annotation); } + // } + + // Setup: A graph where a node is assigned via inheritance (subgraph scenario) + // but has no explicit annotation string on it. + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* input_arg = &graph.GetOrCreateNodeArg("input", &type_proto); + NodeArg* output_arg = &graph.GetOrCreateNodeArg("output", &type_proto); + + // Create a node without explicit annotation + Node& node = graph.AddNode("inherited_node", "Abs", "Node with inherited assignment", + {input_arg}, {output_arg}); + ASSERT_STATUS_OK(graph.Resolve()); + + // Create index where the node is somehow assigned (e.g., through inheritance) + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA", false}); // Index 0 + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + + // The node has no annotation, so it shouldn't be assigned yet + ASSERT_TRUE(node.GetLayeringAnnotation().empty()); + EXPECT_FALSE(index.GetNodeAssignment(graph, node.Index()).has_value()); + + // Now simulate what InlineNodes does: manually annotate and update + // This simulates the case where GetNodeAssignment returns a value + // for a node in a subgraph that inherited its parent's assignment. + node.SetLayeringAnnotation("RuleA"); + std::vector updated = {node.Index()}; + index.Update(graph, updated); + + // After materialization + update, the node should be properly assigned + auto assign = index.GetNodeAssignment(graph, node.Index()); + ASSERT_TRUE(assign.has_value()); + EXPECT_EQ(*assign, 0u); + + // And the annotation string should be on the node + EXPECT_EQ(node.GetLayeringAnnotation(), "RuleA"); +} + +TEST(LayeringIndexPartitionerTest, UpdateBatchMultipleNewAnnotatedNodes) { + // Tests that Update correctly handles a batch of multiple new nodes, + // some annotated with different rules. This mirrors the behavior after + // layout transformation creates several new nodes. + + auto h = SimpleGraphHelper::Create(1); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + node0->SetLayeringAnnotation("RuleA"); + ASSERT_STATUS_OK(h.graph->Resolve()); + + auto index = CreateTwoEpIndex(*h.graph, "DeviceA", "RuleA", "DeviceB", "RuleB"); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + + // Add 3 new nodes: one for RuleA, one for RuleB, one unannotated + NodeArg* out1 = &h.graph->GetOrCreateNodeArg("new_out1", &type_proto); + NodeArg* out2 = &h.graph->GetOrCreateNodeArg("new_out2", &type_proto); + NodeArg* out3 = &h.graph->GetOrCreateNodeArg("new_out3", &type_proto); + NodeArg* output = &h.graph->GetOrCreateNodeArg("output", nullptr); + + Node& new_a = h.graph->AddNode("new_a", "Abs", "", {output}, {out1}); + new_a.SetLayeringAnnotation("RuleA"); + + Node& new_b = h.graph->AddNode("new_b", "Abs", "", {out1}, {out2}); + new_b.SetLayeringAnnotation("RuleB"); + + Node& new_none = h.graph->AddNode("new_none", "Abs", "", {out2}, {out3}); + // No annotation + + ASSERT_STATUS_OK(h.graph->Resolve()); + + std::vector new_nodes = {new_a.Index(), new_b.Index(), new_none.Index()}; + index.Update(*h.graph, new_nodes); + + // new_a -> RuleA -> rule index 0 + auto assign_a = index.GetNodeAssignment(*h.graph, new_a.Index()); + ASSERT_TRUE(assign_a.has_value()); + EXPECT_EQ(*assign_a, 0u); + + // new_b -> RuleB -> rule index 1 + auto assign_b = index.GetNodeAssignment(*h.graph, new_b.Index()); + ASSERT_TRUE(assign_b.has_value()); + EXPECT_EQ(*assign_b, 1u); + + // new_none -> unassigned + auto assign_none = index.GetNodeAssignment(*h.graph, new_none.Index()); + EXPECT_FALSE(assign_none.has_value()); +} + +TEST(LayeringIndexPartitionerTest, MakeUnassignedThenReassignViaPrefixRule) { + // Test that prefix rules work correctly after unassign+update cycle. + // This covers the interaction between MakeNodeUnassigned, prefix matching, + // and Update. + + std::unordered_map domain_to_version; + domain_to_version[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), IOnnxRuntimeOpSchemaRegistryList(), + domain_to_version, std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto type_proto; + type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + NodeArg* input_arg = &graph.GetOrCreateNodeArg("input", &type_proto); + NodeArg* output_arg = &graph.GetOrCreateNodeArg("output", &type_proto); + + Node& node = graph.AddNode("node", "Abs", "Node", {input_arg}, {output_arg}); + node.SetLayeringAnnotation("Layer_GPU_Compute"); + ASSERT_STATUS_OK(graph.Resolve()); + + // Prefix rule: "Layer_GPU" matches "Layer_GPU_Compute" + LayeringRules rules; + rules.rules.push_back({"GPUDevice", "Layer_GPU", true}); // Index 0, prefix match + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["GPUDevice"].insert(0); + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "GPUDevice"; + + auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + + // Node should be assigned via prefix match + auto assign = index.GetNodeAssignment(graph, node.Index()); + ASSERT_TRUE(assign.has_value()); + EXPECT_EQ(*assign, 0u); + + // Unassign the node + index.MakeNodeUnassigned(graph, node.Index()); + EXPECT_FALSE(index.GetNodeAssignment(graph, node.Index()).has_value()); + + // Add a new node with a different annotation that also matches the prefix + NodeArg* new_out = &graph.GetOrCreateNodeArg("new_output", &type_proto); + Node& new_node = graph.AddNode("new_node", "Abs", "", {output_arg}, {new_out}); + new_node.SetLayeringAnnotation("Layer_GPU_Memory"); + ASSERT_STATUS_OK(graph.Resolve()); + + std::vector new_nodes = {new_node.Index()}; + index.Update(graph, new_nodes); + + // New node should also be assigned via prefix match + auto new_assign = index.GetNodeAssignment(graph, new_node.Index()); + ASSERT_TRUE(new_assign.has_value()); + EXPECT_EQ(*new_assign, 0u); +} + +TEST(LayeringIndexPartitionerTest, NoLayeringIndexAllNodesVisible) { + // When layering_index is nullptr (no layering configuration), + // all nodes should be visible to all EPs. This verifies the baseline + // behavior that the filtering code path is only active when layering is enabled. + + auto h = SimpleGraphHelper::Create(3); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + auto* node2 = h.graph->GetNode(h.node_indices[2]); + + // Even if nodes have annotations, without a LayeringIndex, everything is visible + node0->SetLayeringAnnotation("RuleA"); + node2->SetLayeringAnnotation("RuleB"); + ASSERT_STATUS_OK(h.graph->Resolve()); + + // Without LayeringIndex, a standard GraphViewer should see all nodes + GraphViewer viewer(*h.graph); + EXPECT_EQ(viewer.NumberOfNodes(), 3); + + // All nodes accessible + EXPECT_NE(viewer.GetNode(h.node_indices[0]), nullptr); + EXPECT_NE(viewer.GetNode(h.node_indices[1]), nullptr); + EXPECT_NE(viewer.GetNode(h.node_indices[2]), nullptr); +} + +TEST(LayeringIndexPartitionerTest, EpWithNoLayeringRulesSeesAllUnassignedNodes) { + // An EP that has no rules in the LayeringIndex (i.e., GetLayeringRulesForThisEp returns nullopt) + // should see all unassigned nodes but not nodes assigned to other EPs. + // This is the behavior when a CPU fallback EP is not mentioned in layering config. + + auto h = SimpleGraphHelper::Create(4); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + auto* node2 = h.graph->GetNode(h.node_indices[2]); + node0->SetLayeringAnnotation("RuleA"); + node2->SetLayeringAnnotation("RuleB"); + // node1 and node3 are unannotated + ASSERT_STATUS_OK(h.graph->Resolve()); + + auto index = CreateTwoEpIndex(*h.graph, "DeviceA", "RuleA", "DeviceB", "RuleB"); + + // "CPUDevice" has no rules in the index + auto rules_cpu = index.GetLayeringRulesForThisEp("CPUDevice"); + EXPECT_FALSE(rules_cpu.has_value()); + + // In the partitioner, when GetLayeringRulesForThisEp returns nullopt, + // the create_graph_viewer lambda creates a standard (unfiltered) GraphViewer. + // This means CPUDevice sees ALL nodes including those assigned to other EPs. + // This is by design: the layering filtering only activates for EPs that have rules. + GraphViewer viewer(*h.graph); + EXPECT_EQ(viewer.NumberOfNodes(), 4); +} + +TEST(LayeringIndexPartitionerTest, MultipleRulesForSameEp) { + // An EP can have multiple rules assigned to it. All nodes matching any of its + // rules should be visible to it, while nodes matching other EP rules should not. + + auto h = SimpleGraphHelper::Create(4); + auto* node0 = h.graph->GetNode(h.node_indices[0]); + auto* node1 = h.graph->GetNode(h.node_indices[1]); + auto* node2 = h.graph->GetNode(h.node_indices[2]); + + node0->SetLayeringAnnotation("RuleA1"); + node1->SetLayeringAnnotation("RuleA2"); + node2->SetLayeringAnnotation("RuleB"); + // node3 unannotated + ASSERT_STATUS_OK(h.graph->Resolve()); + + // DeviceA has two rules: RuleA1 (index 0) and RuleA2 (index 1) + // DeviceB has one rule: RuleB (index 2) + LayeringRules rules; + rules.rules.push_back({"DeviceA", "RuleA1", false}); // Index 0 + rules.rules.push_back({"DeviceA", "RuleA2", false}); // Index 1 + rules.rules.push_back({"DeviceB", "RuleB", false}); // Index 2 + + LayeringIndex::EpNameToLayeringIndices ep_map; + ep_map["DeviceA"].insert(0); + ep_map["DeviceA"].insert(1); + ep_map["DeviceB"].insert(2); + + LayeringIndex::LayeringIndexToEpName rule_map; + rule_map[0] = "DeviceA"; + rule_map[1] = "DeviceA"; + rule_map[2] = "DeviceB"; + + auto index = LayeringIndex::Create(*h.graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + + auto rules_a = index.GetLayeringRulesForThisEp("DeviceA"); + ASSERT_TRUE(rules_a.has_value()); + EXPECT_EQ(rules_a->get().size(), 2u); // Both rule indices 0 and 1 + + // Simulate filtering for DeviceA + InlinedVector filtered_for_a; + for (auto& node : h.graph->Nodes()) { + auto rule_idx_opt = index.GetNodeAssignment(*h.graph, node.Index()); + bool include = true; + if (rule_idx_opt) { + if (rules_a->get().count(*rule_idx_opt) == 0) { + include = false; + } + } + if (include) { + filtered_for_a.push_back(&node); + } + } + + // DeviceA should see node0, node1 (both its rules), and node3 (unassigned) = 3 nodes + // node2 (RuleB/DeviceB) should be excluded + EXPECT_EQ(filtered_for_a.size(), 3u); + + bool found[4] = {}; + for (const auto* n : filtered_for_a) { + for (int i = 0; i < 4; ++i) { + if (n->Index() == h.node_indices[i]) found[i] = true; + } + } + EXPECT_TRUE(found[0]); // node0 - RuleA1 + EXPECT_TRUE(found[1]); // node1 - RuleA2 + EXPECT_FALSE(found[2]); // node2 - RuleB (excluded) + EXPECT_TRUE(found[3]); // node3 - unassigned +} + } // namespace test } // namespace onnxruntime From 62f3d1149c1d756f32b125d4fe3ae156dde73b87 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Wed, 25 Mar 2026 14:36:07 -0700 Subject: [PATCH 32/57] Fix underaccounting for shared weights in fused nodes --- .../core/graph/indexed_sub_graph.h | 11 + .../core/framework/graph_partitioner.cc | 7 +- .../framework/resource_accountant_test.cc | 301 ++++++++++++++++++ 3 files changed, 314 insertions(+), 5 deletions(-) create mode 100644 onnxruntime/test/framework/resource_accountant_test.cc diff --git a/include/onnxruntime/core/graph/indexed_sub_graph.h b/include/onnxruntime/core/graph/indexed_sub_graph.h index 8ef4fdb66e1e6..b4f7f1f176715 100644 --- a/include/onnxruntime/core/graph/indexed_sub_graph.h +++ b/include/onnxruntime/core/graph/indexed_sub_graph.h @@ -93,6 +93,17 @@ struct IndexedSubGraph { resource_accountant->AddConsumedAmount(nodes_costs[cost_index]); } + // Accounts for all constituent nodes by summing their pre-stored costs. + // Use this when fusing nodes into a single node so the total cost + // reflects what was computed during GetCapability() (with correct + // cross-node weight deduplication already applied). + void AccountForAllNodes() const { + assert(resource_accountant != nullptr); + for (const auto& cost : nodes_costs) { + resource_accountant->AddConsumedAmount(cost); + } + } + // This computes and accounts for the resource cost for the node that just // been fused from other nodes, and the EP did not had a chance to compute the costs. void ComputeAndAccountForNode(const Node& node) const { diff --git a/onnxruntime/core/framework/graph_partitioner.cc b/onnxruntime/core/framework/graph_partitioner.cc index c8558cd44913a..baa6b403fa3a5 100644 --- a/onnxruntime/core/framework/graph_partitioner.cc +++ b/onnxruntime/core/framework/graph_partitioner.cc @@ -1285,7 +1285,7 @@ static Status PartitionOrtFormatModelImpl(const PartitionParams& partition_param Node& fused_node = graph.BeginFuseSubGraph(indexed_sub_graph, node_name); fused_node.SetExecutionProviderType(type); if (indexed_sub_graph.IsAccountingEnabled()) { - indexed_sub_graph.ComputeAndAccountForNode(fused_node); + indexed_sub_graph.AccountForAllNodes(); } // create filtered graph viewer for this set of nodes @@ -1304,7 +1304,6 @@ static Status PartitionOrtFormatModelImpl(const PartitionParams& partition_param #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) // We will compile the fused nodes one by one, and fuse the subgraph if successful. for (const auto& compilation_entry : compilation_entries) { - const bool acc_enabled = compilation_entry.capability.get().sub_graph->IsAccountingEnabled(); Node& node = compilation_entry.fused_node; std::vector single_node_compute_func; ORT_RETURN_IF_ERROR(current_ep.Compile({IExecutionProvider::FusedNodeAndGraph{node, *compilation_entry.viewer}}, @@ -1335,9 +1334,7 @@ static Status PartitionOrtFormatModelImpl(const PartitionParams& partition_param // now that we're done compiling we can remove the original nodes from the Graph and wire in the new one graph.FinalizeFuseSubGraph(indexed_sub_graph, node); - if (acc_enabled) { - compilation_entry.capability.get().sub_graph->ComputeAndAccountForNode(node); - } + // accounting was already done via AccountForAllNodes() when the fused node was created above. } #endif // !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) diff --git a/onnxruntime/test/framework/resource_accountant_test.cc b/onnxruntime/test/framework/resource_accountant_test.cc new file mode 100644 index 0000000000000..074b1567684af --- /dev/null +++ b/onnxruntime/test/framework/resource_accountant_test.cc @@ -0,0 +1,301 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#include "core/framework/resource_accountant.h" +#include "core/graph/indexed_sub_graph.h" +#include "core/graph/constants.h" +#include "core/graph/model.h" + +#include "gtest/gtest.h" + +#include "test/util/include/asserts.h" +#include "test/util/include/test_environment.h" + +namespace onnxruntime { +namespace test { + +// Test accountant mimicking SizeBasedStatsAccountant ad-hoc path: +// ComputeResourceCount inserts weight names into a dedup set so +// calling it again on a node whose weights were already seen +// returns 0 for those weights. +class TestDedupAccountant : public IResourceAccountant { + public: + TestDedupAccountant() = default; + + ResourceCount GetConsumedAmount() const override { + return consumed_; + } + + void AddConsumedAmount(const ResourceCount& amount) noexcept override { + if (std::holds_alternative(amount)) { + consumed_ += std::get(amount); + } + } + + void RemoveConsumedAmount(const ResourceCount& amount) noexcept override { + if (std::holds_alternative(amount)) { + consumed_ -= std::get(amount); + } + } + + ResourceCount ComputeResourceCount(const Node& node) override { + const auto* graph = node.GetContainingGraph(); + if (graph == nullptr) { + return static_cast(0); + } + + size_t total = 0; + for (const auto* input_def : node.InputDefs()) { + if (!input_def->Exists()) { + continue; + } + const auto& name = input_def->Name(); + constexpr bool check_outer_scope = true; + const auto* init = graph->GetInitializer(name, check_outer_scope); + if (init != nullptr) { + if (seen_weights_.count(name) > 0) { + continue; + } + auto it = weight_sizes_.find(name); + if (it != weight_sizes_.end()) { + total += it->second; + } + seen_weights_.insert(name); + } + } + return total; + } + + void RegisterWeight(const std::string& name, size_t size) { + weight_sizes_[name] = size; + } + + size_t GetConsumedSizeT() const { return consumed_; } + + private: + size_t consumed_ = 0; + InlinedHashSet seen_weights_; + InlinedHashMap weight_sizes_; +}; + +// Two Add nodes that share a single initializer weight_W. +struct SharedWeightGraph { + std::unique_ptr model; + Graph* graph = nullptr; + Node* node_a = nullptr; + Node* node_b = nullptr; + + static SharedWeightGraph Create() { + SharedWeightGraph h; + std::unordered_map dom; + dom[kOnnxDomain] = 12; + h.model = std::make_unique( + "test_model", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), dom, + std::vector(), + DefaultLoggingManager().DefaultLogger()); + h.graph = &h.model->MainGraph(); + + ONNX_NAMESPACE::TypeProto ft; + ft.mutable_tensor_type()->set_elem_type( + ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + ft.mutable_tensor_type()->mutable_shape()->add_dim()->set_dim_value(250); + + ONNX_NAMESPACE::TensorProto wp; + wp.set_name("weight_W"); + wp.set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + wp.add_dims(250); + for (int i = 0; i < 250; ++i) { + wp.add_float_data(0.0f); + } + h.graph->AddInitializedTensor(wp); + + auto* ia = &h.graph->GetOrCreateNodeArg("input_a", &ft); + auto* ib = &h.graph->GetOrCreateNodeArg("input_b", &ft); + auto* wa = &h.graph->GetOrCreateNodeArg("weight_W", &ft); + auto* oa = &h.graph->GetOrCreateNodeArg("out_a", &ft); + auto* ob = &h.graph->GetOrCreateNodeArg("out_b", &ft); + + h.node_a = &h.graph->AddNode("node_A", "Add", "A", {ia, wa}, {oa}); + h.node_b = &h.graph->AddNode("node_B", "Add", "B", {ib, wa}, {ob}); + + auto status = h.graph->Resolve(); + ORT_ENFORCE(status.IsOK(), status.ErrorMessage()); + return h; + } +}; + +// Regression: AccountForAllNodes sums pre-stored per-node costs +// that already have correct within-pass weight deduplication. +TEST(ResourceAccountantTest, AccountForAllNodes_CorrectlyUsesPreStoredCosts) { + auto h = SharedWeightGraph::Create(); + TestDedupAccountant accountant; + accountant.RegisterWeight("weight_W", 1000); + + IndexedSubGraph sub_graph; + sub_graph.nodes.push_back(h.node_a->Index()); + sub_graph.nodes.push_back(h.node_b->Index()); + sub_graph.SetAccountant(&accountant); + + auto cost_a = accountant.ComputeResourceCount(*h.node_a); + sub_graph.AppendNodeCost(cost_a); + EXPECT_EQ(std::get(cost_a), size_t{1000}); + + auto cost_b = accountant.ComputeResourceCount(*h.node_b); + sub_graph.AppendNodeCost(cost_b); + EXPECT_EQ(std::get(cost_b), size_t{0}); + + ASSERT_TRUE(sub_graph.IsAccountingEnabled()); + sub_graph.AccountForAllNodes(); + + EXPECT_EQ(accountant.GetConsumedSizeT(), size_t{1000}) + << "AccountForAllNodes should sum pre-stored costs (1000 + 0)"; +} + +// Demonstrates the bug: after probing populates the dedup set, +// ComputeAndAccountForNode on a fused node returns 0. +TEST(ResourceAccountantTest, ComputeAndAccountForNode_UnderCountsAfterProbing) { + auto h = SharedWeightGraph::Create(); + TestDedupAccountant accountant; + accountant.RegisterWeight("weight_W", 1000); + + // Probing pass populates the dedup set + auto cost_a = accountant.ComputeResourceCount(*h.node_a); + EXPECT_EQ(std::get(cost_a), size_t{1000}); + auto cost_b = accountant.ComputeResourceCount(*h.node_b); + EXPECT_EQ(std::get(cost_b), size_t{0}); + + // Old buggy path: re-compute on fused node after probing + IndexedSubGraph sub_graph; + sub_graph.nodes.push_back(h.node_a->Index()); + sub_graph.SetAccountant(&accountant); + sub_graph.ComputeAndAccountForNode(*h.node_a); + + // Bug: consumed is 0 instead of 1000 + EXPECT_EQ(accountant.GetConsumedSizeT(), size_t{0}) + << "ComputeAndAccountForNode after probing under-counts"; +} + +// Each node has a unique initializer. AccountForAllNodes sums both. +TEST(ResourceAccountantTest, AccountForAllNodes_NoSharedWeights) { + std::unordered_map dom; + dom[kOnnxDomain] = 12; + Model model("test_model", false, ModelMetaData(), PathString(), + IOnnxRuntimeOpSchemaRegistryList(), dom, + std::vector(), + DefaultLoggingManager().DefaultLogger()); + Graph& graph = model.MainGraph(); + + ONNX_NAMESPACE::TypeProto ft; + ft.mutable_tensor_type()->set_elem_type( + ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + ft.mutable_tensor_type()->mutable_shape()->add_dim()->set_dim_value(100); + + const char* names[] = {"weight_1", "weight_2"}; + for (const char* wn : names) { + ONNX_NAMESPACE::TensorProto tp; + tp.set_name(wn); + tp.set_data_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); + tp.add_dims(100); + for (int i = 0; i < 100; ++i) { + tp.add_float_data(0.0f); + } + graph.AddInitializedTensor(tp); + } + + auto* input = &graph.GetOrCreateNodeArg("input", &ft); + auto* w1 = &graph.GetOrCreateNodeArg("weight_1", &ft); + auto* w2 = &graph.GetOrCreateNodeArg("weight_2", &ft); + auto* out1 = &graph.GetOrCreateNodeArg("out1", &ft); + auto* out2 = &graph.GetOrCreateNodeArg("out2", &ft); + + auto& node1 = graph.AddNode("n1", "Add", "", {input, w1}, {out1}); + auto& node2 = graph.AddNode("n2", "Add", "", {out1, w2}, {out2}); + ASSERT_STATUS_OK(graph.Resolve()); + + TestDedupAccountant accountant; + accountant.RegisterWeight("weight_1", 400); + accountant.RegisterWeight("weight_2", 600); + + IndexedSubGraph sub_graph; + sub_graph.nodes.push_back(node1.Index()); + sub_graph.nodes.push_back(node2.Index()); + sub_graph.SetAccountant(&accountant); + + sub_graph.AppendNodeCost(accountant.ComputeResourceCount(node1)); + sub_graph.AppendNodeCost(accountant.ComputeResourceCount(node2)); + + ASSERT_TRUE(sub_graph.IsAccountingEnabled()); + sub_graph.AccountForAllNodes(); + + EXPECT_EQ(accountant.GetConsumedSizeT(), size_t{1000}) + << "No shared weights: should sum all costs (400 + 600)"; +} + +// AccountForNode per-node and AccountForAllNodes bulk produce same result. +TEST(ResourceAccountantTest, AccountForNode_MatchesAccountForAllNodes) { + auto h = SharedWeightGraph::Create(); + + // Per-node path + TestDedupAccountant acc1; + acc1.RegisterWeight("weight_W", 1000); + IndexedSubGraph sub1; + sub1.nodes.push_back(h.node_a->Index()); + sub1.nodes.push_back(h.node_b->Index()); + sub1.SetAccountant(&acc1); + sub1.AppendNodeCost(acc1.ComputeResourceCount(*h.node_a)); + sub1.AppendNodeCost(acc1.ComputeResourceCount(*h.node_b)); + sub1.AccountForNode(0); + sub1.AccountForNode(1); + size_t per_node = acc1.GetConsumedSizeT(); + + // Bulk path + TestDedupAccountant acc2; + acc2.RegisterWeight("weight_W", 1000); + IndexedSubGraph sub2; + sub2.nodes.push_back(h.node_a->Index()); + sub2.nodes.push_back(h.node_b->Index()); + sub2.SetAccountant(&acc2); + sub2.AppendNodeCost(acc2.ComputeResourceCount(*h.node_a)); + sub2.AppendNodeCost(acc2.ComputeResourceCount(*h.node_b)); + sub2.AccountForAllNodes(); + size_t bulk = acc2.GetConsumedSizeT(); + + EXPECT_EQ(per_node, bulk) + << "Per-node and bulk should produce identical results"; + EXPECT_EQ(per_node, size_t{1000}); +} + +// Cross-subgraph dedup: EP1 commits node_A, EP2 probes node_B and +// correctly sees weight_W as already accounted. +TEST(ResourceAccountantTest, CrossSubGraph_DedupWorks) { + auto h = SharedWeightGraph::Create(); + TestDedupAccountant accountant; + accountant.RegisterWeight("weight_W", 1000); + + // EP1 probes and commits node_A + IndexedSubGraph sub1; + sub1.nodes.push_back(h.node_a->Index()); + sub1.SetAccountant(&accountant); + sub1.AppendNodeCost(accountant.ComputeResourceCount(*h.node_a)); + sub1.AccountForNode(0); + EXPECT_EQ(accountant.GetConsumedSizeT(), size_t{1000}); + + // EP2 probes node_B: weight_W already committed + auto cost_b = accountant.ComputeResourceCount(*h.node_b); + EXPECT_EQ(std::get(cost_b), size_t{0}) + << "weight_W was committed by EP1, should be deduped for EP2"; + + // EP2 commits node_B with cost 0 + IndexedSubGraph sub2; + sub2.nodes.push_back(h.node_b->Index()); + sub2.SetAccountant(&accountant); + sub2.AppendNodeCost(cost_b); + sub2.AccountForNode(0); + + EXPECT_EQ(accountant.GetConsumedSizeT(), size_t{1000}) + << "Total should still be 1000 - weight_W counted once across both"; +} + +} // namespace test +} // namespace onnxruntime \ No newline at end of file From 3cab988f79c3d4df76be4c0c57aa0f521992b525 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 26 Mar 2026 11:58:40 -0700 Subject: [PATCH 33/57] Update onnxruntime/python/tools/layering/layer_annotate.py Remove multi-threading from the annotation script. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../python/tools/layering/layer_annotate.py | 62 +++---------------- 1 file changed, 9 insertions(+), 53 deletions(-) diff --git a/onnxruntime/python/tools/layering/layer_annotate.py b/onnxruntime/python/tools/layering/layer_annotate.py index 7ae32dbffb8b8..7a78392ffc107 100644 --- a/onnxruntime/python/tools/layering/layer_annotate.py +++ b/onnxruntime/python/tools/layering/layer_annotate.py @@ -132,61 +132,17 @@ def annotate_graph(graph, substring_annotations, parallel=False): parallel (bool): If True, process the graph's nodes in parallel chunks. """ if parallel: + # Parallel processing with threads has been disabled due to lack of thread-safety + # guarantees for ONNX/protobuf objects during concurrent writes. Fall back to + # sequential processing while retaining the 'parallel' argument for API compatibility. logger = get_logger("annotate_model") - num_cores = os.cpu_count() or 1 - nodes = graph.node - total_nodes = len(nodes) - min_nodes_per_thread = 1000 - - if total_nodes > 0: - # Ensure each thread processes at least min_nodes_per_thread, if possible - max_workers = max(1, total_nodes // min_nodes_per_thread) - num_workers = min(num_cores, max_workers) - - logger.info( - f"Parallel processing configuration: Total Nodes={total_nodes}, Cores={num_cores}. " - f"Calculated Workers={num_workers} (Min nodes per thread={min_nodes_per_thread})." - ) - - chunks = [] - start_index = 0 - base_chunk_size = total_nodes // num_workers - remainder = total_nodes % num_workers - - for i in range(num_workers): - # Distribute the remainder (extra nodes) across the first 'remainder' threads - # To avoid the last worker processing very small amount of nodes - current_chunk_size = base_chunk_size + (1 if i < remainder else 0) - end_index = start_index + current_chunk_size - chunks.append(nodes[start_index:end_index]) - start_index = end_index - - # Use current thread for one of the chunks to avoid idle main thread - if num_workers > 1: - # Execute num_workers - 1 chunks in background threads - # Execute the last chunk in the current (main) thread - background_chunks = chunks[:-1] - main_chunk = chunks[-1] - - logger.info(f"Dispatching {len(background_chunks)} chunks to thread pool and 1 chunk to main thread.") - - with concurrent.futures.ThreadPoolExecutor(max_workers=num_workers - 1) as executor: - futures = [ - executor.submit(process_nodes, chunk, substring_annotations) for chunk in background_chunks - ] - - # Run last chunk here - process_nodes(main_chunk, substring_annotations) - - concurrent.futures.wait(futures) - else: - # Only 1 worker needed, run in current thread - logger.info("Using single thread (current) for processing.") - process_nodes(chunks[0], substring_annotations) - else: - process_nodes(graph.node, substring_annotations) - + logger.info( + "Parallel annotation requested, but thread-based parallelism is disabled " + "to avoid unsafe concurrent writes to ONNX/protobuf objects. " + "Proceeding with sequential processing." + ) + process_nodes(graph.node, substring_annotations) def annotate_model(model, substring_annotations): """ Annotates an ONNX model with metadata based on a provided mapping. From e6cb75f15073ff71758b1d887caaedcaaf052bf0 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 26 Mar 2026 12:33:08 -0700 Subject: [PATCH 34/57] Lint --- onnxruntime/python/tools/layering/layer_annotate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/onnxruntime/python/tools/layering/layer_annotate.py b/onnxruntime/python/tools/layering/layer_annotate.py index 7a78392ffc107..a5cc4f419d8ea 100644 --- a/onnxruntime/python/tools/layering/layer_annotate.py +++ b/onnxruntime/python/tools/layering/layer_annotate.py @@ -1,7 +1,5 @@ import argparse -import concurrent.futures import logging -import os import pathlib import threading @@ -143,6 +141,8 @@ def annotate_graph(graph, substring_annotations, parallel=False): ) process_nodes(graph.node, substring_annotations) + + def annotate_model(model, substring_annotations): """ Annotates an ONNX model with metadata based on a provided mapping. From b2ef9a2257b2cb0c0496984d57ea5c4027de5c90 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 26 Mar 2026 12:33:30 -0700 Subject: [PATCH 35/57] Flip = prefix to exact match --- .../core/framework/layering_annotations.cc | 4 +- .../framework/layering_annotations_test.cc | 60 ++++++++----------- 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc index 32343412e8759..5da8149f56adb 100644 --- a/onnxruntime/core/framework/layering_annotations.cc +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -63,9 +63,9 @@ common::Status LayeringRules::FromConfigString(const std::string& config_value, continue; } - bool prefix_match = false; + bool prefix_match = true; if (ann[0] == '=') { - prefix_match = true; + prefix_match = false; ann = ann.substr(1); ann = utils::TrimString(ann); } diff --git a/onnxruntime/test/framework/layering_annotations_test.cc b/onnxruntime/test/framework/layering_annotations_test.cc index f7d91629a08b8..bc3ae820db527 100644 --- a/onnxruntime/test/framework/layering_annotations_test.cc +++ b/onnxruntime/test/framework/layering_annotations_test.cc @@ -915,7 +915,7 @@ TEST(LayeringRulesTest, LayeringRulesParsing) { ASSERT_EQ(rules.rules.size(), 1u); EXPECT_EQ(rules.rules[0].device, "EP1"); EXPECT_EQ(rules.rules[0].annotation, "Annotation1"); - EXPECT_FALSE(rules.rules[0].prefix_match); + EXPECT_TRUE(rules.rules[0].prefix_match); } // Test multiple annotations for one device @@ -925,10 +925,10 @@ TEST(LayeringRulesTest, LayeringRulesParsing) { ASSERT_EQ(rules.rules.size(), 2u); EXPECT_EQ(rules.rules[0].device, "EP1"); EXPECT_EQ(rules.rules[0].annotation, "Annotation1"); - EXPECT_FALSE(rules.rules[0].prefix_match); + EXPECT_TRUE(rules.rules[0].prefix_match); EXPECT_EQ(rules.rules[1].device, "EP1"); EXPECT_EQ(rules.rules[1].annotation, "Annotation2"); - EXPECT_FALSE(rules.rules[1].prefix_match); + EXPECT_TRUE(rules.rules[1].prefix_match); } // Test multiple devices @@ -938,20 +938,20 @@ TEST(LayeringRulesTest, LayeringRulesParsing) { ASSERT_EQ(rules.rules.size(), 2u); EXPECT_EQ(rules.rules[0].device, "EP1"); EXPECT_EQ(rules.rules[0].annotation, "Annotation1"); - EXPECT_FALSE(rules.rules[0].prefix_match); + EXPECT_TRUE(rules.rules[0].prefix_match); EXPECT_EQ(rules.rules[1].device, "EP2"); EXPECT_EQ(rules.rules[1].annotation, "Annotation2"); - EXPECT_FALSE(rules.rules[1].prefix_match); + EXPECT_TRUE(rules.rules[1].prefix_match); } - // Test prefix match + // Test exact match { LayeringRules rules; ASSERT_STATUS_OK(LayeringRules::FromConfigString("EP1(=Annotation1)", rules)); ASSERT_EQ(rules.rules.size(), 1u); EXPECT_EQ(rules.rules[0].device, "EP1"); EXPECT_EQ(rules.rules[0].annotation, "Annotation1"); - EXPECT_TRUE(rules.rules[0].prefix_match); + EXPECT_FALSE(rules.rules[0].prefix_match); } // Test trimming whitespace @@ -961,13 +961,13 @@ TEST(LayeringRulesTest, LayeringRulesParsing) { ASSERT_EQ(rules.rules.size(), 3u); EXPECT_EQ(rules.rules[0].device, "EP1"); EXPECT_EQ(rules.rules[0].annotation, "Annotation1"); - EXPECT_FALSE(rules.rules[0].prefix_match); + EXPECT_TRUE(rules.rules[0].prefix_match); EXPECT_EQ(rules.rules[1].device, "EP1"); EXPECT_EQ(rules.rules[1].annotation, "Annotation2"); - EXPECT_TRUE(rules.rules[1].prefix_match); + EXPECT_FALSE(rules.rules[1].prefix_match); EXPECT_EQ(rules.rules[2].device, "EP2"); EXPECT_EQ(rules.rules[2].annotation, "Annotation3"); - EXPECT_FALSE(rules.rules[2].prefix_match); + EXPECT_TRUE(rules.rules[2].prefix_match); } } @@ -1057,6 +1057,7 @@ TEST(LayeringIndexTest, MakeNodeUnassigned_PreservesEpRuleMapping) { LayeringIndex::LayeringIndexToEpName rule_map; rule_map[0] = "DeviceA"; + // 3. Create Index auto index = LayeringIndex::Create(graph, std::move(ep_map), std::move(rule_map), std::move(rules)); // Both nodes should be assigned @@ -1098,7 +1099,6 @@ TEST(LayeringIndexTest, UpdateAfterFullUnassignment_RestoresVisibility) { Node& node0 = graph.AddNode("node0", "Abs", "Node 0", {input_arg}, {output_arg}); node0.SetLayeringAnnotation("RuleA"); - ASSERT_STATUS_OK(graph.Resolve()); // 2. Setup Rules: RuleA -> DeviceA @@ -1119,7 +1119,8 @@ TEST(LayeringIndexTest, UpdateAfterFullUnassignment_RestoresVisibility) { // 4. Simulate layout transform adding a new node with inherited annotation NodeArg* new_output_arg = &graph.GetOrCreateNodeArg("new_output", &type_proto); - Node& new_node = graph.AddNode("new_node", "Abs", "New Node", {output_arg}, {new_output_arg}); + Node& new_node = graph.AddNode("new_node", "Abs", "Node with inherited assignment", + {output_arg}, {new_output_arg}); new_node.SetLayeringAnnotation("RuleA"); // Inherits parent's annotation ASSERT_STATUS_OK(graph.Resolve()); @@ -1313,10 +1314,11 @@ TEST(LayeringIndexPartitionerTest, ResetUnclaimedNodesRemovesAssignment) { // Nodes that were pre-assigned to an EP via layering but NOT claimed in capabilities // should be unassigned so subsequent EPs can pick them up. - auto h = SimpleGraphHelper::Create(3); + auto h = SimpleGraphHelper::Create(4); auto* node0 = h.graph->GetNode(h.node_indices[0]); auto* node1 = h.graph->GetNode(h.node_indices[1]); auto* node2 = h.graph->GetNode(h.node_indices[2]); + node0->SetLayeringAnnotation("RuleA"); node1->SetLayeringAnnotation("RuleA"); node2->SetLayeringAnnotation("RuleA"); @@ -1370,20 +1372,12 @@ TEST(LayeringIndexPartitionerTest, UpdateAfterLayoutTransformAddsNewNodes) { // In GetCapabilityForEP, after layout transform, new nodes with inherited annotations // are added and the index is updated. - auto h = SimpleGraphHelper::Create(2); + auto h = SimpleGraphHelper::Create(1); auto* node0 = h.graph->GetNode(h.node_indices[0]); node0->SetLayeringAnnotation("RuleA"); ASSERT_STATUS_OK(h.graph->Resolve()); - LayeringRules rules; - rules.rules.push_back({"DeviceA", "RuleA", false}); - - LayeringIndex::EpNameToLayeringIndices ep_map; - ep_map["DeviceA"].insert(0); - LayeringIndex::LayeringIndexToEpName rule_map; - rule_map[0] = "DeviceA"; - - auto index = LayeringIndex::Create(*h.graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + auto index = CreateTwoEpIndex(*h.graph, "DeviceA", "RuleA", "DeviceB", "RuleB"); // Record the max node index before "layout transformation" const NodeIndex first_new_node = h.graph->MaxNodeIndex(); @@ -1393,7 +1387,7 @@ TEST(LayeringIndexPartitionerTest, UpdateAfterLayoutTransformAddsNewNodes) { type_proto.mutable_tensor_type()->set_elem_type(ONNX_NAMESPACE::TensorProto_DataType_FLOAT); NodeArg* extra_out = &h.graph->GetOrCreateNodeArg("extra_output", &type_proto); NodeArg* output_arg = &h.graph->GetOrCreateNodeArg("output", nullptr); // reuse existing - Node& new_node = h.graph->AddNode("layout_inserted_node", "Abs", "Layout inserted", + Node& new_node = h.graph->AddNode("new_node", "Abs", "Node with inherited annotation", {output_arg}, {extra_out}); new_node.SetLayeringAnnotation("RuleA"); // Inherits parent's annotation ASSERT_STATUS_OK(h.graph->Resolve()); @@ -1412,10 +1406,13 @@ TEST(LayeringIndexPartitionerTest, UpdateAfterLayoutTransformAddsNewNodes) { ASSERT_FALSE(new_node_indices.empty()); index.Update(*h.graph, new_node_indices); - // New node should now be assigned to rule 0 (DeviceA) + // New node should be assigned to rule 0 (DeviceA) auto assign = index.GetNodeAssignment(*h.graph, new_node.Index()); ASSERT_TRUE(assign.has_value()); EXPECT_EQ(*assign, 0u); + + // And the annotation string should be on the node + EXPECT_EQ(new_node.GetLayeringAnnotation(), "RuleA"); } TEST(LayeringIndexPartitionerTest, UpdateWithUnannotatedNewNodeRemainsUnassigned) { @@ -1427,15 +1424,7 @@ TEST(LayeringIndexPartitionerTest, UpdateWithUnannotatedNewNodeRemainsUnassigned node0->SetLayeringAnnotation("RuleA"); ASSERT_STATUS_OK(h.graph->Resolve()); - LayeringRules rules; - rules.rules.push_back({"DeviceA", "RuleA", false}); - - LayeringIndex::EpNameToLayeringIndices ep_map; - ep_map["DeviceA"].insert(0); - LayeringIndex::LayeringIndexToEpName rule_map; - rule_map[0] = "DeviceA"; - - auto index = LayeringIndex::Create(*h.graph, std::move(ep_map), std::move(rule_map), std::move(rules)); + auto index = CreateTwoEpIndex(*h.graph, "DeviceA", "RuleA", "DeviceB", "RuleB"); // Add a new node WITHOUT annotation ONNX_NAMESPACE::TypeProto type_proto; @@ -1607,7 +1596,8 @@ TEST(LayeringIndexPartitionerTest, MakeUnassignedThenReassignViaPrefixRule) { // Add a new node with a different annotation that also matches the prefix NodeArg* new_out = &graph.GetOrCreateNodeArg("new_output", &type_proto); - Node& new_node = graph.AddNode("new_node", "Abs", "", {output_arg}, {new_out}); + Node& new_node = graph.AddNode("new_node", "Abs", "Node with inherited annotation", + {output_arg}, {new_out}); new_node.SetLayeringAnnotation("Layer_GPU_Memory"); ASSERT_STATUS_OK(graph.Resolve()); From fde63000dc3d2fbd1f793bf4a09503d1d6276a3b Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 26 Mar 2026 12:35:46 -0700 Subject: [PATCH 36/57] Adjust comments for duplicate annotations --- .../test/framework/layering_annotations_test.cc | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/onnxruntime/test/framework/layering_annotations_test.cc b/onnxruntime/test/framework/layering_annotations_test.cc index bc3ae820db527..832e2ef80af5a 100644 --- a/onnxruntime/test/framework/layering_annotations_test.cc +++ b/onnxruntime/test/framework/layering_annotations_test.cc @@ -1000,23 +1000,23 @@ TEST(LayeringRulesTest, FromConfigString_IgnoresEmptyEntries) { TEST(LayeringRulesTest, FromConfigString_RejectsDuplicateAnnotations) { LayeringRules rules; - // Duplicate exact annotation within the same device + // Duplicate prefix annotation within the same device EXPECT_FALSE(LayeringRules::FromConfigString("EP1(Ann1, Ann1)", rules).IsOK()); - // Duplicate exact annotation across different devices + // Duplicate prefix annotation across different devices EXPECT_FALSE(LayeringRules::FromConfigString("EP1(Ann1); EP2(Ann1)", rules).IsOK()); - // Duplicate prefix annotation within the same device + // Duplicate exact annotation within the same device EXPECT_FALSE(LayeringRules::FromConfigString("EP1(=Ann1, =Ann1)", rules).IsOK()); - // Duplicate prefix annotation across different devices + // Duplicate exact annotation across different devices EXPECT_FALSE(LayeringRules::FromConfigString("EP1(=Ann1); EP2(=Ann1)", rules).IsOK()); - // Same annotation but different match types (exact vs prefix) should be OK + // Same annotation but different match types (prefix vs exact) should be OK ASSERT_STATUS_OK(LayeringRules::FromConfigString("EP1(Ann1, =Ann1)", rules)); ASSERT_EQ(rules.rules.size(), 2u); - EXPECT_FALSE(rules.rules[0].prefix_match); - EXPECT_TRUE(rules.rules[1].prefix_match); + EXPECT_TRUE(rules.rules[0].prefix_match); + EXPECT_FALSE(rules.rules[1].prefix_match); } TEST(LayeringIndexTest, MakeNodeUnassigned_PreservesEpRuleMapping) { From 7871afa07c4a1ae656af5266d5fb1d75d2b72810 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 26 Mar 2026 12:38:44 -0700 Subject: [PATCH 37/57] Remove bad comment --- onnxruntime/core/framework/layering_annotations.cc | 3 --- 1 file changed, 3 deletions(-) diff --git a/onnxruntime/core/framework/layering_annotations.cc b/onnxruntime/core/framework/layering_annotations.cc index 5da8149f56adb..91df102abef17 100644 --- a/onnxruntime/core/framework/layering_annotations.cc +++ b/onnxruntime/core/framework/layering_annotations.cc @@ -572,9 +572,6 @@ void LayeringIndex::MakeNodeUnassigned(const Graph& graph, NodeIndex node_id) { auto layer_to_nodes_hit = graph_layering_index.layer_to_node_ids_.find(*layer_idx); if (layer_to_nodes_hit != graph_layering_index.layer_to_node_ids_.end()) { layer_to_nodes_hit->second.erase(node_id); - // If the layer has no more nodes assigned across this graph, - // remove the layer index from the EP mapping so subsequent - // partitioning passes no longer reserve this layer for the EP. if (layer_to_nodes_hit->second.empty()) { graph_layering_index.layer_to_node_ids_.erase(layer_to_nodes_hit); } From 16ec921b351081572f77f0b84eef5ede5bb4ceff Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 26 Mar 2026 12:46:29 -0700 Subject: [PATCH 38/57] Adjust EpWithNoLayeringRulesSeesAllUnassignedNodes --- .../framework/layering_annotations_test.cc | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/onnxruntime/test/framework/layering_annotations_test.cc b/onnxruntime/test/framework/layering_annotations_test.cc index 832e2ef80af5a..f865be7bfc686 100644 --- a/onnxruntime/test/framework/layering_annotations_test.cc +++ b/onnxruntime/test/framework/layering_annotations_test.cc @@ -1636,8 +1636,10 @@ TEST(LayeringIndexPartitionerTest, NoLayeringIndexAllNodesVisible) { TEST(LayeringIndexPartitionerTest, EpWithNoLayeringRulesSeesAllUnassignedNodes) { // An EP that has no rules in the LayeringIndex (i.e., GetLayeringRulesForThisEp returns nullopt) - // should see all unassigned nodes but not nodes assigned to other EPs. - // This is the behavior when a CPU fallback EP is not mentioned in layering config. + // should still see unassigned nodes, but nodes assigned to other EPs are excluded. + // This is the behavior for a CPU fallback EP not mentioned in layering config, + // as implemented in graph_partitioner.cc create_graph_viewer: + // if (!rules_opt || rules_opt->get().count(*rule_idx_opt) == 0) { include = false; } auto h = SimpleGraphHelper::Create(4); auto* node0 = h.graph->GetNode(h.node_indices[0]); @@ -1653,14 +1655,39 @@ TEST(LayeringIndexPartitionerTest, EpWithNoLayeringRulesSeesAllUnassignedNodes) auto rules_cpu = index.GetLayeringRulesForThisEp("CPUDevice"); EXPECT_FALSE(rules_cpu.has_value()); - // In the partitioner, when GetLayeringRulesForThisEp returns nullopt, - // the create_graph_viewer lambda creates a standard (unfiltered) GraphViewer. - // This means CPUDevice sees ALL nodes including those assigned to other EPs. - // This is by design: the layering filtering only activates for EPs that have rules. - GraphViewer viewer(*h.graph); - EXPECT_EQ(viewer.NumberOfNodes(), 4); -} + // Replicate create_graph_viewer filtering logic for an EP with no rules. + // When rules_opt is nullopt, any node with an assignment is excluded: + // if (!rules_opt || ...) { include = false; } + // Unassigned nodes remain included. + InlinedVector filtered_for_cpu; + for (auto& node : h.graph->Nodes()) { + auto rule_idx_opt = index.GetNodeAssignment(*h.graph, node.Index()); + bool include = true; + if (rule_idx_opt) { + if (!rules_cpu || rules_cpu->get().count(*rule_idx_opt) == 0) { + include = false; + } + } + if (include) { + filtered_for_cpu.push_back(&node); + } + } + // CPUDevice should see only the 2 unassigned nodes (node1, node3). + // node0 (RuleA/DeviceA) and node2 (RuleB/DeviceB) are excluded. + EXPECT_EQ(filtered_for_cpu.size(), 2u); + + bool found[4] = {}; + for (const auto* n : filtered_for_cpu) { + for (size_t i = 0; i < std::size(found); ++i) { + if (n->Index() == h.node_indices[i]) found[i] = true; + } + } + EXPECT_FALSE(found[0]) << "node0 assigned to DeviceA should be excluded"; + EXPECT_TRUE(found[1]) << "node1 unassigned should be included"; + EXPECT_FALSE(found[2]) << "node2 assigned to DeviceB should be excluded"; + EXPECT_TRUE(found[3]) << "node3 unassigned should be included"; +} TEST(LayeringIndexPartitionerTest, MultipleRulesForSameEp) { // An EP can have multiple rules assigned to it. All nodes matching any of its // rules should be visible to it, while nodes matching other EP rules should not. From 4da5c3b4689e4d03ee331d7fdddb859bfafb7ee3 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 26 Mar 2026 13:39:14 -0700 Subject: [PATCH 39/57] Throw on multiple annotations --- onnxruntime/core/framework/tensorprotoutils.cc | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/onnxruntime/core/framework/tensorprotoutils.cc b/onnxruntime/core/framework/tensorprotoutils.cc index 9010cc83109bf..061b329be3c1d 100644 --- a/onnxruntime/core/framework/tensorprotoutils.cc +++ b/onnxruntime/core/framework/tensorprotoutils.cc @@ -2532,13 +2532,19 @@ Status UnpackInitializerData(const ONNX_NAMESPACE::TensorProto& initializer, std } std::optional GetNodeProtoLayeringAnnotation(const ONNX_NAMESPACE::NodeProto& node_proto) { + size_t annotation_count = 0; std::optional result; for (const auto& prop : node_proto.metadata_props()) { if (prop.key() == kNodeProtoLayerAnnotation) { + if (++annotation_count > 1) { + // Multiple annotations found, this is unexpected + ORT_THROW("Multiple '", kNodeProtoLayerAnnotation, "' annotations found in NodeProto '", node_proto.name(), + "'. This is unexpected as only one annotation should be present."); + } if (!prop.value().empty()) { result = prop.value(); + break; } - break; } } return result; From 01e4506dce2af20e2c50cc4c4af180c83ef4b04d Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 26 Mar 2026 15:27:01 -0700 Subject: [PATCH 40/57] Make sure annotations are propagated on function inlining --- include/onnxruntime/core/graph/graph.h | 4 +- onnxruntime/core/graph/graph.cc | 63 +++++++++++++------------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/include/onnxruntime/core/graph/graph.h b/include/onnxruntime/core/graph/graph.h index dc47c076b73cd..60492058bcae0 100644 --- a/include/onnxruntime/core/graph/graph.h +++ b/include/onnxruntime/core/graph/graph.h @@ -1338,10 +1338,12 @@ class Graph { // NOLINT(clang-analyzer-optin.performance.Padding): preserve exi The Graph needs to be Resolve()d after this call. @param func_to_inline + @param parent_annotation. Annotation inherited from the parent node that is being inlined. @returns Status indicating success or providing an error message. */ - Status InlineFunctionProto(const ONNX_NAMESPACE::FunctionProto& func_to_inline); + Status InlineFunctionProto(const ONNX_NAMESPACE::FunctionProto& func_to_inline, + const std::string& parent_annotation); /** Mark a NodeArg name as coming from the outer scope when programmatically constructing a Graph that will be used as a GraphProto attribute in another Node. diff --git a/onnxruntime/core/graph/graph.cc b/onnxruntime/core/graph/graph.cc index a66355f6cb8af..56a3bd3771c94 100644 --- a/onnxruntime/core/graph/graph.cc +++ b/onnxruntime/core/graph/graph.cc @@ -6102,7 +6102,8 @@ Status Graph::InlineIfSubgraph(bool condition_value, Node& if_node, const loggin return Status::OK(); } -Status Graph::InlineFunctionProto(const ONNX_NAMESPACE::FunctionProto& func_to_inline) { +Status Graph::InlineFunctionProto(const ONNX_NAMESPACE::FunctionProto& func_to_inline, + const std::string& parent_annotation) { auto to_node_arg = [this](const std::string& name) { return &this->GetOrCreateNodeArg(name, nullptr); }; @@ -6137,22 +6138,21 @@ Status Graph::InlineFunctionProto(const ONNX_NAMESPACE::FunctionProto& func_to_i for (const auto& node_attr : inlined_node->attribute()) { new_attr_map.insert_or_assign(node_attr.name(), node_attr); } - ORT_IGNORE_RETURN_VALUE(AddNode(inlined_node->name(), inlined_node->op_type(), - inlined_node->doc_string(), inputs, outputs, - &new_attr_map, inlined_node->domain())); + auto& new_node = AddNode(inlined_node->name(), inlined_node->op_type(), + inlined_node->doc_string(), inputs, outputs, + &new_attr_map, inlined_node->domain()); + + // Nodes that come from function_proto currently can not have any annotations. + // So we set it to parent. + if (!parent_annotation.empty()) { + new_node.SetLayeringAnnotation(parent_annotation); + } } return Status::OK(); } Status Graph::InlineFunction(Node& callnode) { - // Remove output edges. Requirement for RemoveNode() below. - auto output_edges = callnode.GetRelationships().output_edges; // copy so RemoveEdge doesn't invalidate iterator - for (const auto& output_edge : output_edges) { - RemoveEdge(callnode.Index(), output_edge.GetNode().Index(), output_edge.GetSrcArgIndex(), - output_edge.GetDstArgIndex()); - } - // create a uniq_identifier to append to every node name and intermediate input\outputs // to make sure there are no unintended duplicates std::string base_uniq_identifier{"_inlfunc_"}; @@ -6163,9 +6163,6 @@ Status Graph::InlineFunction(Node& callnode) { // Inlined nodes that don't already have their own annotation will inherit this. const std::string parent_annotation = callnode.GetLayeringAnnotation(); - // Record the current max node index so we can identify newly inlined nodes afterward. - const int max_node_index_before_inline = MaxNodeIndex(); - // Replace a (function-call) node by an inlined graph. if (!callnode.GetFunctionBody()) { // This is the normal use-case: inlining a FunctionProto (representing @@ -6177,7 +6174,7 @@ Status Graph::InlineFunction(Node& callnode) { function_utils::Specialize(inlined_fp, callnode, uniq_identifier); // In this case, global Resolve() will take care of everything. - ORT_RETURN_IF_ERROR(InlineFunctionProto(inlined_fp)); + ORT_RETURN_IF_ERROR(InlineFunctionProto(inlined_fp, parent_annotation)); } else { // Uncommon scenario. Inlining a node representing a fused sub-graph. // TODO: Unclear that this feature is needed. Can this be removed? @@ -6196,11 +6193,18 @@ Status Graph::InlineFunction(Node& callnode) { outputs.push_back(&n_output); } - AddNode(subgraph_node.Name() + uniq_identifier, subgraph_node.OpType(), subgraph_node.Description(), - inputs, - outputs, - &subgraph_node.GetAttributes(), - subgraph_node.Domain()); + auto& new_node = AddNode(subgraph_node.Name() + uniq_identifier, subgraph_node.OpType(), + subgraph_node.Description(), + inputs, + outputs, + &subgraph_node.GetAttributes(), + subgraph_node.Domain()); + if (!subgraph_node.GetLayeringAnnotation().empty()) { + new_node.SetLayeringAnnotation(subgraph_node.GetLayeringAnnotation()); + } else if (!parent_annotation.empty()) { + // If the subgraph node doesn't have its own annotation, use the parent function node's annotation. + new_node.SetLayeringAnnotation(parent_annotation); + } } } @@ -6227,24 +6231,19 @@ Status Graph::InlineFunction(Node& callnode) { } } - // Propagate the parent function node's layering annotation to all newly inlined nodes - // that don't already have their own annotation. - if (!parent_annotation.empty()) { - const int max_node_index_after_inline = MaxNodeIndex(); - for (int i = max_node_index_before_inline; i < max_node_index_after_inline; ++i) { - Node* node = GetNode(static_cast(i)); - if (node != nullptr && node->GetLayeringAnnotation().empty()) { - node->SetLayeringAnnotation(parent_annotation); - } - } + // Requirement for RemoveNode() below. + // copy so RemoveEdge doesn't invalidate iterator + auto output_edges = callnode.GetRelationships().output_edges; + for (const auto& output_edge : output_edges) { + RemoveEdge(callnode.Index(), output_edge.GetNode().Index(), output_edge.GetSrcArgIndex(), + output_edge.GetDstArgIndex()); } RemoveNode(callnode.Index()); - // std::cout << "Graph after inlining\n\n" << *this << std::endl << std::flush; - return Status::OK(); } + void Graph::SetInputs(gsl::span inputs) { graph_inputs_including_initializers_.clear(); graph_inputs_excluding_initializers_.clear(); From 3e52f145ff82ffabfc1d4bf53242a0189bbd957b Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 26 Mar 2026 15:45:11 -0700 Subject: [PATCH 41/57] Update include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h Fix the documentation. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../core/session/onnxruntime_session_options_config_keys.h | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h b/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h index 2ac4d7f649008..44ff0256c33fe 100644 --- a/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h +++ b/include/onnxruntime/core/session/onnxruntime_session_options_config_keys.h @@ -344,9 +344,10 @@ static const char* const kOrtSessionOptionsResourceCudaPartitioningSettings = /// Where: /// - device1, device2, ... are the recognized device names to be matched against EPs configured in /// the given session. -/// - annotation1, annotation2, ... are the exact annotation strings to be matched against node annotations -/// - =annotation3 indicates a prefix match for annotation3. Any node annotation that starts with -/// 'annotation3' will be matched. +/// - annotation1, annotation2, ... are annotation prefixes to be matched against node annotations. Any +/// node annotation that starts with one of these prefixes will be matched. +/// - =annotation3 indicates an exact match for annotation3. Only node annotations that are exactly +/// equal to 'annotation3' will be matched. /// TODO: add a list of recognized devices here. /// static const char* const kOrtSessionOptionsLayerAssignmentSettings = "session.layer_assignment_settings"; From 24a46e8cdf284dd3314763874f4e2b87aa92d436 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 26 Mar 2026 15:46:45 -0700 Subject: [PATCH 42/57] Update onnxruntime/core/framework/graph_partitioner.cc Remove duplicate comment. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- onnxruntime/core/framework/graph_partitioner.cc | 1 - 1 file changed, 1 deletion(-) diff --git a/onnxruntime/core/framework/graph_partitioner.cc b/onnxruntime/core/framework/graph_partitioner.cc index baa6b403fa3a5..8075294a200ff 100644 --- a/onnxruntime/core/framework/graph_partitioner.cc +++ b/onnxruntime/core/framework/graph_partitioner.cc @@ -759,7 +759,6 @@ static Status PartitionOnnxFormatModelImpl(Graph& graph, FuncManager& func_mgr, return Status::OK(); } -// expand any nodes that have an ONNX function definition but no matching ORT kernel // expand any nodes that have an ONNX function definition but no matching ORT kernel static Status InlineNodes(Graph& graph, bool& modified_graph, LayeringIndex* layering_index) { // recurse into nested graphs first so we process from bottom up From e745e02ac1243b1727f2ec9865a27108e0d037ca Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 26 Mar 2026 15:48:03 -0700 Subject: [PATCH 43/57] Update onnxruntime/core/graph/graph_utils.h Address the comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- onnxruntime/core/graph/graph_utils.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onnxruntime/core/graph/graph_utils.h b/onnxruntime/core/graph/graph_utils.h index 8152821192795..2106da1a96327 100644 --- a/onnxruntime/core/graph/graph_utils.h +++ b/onnxruntime/core/graph/graph_utils.h @@ -479,7 +479,7 @@ NodeArg& CreateNodeArg(Graph& graph, const NodeArg& base_arg); /// /// This function creates an indexed subgraph from a collection of nodes -/// using the graph instance. The IndexedSubgraph can then we used to create +/// using the graph instance. The IndexedSubgraph can then be used to create /// a filtered GraphViewer instance that only contains the nodes in the collection. /// /// From 59b5ccd134ad6bb5bea606bdd44ebfa36df6d1b6 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 26 Mar 2026 15:55:36 -0700 Subject: [PATCH 44/57] Fix issues in python --- .../python/tools/layering/layer_annotate.py | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/onnxruntime/python/tools/layering/layer_annotate.py b/onnxruntime/python/tools/layering/layer_annotate.py index a5cc4f419d8ea..1295d33ff14c1 100644 --- a/onnxruntime/python/tools/layering/layer_annotate.py +++ b/onnxruntime/python/tools/layering/layer_annotate.py @@ -1,7 +1,6 @@ import argparse import logging import pathlib -import threading import onnx @@ -79,7 +78,7 @@ def process_nodes(nodes, substring_annotations): Helper function to process a list of nodes sequentially. """ logger = get_logger("annotate_model") - logger.info(f"Thread {threading.get_ident()} processing {len(nodes)} nodes.") + logger.info(f"Processing {len(nodes)} nodes.") for node in nodes: matched_annotation = None @@ -105,13 +104,13 @@ def process_nodes(nodes, substring_annotations): # Recurse into subgraphs for control flow nodes for attr in node.attribute: if attr.type == onnx.AttributeProto.GRAPH: - annotate_graph(attr.g, substring_annotations, parallel=False) + annotate_graph(attr.g, substring_annotations) elif attr.type == onnx.AttributeProto.GRAPHS: for sub_graph in attr.graphs: - annotate_graph(sub_graph, substring_annotations, parallel=False) + annotate_graph(sub_graph, substring_annotations) -def annotate_graph(graph, substring_annotations, parallel=False): +def annotate_graph(graph, substring_annotations): """ Recursively applies annotations to nodes where a configured substring appears in the node name. @@ -127,19 +126,7 @@ def annotate_graph(graph, substring_annotations, parallel=False): Args: graph (onnx.GraphProto): The ONNX graph to process. substring_annotations (list): A list of tuples (substring, annotation_string). - parallel (bool): If True, process the graph's nodes in parallel chunks. """ - if parallel: - # Parallel processing with threads has been disabled due to lack of thread-safety - # guarantees for ONNX/protobuf objects during concurrent writes. Fall back to - # sequential processing while retaining the 'parallel' argument for API compatibility. - logger = get_logger("annotate_model") - logger.info( - "Parallel annotation requested, but thread-based parallelism is disabled " - "to avoid unsafe concurrent writes to ONNX/protobuf objects. " - "Proceeding with sequential processing." - ) - process_nodes(graph.node, substring_annotations) @@ -154,7 +141,7 @@ def annotate_model(model, substring_annotations): model (onnx.ModelProto): The ONNX model to annotate. substring_annotations (list): A list of tuples (substring, annotation_string). """ - annotate_graph(model.graph, substring_annotations, parallel=True) + annotate_graph(model.graph, substring_annotations) if __name__ == "__main__": From 054f8941af48c12da0627580fe3168fe1e400ac0 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 26 Mar 2026 17:16:45 -0700 Subject: [PATCH 45/57] Address undercounting problem --- .../core/framework/resource_accountant.h | 9 +++ .../core/graph/indexed_sub_graph.h | 21 ++++--- .../core/framework/graph_partitioner.cc | 9 ++- .../core/framework/resource_accountant.cc | 39 ++++++++++-- .../framework/resource_accountant_test.cc | 59 ++++++++++++++----- 5 files changed, 107 insertions(+), 30 deletions(-) diff --git a/include/onnxruntime/core/framework/resource_accountant.h b/include/onnxruntime/core/framework/resource_accountant.h index 99bfcca869572..7bb5a993d140b 100644 --- a/include/onnxruntime/core/framework/resource_accountant.h +++ b/include/onnxruntime/core/framework/resource_accountant.h @@ -61,6 +61,15 @@ class IResourceAccountant { bool IsStopIssued() const noexcept { return stop_assignment_; } + // Called before each GetCapability pass to discard pending weight tracking + // from a previous (discarded) pass. Default no-op for stats-based accountants. + virtual void ResetPendingWeights() {} + + // Called when a node's cost is committed (AccountForNode/AccountForAllNodes). + // Moves the node's pending weights into the committed set so they persist + // across GetCapability passes. Default no-op for stats-based accountants. + virtual void CommitWeightsForNode(size_t /*node_index*/) {} + static std::string MakeUniqueNodeName(const Node& node); private: diff --git a/include/onnxruntime/core/graph/indexed_sub_graph.h b/include/onnxruntime/core/graph/indexed_sub_graph.h index b4f7f1f176715..8e1ae539bc764 100644 --- a/include/onnxruntime/core/graph/indexed_sub_graph.h +++ b/include/onnxruntime/core/graph/indexed_sub_graph.h @@ -86,11 +86,12 @@ struct IndexedSubGraph { // Should call IsAccountingEnabled() first // Takes the previously computed ResourceCount for the node - // (usually during GetCapabiilty()) + // (usually during GetCapability()) // if present and adds it to the consumed amount void AccountForNode(size_t cost_index) const { assert(cost_index < nodes_costs.size()); resource_accountant->AddConsumedAmount(nodes_costs[cost_index]); + resource_accountant->CommitWeightsForNode(nodes[cost_index]); } // Accounts for all constituent nodes by summing their pre-stored costs. @@ -99,22 +100,28 @@ struct IndexedSubGraph { // cross-node weight deduplication already applied). void AccountForAllNodes() const { assert(resource_accountant != nullptr); - for (const auto& cost : nodes_costs) { - resource_accountant->AddConsumedAmount(cost); + for (size_t i = 0; i < nodes_costs.size(); ++i) { + resource_accountant->AddConsumedAmount(nodes_costs[i]); + resource_accountant->CommitWeightsForNode(nodes[i]); } } - // This computes and accounts for the resource cost for the node that just - // been fused from other nodes, and the EP did not had a chance to compute the costs. - void ComputeAndAccountForNode(const Node& node) const { + // Accounts for a node given its index and a pre-computed resource cost. + // Use this when the cost was computed externally (e.g. for a fused node). + void AccountForNode(NodeIndex node_index, const ResourceCount& resource_count) const { assert(resource_accountant != nullptr); - resource_accountant->AddConsumedAmount(resource_accountant->ComputeResourceCount(node)); + resource_accountant->AddConsumedAmount(resource_count); + resource_accountant->CommitWeightsForNode(node_index); } void SetAccountant(IResourceAccountant* res_accountant) { resource_accountant = res_accountant; } + IResourceAccountant* GetAccountant() const noexcept { + return resource_accountant; + } + // Append resource count to the list of costs for the nodes. void AppendNodeCost(const ResourceCount& cost) { assert(resource_accountant != nullptr); diff --git a/onnxruntime/core/framework/graph_partitioner.cc b/onnxruntime/core/framework/graph_partitioner.cc index 8075294a200ff..6c01a947ed021 100644 --- a/onnxruntime/core/framework/graph_partitioner.cc +++ b/onnxruntime/core/framework/graph_partitioner.cc @@ -276,6 +276,9 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l std::unique_ptr graph_viewer; ORT_RETURN_IF_ERROR(create_graph_viewer(sub_graph_holder, graph_viewer)); + if (params.resource_accountant) { + params.resource_accountant->ResetPendingWeights(); + } capabilities = get_capabilities(current_ep, *graph_viewer, kernel_lookup, params.resource_accountant, graph_optimizer_registry); @@ -342,6 +345,9 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l std::unique_ptr graph_viewer; ORT_RETURN_IF_ERROR(create_graph_viewer(sub_graph_holder, graph_viewer)); + if (params.resource_accountant) { + params.resource_accountant->ResetPendingWeights(); + } capabilities = get_capabilities(current_ep, *graph_viewer, kernel_lookup, params.resource_accountant, graph_optimizer_registry); @@ -495,7 +501,8 @@ static Node* PlaceNode(Graph& graph, const IndexedSubGraph& capability, // that the fused node would use no more memory when the nodes we are fusing. // and potentially less than that, and therefore, no threshold check is needed here. // All threshold checks are done within the EP. - capability.ComputeAndAccountForNode(*fused_node); + auto resource_count = capability.GetAccountant()->ComputeResourceCount(*fused_node); + capability.AccountForNode(fused_node->Index(), resource_count); } result = fused_node; diff --git a/onnxruntime/core/framework/resource_accountant.cc b/onnxruntime/core/framework/resource_accountant.cc index b948a3f6ab2ee..e50921fb75654 100644 --- a/onnxruntime/core/framework/resource_accountant.cc +++ b/onnxruntime/core/framework/resource_accountant.cc @@ -67,8 +67,6 @@ class SizeBasedStatsAccountant : public IResourceAccountant { const auto* graph = node.GetContainingGraph(); if (!graph) return static_cast(0); - /// XXX: Consider accounting for intermediate tensors as well - /// if they have shape. SafeInt total_size = 0; for (const auto* input_def : node.InputDefs()) { if (!input_def->Exists()) continue; @@ -78,8 +76,13 @@ class SizeBasedStatsAccountant : public IResourceAccountant { const auto* tensor_proto = graph->GetInitializer(name, check_outer_scope); if (tensor_proto) { - // Already accounted for this initializer in another node, skip to avoid double counting. - if (accounted_weights_.find(name) != accounted_weights_.end()) { + // Skip if already committed from a previous partitioning iteration + if (committed_weights_.count(name) > 0) { + continue; + } + + // Skip if already pending from another node in this GetCapability pass + if (IsInPendingWeights(name)) { continue; } @@ -88,7 +91,7 @@ class SizeBasedStatsAccountant : public IResourceAccountant { if (status.IsOK()) { total_size += size; - accounted_weights_.insert(name); + pending_weights_by_node_[node.Index()].insert(name); } } } @@ -116,10 +119,34 @@ class SizeBasedStatsAccountant : public IResourceAccountant { } } + void ResetPendingWeights() override { + pending_weights_by_node_.clear(); + } + + void CommitWeightsForNode(NodeIndex node_index) override { + auto it = pending_weights_by_node_.find(node_index); + if (it != pending_weights_by_node_.end()) { + committed_weights_.insert(it->second.begin(), it->second.end()); + pending_weights_by_node_.erase(it); + } + } + private: + bool IsInPendingWeights(const std::string& name) const { + for (const auto& [node_idx, weights] : pending_weights_by_node_) { + if (weights.count(name) > 0) return true; + } + return false; + } + size_t consumed_amount_ = 0; std::optional> node_stats_; - InlinedHashSet accounted_weights_; + // Weights committed from previous partitioning iterations. + // These persist across GetCapability passes. + InlinedHashSet committed_weights_; + // Weights seen during the current GetCapability pass, keyed by node index. + // Discarded by ResetPendingWeights() or promoted by CommitWeightsForNode(). + InlinedHashMap> pending_weights_by_node_; }; struct NodeStatsRecorder::Impl { diff --git a/onnxruntime/test/framework/resource_accountant_test.cc b/onnxruntime/test/framework/resource_accountant_test.cc index 074b1567684af..cc25183b04596 100644 --- a/onnxruntime/test/framework/resource_accountant_test.cc +++ b/onnxruntime/test/framework/resource_accountant_test.cc @@ -15,9 +15,9 @@ namespace onnxruntime { namespace test { // Test accountant mimicking SizeBasedStatsAccountant ad-hoc path: -// ComputeResourceCount inserts weight names into a dedup set so -// calling it again on a node whose weights were already seen -// returns 0 for those weights. +// Uses pending/committed weight sets so that: +// - Within a GetCapability pass, shared weights are deduped +// - Across passes, only committed weights persist and pending are discarded class TestDedupAccountant : public IResourceAccountant { public: TestDedupAccountant() = default; @@ -53,19 +53,34 @@ class TestDedupAccountant : public IResourceAccountant { constexpr bool check_outer_scope = true; const auto* init = graph->GetInitializer(name, check_outer_scope); if (init != nullptr) { - if (seen_weights_.count(name) > 0) { + if (committed_weights_.count(name) > 0) { + continue; + } + if (IsInPendingWeights(name)) { continue; } auto it = weight_sizes_.find(name); if (it != weight_sizes_.end()) { total += it->second; } - seen_weights_.insert(name); + pending_weights_by_node_[node.Index()].insert(name); } } return total; } + void ResetPendingWeights() override { + pending_weights_by_node_.clear(); + } + + void CommitWeightsForNode(NodeIndex node_index) override { + auto it = pending_weights_by_node_.find(node_index); + if (it != pending_weights_by_node_.end()) { + committed_weights_.insert(it->second.begin(), it->second.end()); + pending_weights_by_node_.erase(it); + } + } + void RegisterWeight(const std::string& name, size_t size) { weight_sizes_[name] = size; } @@ -73,8 +88,16 @@ class TestDedupAccountant : public IResourceAccountant { size_t GetConsumedSizeT() const { return consumed_; } private: + bool IsInPendingWeights(const std::string& name) const { + for (const auto& [node_idx, weights] : pending_weights_by_node_) { + if (weights.count(name) > 0) return true; + } + return false; + } + size_t consumed_ = 0; - InlinedHashSet seen_weights_; + InlinedHashSet committed_weights_; + InlinedHashMap> pending_weights_by_node_; InlinedHashMap weight_sizes_; }; @@ -152,28 +175,32 @@ TEST(ResourceAccountantTest, AccountForAllNodes_CorrectlyUsesPreStoredCosts) { << "AccountForAllNodes should sum pre-stored costs (1000 + 0)"; } -// Demonstrates the bug: after probing populates the dedup set, -// ComputeAndAccountForNode on a fused node returns 0. -TEST(ResourceAccountantTest, ComputeAndAccountForNode_UnderCountsAfterProbing) { +// Verifies that ResetPendingWeights + re-probe produces correct results. +// After probing (which only writes to pending), resetting pending and +// re-probing should see the full weight cost again since nothing was committed. +TEST(ResourceAccountantTest, ComputeAndAccountForNode_CorrectAfterReset) { auto h = SharedWeightGraph::Create(); TestDedupAccountant accountant; accountant.RegisterWeight("weight_W", 1000); - // Probing pass populates the dedup set + // Probing pass populates pending weights auto cost_a = accountant.ComputeResourceCount(*h.node_a); EXPECT_EQ(std::get(cost_a), size_t{1000}); auto cost_b = accountant.ComputeResourceCount(*h.node_b); EXPECT_EQ(std::get(cost_b), size_t{0}); - // Old buggy path: re-compute on fused node after probing + // Discard the pass (simulating capabilities.clear() before second GetCapability) + accountant.ResetPendingWeights(); + + // Re-probe: weight_W was never committed, so it should be counted again IndexedSubGraph sub_graph; sub_graph.nodes.push_back(h.node_a->Index()); sub_graph.SetAccountant(&accountant); - sub_graph.ComputeAndAccountForNode(*h.node_a); + auto recomputed_cost = accountant.ComputeResourceCount(*h.node_a); + sub_graph.AccountForNode(h.node_a->Index(), recomputed_cost); - // Bug: consumed is 0 instead of 1000 - EXPECT_EQ(accountant.GetConsumedSizeT(), size_t{0}) - << "ComputeAndAccountForNode after probing under-counts"; + EXPECT_EQ(accountant.GetConsumedSizeT(), size_t{1000}) + << "After ResetPendingWeights, re-probe should see full weight cost"; } // Each node has a unique initializer. AccountForAllNodes sums both. @@ -298,4 +325,4 @@ TEST(ResourceAccountantTest, CrossSubGraph_DedupWorks) { } } // namespace test -} // namespace onnxruntime \ No newline at end of file +} // namespace onnxruntime From 09967c3fc5eba36df0f3e6cef673420bdc9c060a Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Thu, 26 Mar 2026 17:18:05 -0700 Subject: [PATCH 46/57] Add copyright header --- onnxruntime/python/tools/layering/layer_annotate.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/onnxruntime/python/tools/layering/layer_annotate.py b/onnxruntime/python/tools/layering/layer_annotate.py index 1295d33ff14c1..94979725430e3 100644 --- a/onnxruntime/python/tools/layering/layer_annotate.py +++ b/onnxruntime/python/tools/layering/layer_annotate.py @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + import argparse import logging import pathlib From d23ee080e54de5a5f65085d29d697e5ee3eedb66 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Fri, 27 Mar 2026 10:20:56 -0700 Subject: [PATCH 47/57] Update onnxruntime/core/framework/graph_partitioner.cc Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- onnxruntime/core/framework/graph_partitioner.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/onnxruntime/core/framework/graph_partitioner.cc b/onnxruntime/core/framework/graph_partitioner.cc index 6c01a947ed021..a32ecbc9bf42a 100644 --- a/onnxruntime/core/framework/graph_partitioner.cc +++ b/onnxruntime/core/framework/graph_partitioner.cc @@ -335,7 +335,9 @@ static Status GetCapabilityForEP(const GetCapabilityForEPParams& params, const l // with inherited annotations InlinedVector new_node_indices; for (NodeIndex idx = first_new_node; idx < end_node; ++idx) { - new_node_indices.push_back(idx); + if (graph.GetNode(idx) != nullptr) { + new_node_indices.push_back(idx); + } } params.layering_index->Update(graph, new_node_indices); } From 9e8be7a4a452b5fb3dcca9109532af544bbd5079 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Fri, 27 Mar 2026 10:31:07 -0700 Subject: [PATCH 48/57] Adjust doc and implementaton for fetching layering ann --- onnxruntime/core/framework/tensorprotoutils.cc | 5 ----- onnxruntime/core/framework/tensorprotoutils.h | 6 +++--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/onnxruntime/core/framework/tensorprotoutils.cc b/onnxruntime/core/framework/tensorprotoutils.cc index 061b329be3c1d..02cc5f52367e8 100644 --- a/onnxruntime/core/framework/tensorprotoutils.cc +++ b/onnxruntime/core/framework/tensorprotoutils.cc @@ -2536,11 +2536,6 @@ std::optional GetNodeProtoLayeringAnnotation(const ONNX_NAMESPACE:: std::optional result; for (const auto& prop : node_proto.metadata_props()) { if (prop.key() == kNodeProtoLayerAnnotation) { - if (++annotation_count > 1) { - // Multiple annotations found, this is unexpected - ORT_THROW("Multiple '", kNodeProtoLayerAnnotation, "' annotations found in NodeProto '", node_proto.name(), - "'. This is unexpected as only one annotation should be present."); - } if (!prop.value().empty()) { result = prop.value(); break; diff --git a/onnxruntime/core/framework/tensorprotoutils.h b/onnxruntime/core/framework/tensorprotoutils.h index fb9ba0f021122..8b22e8d6d1c89 100644 --- a/onnxruntime/core/framework/tensorprotoutils.h +++ b/onnxruntime/core/framework/tensorprotoutils.h @@ -676,9 +676,9 @@ constexpr const char* kNodeProtoLayerAnnotation = "layer_ann"; /** * This function examines the given node proto and looks into its metadata_props. - * It returns the first value found for the key kNodeProtoLayerAnnotation. - * Empty annotations are ignored. - * If no annotation is found, std::nullopt is returned. + * It returns the first non-empty value found for the key kNodeProtoLayerAnnotation. + * A node is expected to have only one such annotation. + * If no non-empty annotation is found, std::nullopt is returned. */ std::optional GetNodeProtoLayeringAnnotation(const ONNX_NAMESPACE::NodeProto& node_proto); } // namespace utils From f636138d80acdb0fde8fa0b84968f9d05bb54d89 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Fri, 27 Mar 2026 10:36:05 -0700 Subject: [PATCH 49/57] Make GetContainingGraph public --- include/onnxruntime/core/graph/graph.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/onnxruntime/core/graph/graph.h b/include/onnxruntime/core/graph/graph.h index 60492058bcae0..3f2cfae71c394 100644 --- a/include/onnxruntime/core/graph/graph.h +++ b/include/onnxruntime/core/graph/graph.h @@ -178,6 +178,8 @@ class Node { const std::string& GetLayeringAnnotation() const noexcept { return layering_annotation_; } + const Graph* GetContainingGraph() const noexcept { return graph_; } + #if !defined(ORT_MINIMAL_BUILD) /** Gets the Node's OpSchema. @@ -580,8 +582,6 @@ class Node { friend class Graph; Node(NodeIndex index, Graph& graph) : index_(index), graph_(&graph), can_be_saved_(true) {} - const Graph* GetContainingGraph() const noexcept { return graph_; } - protected: #if !defined(ORT_MINIMAL_BUILD) || defined(ORT_EXTENDED_MINIMAL_BUILD) // internal only method to allow selected classes to directly alter the input/output definitions and arg counts From fcda5245656bff3c4927b273d56d3753125de33d Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Fri, 27 Mar 2026 11:05:26 -0700 Subject: [PATCH 50/57] Adjust accounting for fused node and remove stray local var --- include/onnxruntime/core/graph/indexed_sub_graph.h | 4 ---- onnxruntime/core/framework/graph_partitioner.cc | 13 ++++++------- onnxruntime/core/framework/tensorprotoutils.cc | 1 - 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/include/onnxruntime/core/graph/indexed_sub_graph.h b/include/onnxruntime/core/graph/indexed_sub_graph.h index 8e1ae539bc764..54e878761ba87 100644 --- a/include/onnxruntime/core/graph/indexed_sub_graph.h +++ b/include/onnxruntime/core/graph/indexed_sub_graph.h @@ -118,10 +118,6 @@ struct IndexedSubGraph { resource_accountant = res_accountant; } - IResourceAccountant* GetAccountant() const noexcept { - return resource_accountant; - } - // Append resource count to the list of costs for the nodes. void AppendNodeCost(const ResourceCount& cost) { assert(resource_accountant != nullptr); diff --git a/onnxruntime/core/framework/graph_partitioner.cc b/onnxruntime/core/framework/graph_partitioner.cc index a32ecbc9bf42a..cc65142318d02 100644 --- a/onnxruntime/core/framework/graph_partitioner.cc +++ b/onnxruntime/core/framework/graph_partitioner.cc @@ -499,14 +499,13 @@ static Node* PlaceNode(Graph& graph, const IndexedSubGraph& capability, fused_node->SetExecutionProviderType(provider_type); if (acc_enabled) { - // We account for the fused node. We operate under assumption - // that the fused node would use no more memory when the nodes we are fusing. - // and potentially less than that, and therefore, no threshold check is needed here. - // All threshold checks are done within the EP. - auto resource_count = capability.GetAccountant()->ComputeResourceCount(*fused_node); - capability.AccountForNode(fused_node->Index(), resource_count); + // Account for all constituent nodes using the per-node costs computed + // during GetCapability() (which already includes within-pass weight dedup). + // Computing the cost for the newly created fused node would undercount + // because the fused node often doesn't expose all original initializers, + // and would commit weights for the wrong node index. + capability.AccountForAllNodes(); } - result = fused_node; } else { // assign the nodes in the indexed subgraph to the current EP so that level 2+ optimizers will not change them. diff --git a/onnxruntime/core/framework/tensorprotoutils.cc b/onnxruntime/core/framework/tensorprotoutils.cc index 02cc5f52367e8..74fbe4d24de96 100644 --- a/onnxruntime/core/framework/tensorprotoutils.cc +++ b/onnxruntime/core/framework/tensorprotoutils.cc @@ -2532,7 +2532,6 @@ Status UnpackInitializerData(const ONNX_NAMESPACE::TensorProto& initializer, std } std::optional GetNodeProtoLayeringAnnotation(const ONNX_NAMESPACE::NodeProto& node_proto) { - size_t annotation_count = 0; std::optional result; for (const auto& prop : node_proto.metadata_props()) { if (prop.key() == kNodeProtoLayerAnnotation) { From 2da1394ae9a5b2c9ccf5034629e8f90d23150dae Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Fri, 27 Mar 2026 11:13:43 -0700 Subject: [PATCH 51/57] Address flaky test --- .../test/framework/session_state_test.cc | 76 +++++++++++++++++-- 1 file changed, 70 insertions(+), 6 deletions(-) diff --git a/onnxruntime/test/framework/session_state_test.cc b/onnxruntime/test/framework/session_state_test.cc index 5095e0f219d5c..47c8014286891 100644 --- a/onnxruntime/test/framework/session_state_test.cc +++ b/onnxruntime/test/framework/session_state_test.cc @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#include #include #include @@ -13,6 +14,7 @@ #include "core/framework/op_kernel.h" #include "core/framework/bfc_arena.h" #include "core/framework/ep_context_options.h" +#include "core/framework/resource_accountant.h" #include "core/framework/session_state.h" #include "core/graph/graph_utils.h" #include "core/graph/graph_viewer.h" @@ -416,6 +418,45 @@ namespace { using ParitionVerifierFn = std::function; +// Collect unique node names from a graph and all its subgraphs +// using the same naming scheme as the resource accountant. +static void CollectNodeNames(const Graph& graph, std::vector& names) { + for (const auto& node : graph.Nodes()) { + names.push_back(IResourceAccountant::MakeUniqueNodeName(node)); + for (const auto& [_, subgraph] : node.GetAttributeNameToSubgraphMap()) { + CollectNodeNames(*subgraph, names); + } + } +} + +// Generates a node stats file dynamically from the current graph, +// assigning each node a fixed cost. Returns the total cost across +// all nodes so callers can choose a threshold relative to the actual total. +// This avoids relying on a pre-baked stats file whose node name hashes +// become stale when graph optimizers change node input/output names. +static size_t GenerateDynamicNodeStatsFile(const ORTCHAR_T* model_path, + const std::filesystem::path& output_path, + size_t cost_per_node = 1024) { + const auto& default_logger = DefaultLoggingManager().DefaultLogger(); + std::shared_ptr model; + EXPECT_STATUS_OK(Model::Load(model_path, model, nullptr, default_logger)); + Graph& graph = model->MainGraph(); + EXPECT_STATUS_OK(graph.Resolve()); + + std::vector node_names; + CollectNodeNames(graph, node_names); + + std::ofstream ofs(output_path); + EXPECT_TRUE(ofs.is_open()); + ofs << "#name,input_sizes,initializers_sizes,total_dynamic_sizes,total_temp_allocations\n"; + for (const auto& name : node_names) { + ofs << name << "," << cost_per_node << ",0,0,0\n"; + } + ofs.close(); + + return node_names.size() * cost_per_node; +} + void LoadWithResourceAwarePartitioning(const ORTCHAR_T* model_path, const SessionOptions& sess_options, const ParitionVerifierFn& verifier_fn, @@ -499,16 +540,25 @@ TEST(SessionStateTest, TestResourceAwarePartitioning_NoLimit) { TEST(SessionStateTest, TestResourceAwarePartitioning_LargeLimit) { constexpr const ORTCHAR_T* model_path = ORT_TSTR("testdata/transformers/tiny_gpt2_beamsearch.onnx"); - constexpr const char* limit_setting = "10000,tiny_gpt2_beamsearch_node_stats.txt"; + const std::filesystem::path stats_path = + std::filesystem::temp_directory_path() / "tiny_gpt2_beamsearch_dynamic_stats_large.txt"; + + // Generate node stats dynamically so names always match the current graph + constexpr size_t cost_per_node = 1024; + size_t total_cost = GenerateDynamicNodeStatsFile(model_path, stats_path, cost_per_node); + ASSERT_GT(total_cost, 0U); + + // Use a limit much larger than total cost so all nodes are assigned CUDA. + size_t large_limit_kb = (total_cost * 2) / 1024 + 1; + std::string limit_setting = std::to_string(large_limit_kb) + "," + stats_path.string(); - // Large limit, all nodes are still assigned SessionOptions sess_options; sess_options.enable_mem_pattern = false; sess_options.execution_mode = ExecutionMode::ORT_SEQUENTIAL; sess_options.use_deterministic_compute = false; sess_options.enable_mem_reuse = false; ASSERT_STATUS_OK(sess_options.config_options.AddConfigEntry( - kOrtSessionOptionsResourceCudaPartitioningSettings, limit_setting)); + kOrtSessionOptionsResourceCudaPartitioningSettings, limit_setting.c_str())); LoadWithResourceAwarePartitioning(model_path, sess_options, [](const Graph& graph) { const auto& graph_nodes = graph.Nodes(); @@ -516,20 +566,32 @@ TEST(SessionStateTest, TestResourceAwarePartitioning_LargeLimit) { EXPECT_EQ(node.GetExecutionProviderType(), kCudaExecutionProvider); } }); + + std::filesystem::remove(stats_path); } TEST(SessionStateTest, TestResourceAwarePartitioning_CPUOffloaded) { constexpr const ORTCHAR_T* model_path = ORT_TSTR("testdata/transformers/tiny_gpt2_beamsearch.onnx"); - constexpr const char* limit_setting = "5000,tiny_gpt2_beamsearch_node_stats.txt"; + const std::filesystem::path stats_path = + std::filesystem::temp_directory_path() / "tiny_gpt2_beamsearch_dynamic_stats_offload.txt"; + + // Generate node stats dynamically so names always match the current graph. + constexpr size_t cost_per_node = 1024; + size_t total_cost = GenerateDynamicNodeStatsFile(model_path, stats_path, cost_per_node); + ASSERT_GT(total_cost, 0U); + + // Set threshold to half the total cost so some nodes must be offloaded to CPU. + size_t half_limit_kb = (total_cost / 2) / 1024; + ASSERT_GT(half_limit_kb, 0U); + std::string limit_setting = std::to_string(half_limit_kb) + "," + stats_path.string(); - // Limit is smaller, we expect some nodes to be offloaded to CPU. SessionOptions sess_options; sess_options.enable_mem_pattern = false; sess_options.execution_mode = ExecutionMode::ORT_SEQUENTIAL; sess_options.use_deterministic_compute = false; sess_options.enable_mem_reuse = false; ASSERT_STATUS_OK(sess_options.config_options.AddConfigEntry( - kOrtSessionOptionsResourceCudaPartitioningSettings, limit_setting)); + kOrtSessionOptionsResourceCudaPartitioningSettings, limit_setting.c_str())); LoadWithResourceAwarePartitioning(model_path, sess_options, [](const Graph& graph) { const auto& graph_nodes = graph.Nodes(); @@ -542,6 +604,8 @@ TEST(SessionStateTest, TestResourceAwarePartitioning_CPUOffloaded) { } EXPECT_TRUE(cpu_node_found); }); + + std::filesystem::remove(stats_path); } TEST(SessionStateTest, TestLayeringPartitioning) { From c0c5e519c8e541d27fc4977d37e18786af0afe18 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Fri, 27 Mar 2026 11:32:21 -0700 Subject: [PATCH 52/57] Update onnxruntime/core/providers/cuda/cuda_execution_provider.cc Adjust warning Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- onnxruntime/core/providers/cuda/cuda_execution_provider.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/onnxruntime/core/providers/cuda/cuda_execution_provider.cc b/onnxruntime/core/providers/cuda/cuda_execution_provider.cc index 7c870bec84cf2..acd652cdfe346 100755 --- a/onnxruntime/core/providers/cuda/cuda_execution_provider.cc +++ b/onnxruntime/core/providers/cuda/cuda_execution_provider.cc @@ -3115,7 +3115,7 @@ CUDAExecutionProvider::GetCapability(const onnxruntime::GraphViewer& graph, << "CUDA_EP failed to get available GPU memory info. Using info_.gpu_mem_limit instead: " << info_.gpu_mem_limit; } else { memory_threshold = std::min(free_memory, info_.gpu_mem_limit); - LOGS(logger, WARNING) + LOGS(logger, VERBOSE) << "CUDA_EP Using threshold: " << memory_threshold << " Free memory reported: " << free_memory; } } else { From c55bb6ece54151ce94ea14b68e196bf406b14113 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Fri, 27 Mar 2026 12:46:43 -0700 Subject: [PATCH 53/57] Update onnxruntime/core/graph/graph_utils.cc Adjust ordering Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- onnxruntime/core/graph/graph_utils.cc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/onnxruntime/core/graph/graph_utils.cc b/onnxruntime/core/graph/graph_utils.cc index 00243574dffdf..85de654581161 100644 --- a/onnxruntime/core/graph/graph_utils.cc +++ b/onnxruntime/core/graph/graph_utils.cc @@ -84,7 +84,10 @@ Status CreateFilteredIndexedGraph(gsl::span nodes, const Grap // If not produced by this subgraph, it's a boundary input if (internal_outputs.count(input) == 0) { // Use insert to keep the first occurrence's order - subgraph_inputs.emplace(input, input_order++); + auto emplace_result = subgraph_inputs.emplace(input, input_order); + if (emplace_result.second) { + ++input_order; + } } } }; From cdd9faa83083e5967a0529fcad81f7c30814e421 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Fri, 27 Mar 2026 14:02:07 -0700 Subject: [PATCH 54/57] Address review issues --- .../core/providers/cuda/cuda_execution_provider.cc | 2 +- onnxruntime/core/session/onnxruntime_c_api.cc | 5 ++++- onnxruntime/test/framework/session_state_test.cc | 8 ++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/onnxruntime/core/providers/cuda/cuda_execution_provider.cc b/onnxruntime/core/providers/cuda/cuda_execution_provider.cc index acd652cdfe346..c6354b1c533cd 100755 --- a/onnxruntime/core/providers/cuda/cuda_execution_provider.cc +++ b/onnxruntime/core/providers/cuda/cuda_execution_provider.cc @@ -3111,7 +3111,7 @@ CUDAExecutionProvider::GetCapability(const onnxruntime::GraphViewer& graph, size_t free_memory, total_memory; if (0 != cudaMemGetInfo(&free_memory, &total_memory)) { memory_threshold = info_.gpu_mem_limit; - LOGS(logger, WARNING) + LOGS(logger, INFO) << "CUDA_EP failed to get available GPU memory info. Using info_.gpu_mem_limit instead: " << info_.gpu_mem_limit; } else { memory_threshold = std::min(free_memory, info_.gpu_mem_limit); diff --git a/onnxruntime/core/session/onnxruntime_c_api.cc b/onnxruntime/core/session/onnxruntime_c_api.cc index ec9505308809d..9834902cea2b1 100644 --- a/onnxruntime/core/session/onnxruntime_c_api.cc +++ b/onnxruntime/core/session/onnxruntime_c_api.cc @@ -3099,7 +3099,10 @@ ORT_API_STATUS_IMPL(OrtApis::Graph_GetGraphView, _In_ const OrtGraph* src_graph, // If not produced by this subgraph, it's a boundary input if (internal_outputs.count(input) == 0) { // Use insert to keep the first occurrence's order - subgraph_inputs.emplace(input, input_order++); + auto p = subgraph_inputs.emplace(input, input_order); + if (p.second) { + input_order++; + } } } }; diff --git a/onnxruntime/test/framework/session_state_test.cc b/onnxruntime/test/framework/session_state_test.cc index 47c8014286891..48779cc721025 100644 --- a/onnxruntime/test/framework/session_state_test.cc +++ b/onnxruntime/test/framework/session_state_test.cc @@ -540,8 +540,10 @@ TEST(SessionStateTest, TestResourceAwarePartitioning_NoLimit) { TEST(SessionStateTest, TestResourceAwarePartitioning_LargeLimit) { constexpr const ORTCHAR_T* model_path = ORT_TSTR("testdata/transformers/tiny_gpt2_beamsearch.onnx"); + std::error_code ec; const std::filesystem::path stats_path = - std::filesystem::temp_directory_path() / "tiny_gpt2_beamsearch_dynamic_stats_large.txt"; + std::filesystem::temp_directory_path(ec) / "tiny_gpt2_beamsearch_dynamic_stats_large.txt"; + ASSERT_FALSE(ec) << "temp_directory_path failed: " << ec.message(); // Generate node stats dynamically so names always match the current graph constexpr size_t cost_per_node = 1024; @@ -572,8 +574,10 @@ TEST(SessionStateTest, TestResourceAwarePartitioning_LargeLimit) { TEST(SessionStateTest, TestResourceAwarePartitioning_CPUOffloaded) { constexpr const ORTCHAR_T* model_path = ORT_TSTR("testdata/transformers/tiny_gpt2_beamsearch.onnx"); + std::error_code ec; const std::filesystem::path stats_path = - std::filesystem::temp_directory_path() / "tiny_gpt2_beamsearch_dynamic_stats_offload.txt"; + std::filesystem::temp_directory_path(ec) / "tiny_gpt2_beamsearch_dynamic_stats_offload.txt"; + ASSERT_FALSE(ec) << "temp_directory_path failed: " << ec.message(); // Generate node stats dynamically so names always match the current graph. constexpr size_t cost_per_node = 1024; From 67a947b4c29039a9811b21c2a613dc02169af677 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Fri, 27 Mar 2026 14:13:16 -0700 Subject: [PATCH 55/57] Fix potential perf issue --- .../core/framework/resource_accountant.cc | 19 +++++++++---------- .../framework/resource_accountant_test.cc | 15 +++++++-------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/onnxruntime/core/framework/resource_accountant.cc b/onnxruntime/core/framework/resource_accountant.cc index e50921fb75654..68610ebb4be17 100644 --- a/onnxruntime/core/framework/resource_accountant.cc +++ b/onnxruntime/core/framework/resource_accountant.cc @@ -82,7 +82,7 @@ class SizeBasedStatsAccountant : public IResourceAccountant { } // Skip if already pending from another node in this GetCapability pass - if (IsInPendingWeights(name)) { + if (pending_weights_.count(name) > 0) { continue; } @@ -91,6 +91,7 @@ class SizeBasedStatsAccountant : public IResourceAccountant { if (status.IsOK()) { total_size += size; + pending_weights_.insert(name); pending_weights_by_node_[node.Index()].insert(name); } } @@ -120,32 +121,30 @@ class SizeBasedStatsAccountant : public IResourceAccountant { } void ResetPendingWeights() override { + pending_weights_.clear(); pending_weights_by_node_.clear(); } void CommitWeightsForNode(NodeIndex node_index) override { auto it = pending_weights_by_node_.find(node_index); if (it != pending_weights_by_node_.end()) { + for (const auto& name : it->second) { + pending_weights_.erase(name); + } committed_weights_.insert(it->second.begin(), it->second.end()); pending_weights_by_node_.erase(it); } } private: - bool IsInPendingWeights(const std::string& name) const { - for (const auto& [node_idx, weights] : pending_weights_by_node_) { - if (weights.count(name) > 0) return true; - } - return false; - } - size_t consumed_amount_ = 0; std::optional> node_stats_; // Weights committed from previous partitioning iterations. // These persist across GetCapability passes. InlinedHashSet committed_weights_; - // Weights seen during the current GetCapability pass, keyed by node index. - // Discarded by ResetPendingWeights() or promoted by CommitWeightsForNode(). + // Flat set of all pending weight names for O(1) membership checks. + InlinedHashSet pending_weights_; + // Same pending weights keyed by node index, used by CommitWeightsForNode. InlinedHashMap> pending_weights_by_node_; }; diff --git a/onnxruntime/test/framework/resource_accountant_test.cc b/onnxruntime/test/framework/resource_accountant_test.cc index cc25183b04596..a102fe4e7770b 100644 --- a/onnxruntime/test/framework/resource_accountant_test.cc +++ b/onnxruntime/test/framework/resource_accountant_test.cc @@ -56,13 +56,14 @@ class TestDedupAccountant : public IResourceAccountant { if (committed_weights_.count(name) > 0) { continue; } - if (IsInPendingWeights(name)) { + if (pending_weights_.count(name) > 0) { continue; } auto it = weight_sizes_.find(name); if (it != weight_sizes_.end()) { total += it->second; } + pending_weights_.insert(name); pending_weights_by_node_[node.Index()].insert(name); } } @@ -70,12 +71,16 @@ class TestDedupAccountant : public IResourceAccountant { } void ResetPendingWeights() override { + pending_weights_.clear(); pending_weights_by_node_.clear(); } void CommitWeightsForNode(NodeIndex node_index) override { auto it = pending_weights_by_node_.find(node_index); if (it != pending_weights_by_node_.end()) { + for (const auto& name : it->second) { + pending_weights_.erase(name); + } committed_weights_.insert(it->second.begin(), it->second.end()); pending_weights_by_node_.erase(it); } @@ -88,15 +93,9 @@ class TestDedupAccountant : public IResourceAccountant { size_t GetConsumedSizeT() const { return consumed_; } private: - bool IsInPendingWeights(const std::string& name) const { - for (const auto& [node_idx, weights] : pending_weights_by_node_) { - if (weights.count(name) > 0) return true; - } - return false; - } - size_t consumed_ = 0; InlinedHashSet committed_weights_; + InlinedHashSet pending_weights_; InlinedHashMap> pending_weights_by_node_; InlinedHashMap weight_sizes_; }; From 44c690463c0b01750267afe5f47b637e966551ac Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Fri, 27 Mar 2026 14:37:01 -0700 Subject: [PATCH 56/57] Address review comments --- onnxruntime/python/tools/layering/layer_annotate.py | 3 ++- onnxruntime/test/framework/session_state_test.cc | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/onnxruntime/python/tools/layering/layer_annotate.py b/onnxruntime/python/tools/layering/layer_annotate.py index 94979725430e3..738c528b28754 100644 --- a/onnxruntime/python/tools/layering/layer_annotate.py +++ b/onnxruntime/python/tools/layering/layer_annotate.py @@ -138,7 +138,8 @@ def annotate_model(model, substring_annotations): Annotates an ONNX model with metadata based on a provided mapping. This function serves as the entry point to annotate the model's graph. - It delegates the work to `annotate_graph` enabling parallel processing for the main graph. + It delegates the work to `annotate_graph`, which recursively processes + all nodes in the main graph and any nested subgraphs. Args: model (onnx.ModelProto): The ONNX model to annotate. diff --git a/onnxruntime/test/framework/session_state_test.cc b/onnxruntime/test/framework/session_state_test.cc index 48779cc721025..de302131a2fd3 100644 --- a/onnxruntime/test/framework/session_state_test.cc +++ b/onnxruntime/test/framework/session_state_test.cc @@ -439,15 +439,15 @@ static size_t GenerateDynamicNodeStatsFile(const ORTCHAR_T* model_path, size_t cost_per_node = 1024) { const auto& default_logger = DefaultLoggingManager().DefaultLogger(); std::shared_ptr model; - EXPECT_STATUS_OK(Model::Load(model_path, model, nullptr, default_logger)); + ASSERT_STATUS_OK(Model::Load(model_path, model, nullptr, default_logger)); Graph& graph = model->MainGraph(); - EXPECT_STATUS_OK(graph.Resolve()); + ASSERT_STATUS_OK(graph.Resolve()); std::vector node_names; CollectNodeNames(graph, node_names); std::ofstream ofs(output_path); - EXPECT_TRUE(ofs.is_open()); + ASSERT_TRUE(ofs.is_open()); ofs << "#name,input_sizes,initializers_sizes,total_dynamic_sizes,total_temp_allocations\n"; for (const auto& name : node_names) { ofs << name << "," << cost_per_node << ",0,0,0\n"; @@ -497,7 +497,9 @@ void LoadWithResourceAwarePartitioning(const ORTCHAR_T* model_path, if (!layering_config.empty()) { ASSERT_STATUS_OK(LayeringIndex::Create(graph, layering_config, {}, execution_providers, default_logger, layering_index_storage)); - layering_index = &layering_index_storage.value(); + if (layering_index_storage.has_value()) { + layering_index = &layering_index_storage.value(); + } } // Create GraphOptimizerRegistry instance for providing predefined graph optimizers and selection functions for EPs to lookup From 927a0efa15ca2a2f5af090430a0db4c4f346c0f8 Mon Sep 17 00:00:00 2001 From: Dmitri Smirnov Date: Fri, 27 Mar 2026 16:47:29 -0700 Subject: [PATCH 57/57] Add documentation for ann and ep propagation. Fix L1 optimizers, add new AddNode(), fix sesstion state test --- docs/Optimizer_Layering_Annotations.md | 130 ++++++++++++++++++ include/onnxruntime/core/graph/graph.h | 80 +++++++++++ onnxruntime/core/graph/graph.cc | 32 +++++ .../core/optimizer/dq_matmulnbits_fusion.cc | 4 +- .../core/optimizer/embed_layer_norm_fusion.cc | 6 +- onnxruntime/core/optimizer/gelu_fusion.cc | 2 +- onnxruntime/core/optimizer/gemm_sum_fusion.cc | 3 +- .../core/optimizer/gemm_transpose_fusion.cc | 3 +- .../core/optimizer/layer_norm_fusion.cc | 4 +- .../core/optimizer/matmul_add_fusion.cc | 10 +- .../core/optimizer/matmul_bn_fusion.cc | 1 + .../ensure_unique_dq_for_node_unit.cc | 2 +- .../weight_bias_quantization.cc | 21 +-- .../qdq_transformer/where_dummy_dq.cc | 3 +- onnxruntime/core/optimizer/reshape_fusion.cc | 4 +- .../slice_concat_to_space_to_depth_fusion.cc | 2 + .../core/optimizer/stft_decomposition.cc | 68 +++++---- .../test/framework/session_state_test.cc | 21 +-- 18 files changed, 326 insertions(+), 70 deletions(-) create mode 100644 docs/Optimizer_Layering_Annotations.md diff --git a/docs/Optimizer_Layering_Annotations.md b/docs/Optimizer_Layering_Annotations.md new file mode 100644 index 0000000000000..a268bd8fbe84f --- /dev/null +++ b/docs/Optimizer_Layering_Annotations.md @@ -0,0 +1,130 @@ +# Optimizer Layering Annotations + +## Overview + +Layering annotations are per-node metadata strings that guide graph partitioning by indicating which execution provider (EP) layer a node belongs to. They are loaded from the ONNX model's `NodeProto` metadata (key `"layer_ann"`) and consumed during the partitioning phase to influence EP assignment. + +## Execution Pipeline + +Graph optimizers run in ordered levels: + +``` +Level 0 (Basic) ─► Level 1 (Extended) ─► Partitioning ─► Level 2+ (Layout, etc.) +``` + +1. **Level 0 and Level 1** optimizers run **before** partitioning. At this point, layering annotations are present on nodes and must be preserved through any graph transformations. +2. **Partitioning** reads the annotations to assign nodes to execution providers. +3. After partitioning, `Graph::RemoveAllLayeringAnnotations()` clears all annotations. +4. **Level 2, 3, and 4** optimizers run **after** annotations have been cleared. They do not need to handle annotations. + +**Key rule: Only Level 1 (and Level 0) optimizers need to propagate layering annotations.** + +## Why Propagation Matters + +When an optimizer replaces, fuses, or decomposes nodes, the original annotated node is removed and new nodes are created. If the new nodes do not carry the original annotation, partitioning loses the assignment hint for that subgraph, potentially causing incorrect EP placement. + +## How to Propagate Annotations + +### Preferred: Use the `AddNode` Overload with `annotation_source` + +`Graph::AddNode` provides overloads that accept a `const Node& annotation_source` parameter. The new node automatically inherits the layering annotation from the source node. + +```cpp +// Instead of: +Node& new_node = graph.AddNode(name, op_type, description, inputs, outputs); +// Missing annotation propagation! + +// Use: +Node& new_node = graph.AddNode(name, op_type, description, inputs, outputs, + original_node); // annotation_source +``` + +All standard `AddNode` signatures have a corresponding `annotation_source` variant: + +```cpp +// With const NodeAttributes* +Node& AddNode(name, op_type, description, + gsl::span inputs, + gsl::span outputs, + const Node& annotation_source, + const NodeAttributes* attributes = nullptr, + const std::string& domain = kOnnxDomain); + +// With NodeAttributes&& +Node& AddNode(name, op_type, description, + gsl::span inputs, + gsl::span outputs, + const Node& annotation_source, + NodeAttributes&& attributes, + const std::string& domain = kOnnxDomain); + +// initializer_list variants also available +``` + +### Legacy: `DuplicateNodeAnnotation` + +The utility function `optimizer_utils::DuplicateNodeAnnotation(src, dst)` copies annotations between existing nodes. This is still used when the annotation source is conditional (e.g., when the source node pointer may be null). Prefer the `AddNode` overload for unconditional propagation. + +### Automatic Propagation + +`Graph::AddNode(const Node& other)` — the copy overload used for duplicating nodes — automatically copies annotations. No additional action is needed when duplicating a node via this overload. + +## Post-Partitioning: Propagating EP Assignments + +Although Level 2+ optimizers do not deal with layering annotations directly (they have been cleared), they must still propagate **execution provider (EP) assignments**. EP assignments are the downstream result of the annotation-driven partitioning step. After partitioning, each node carries an EP assignment (e.g., `CUDAExecutionProvider`, `CPUExecutionProvider`) that determines where the node's kernel runs. + +When a Level 2+ optimizer creates new nodes that replace or derive from existing ones, it must copy the EP assignment from the source node: + +```cpp +Node& new_node = graph.AddNode(name, op_type, description, inputs, outputs); +new_node.SetExecutionProviderType(original_node.GetExecutionProviderType()); +``` + +Failing to propagate the EP assignment causes the new node to fall back to the default provider (typically CPU), silently breaking the intended placement and potentially degrading performance or correctness. This requirement predates the layering annotation feature and applies to all optimizers that run after partitioning. + +> **Note:** The `AddNode` overload with `annotation_source` propagates both the layering annotation *and* nothing else — EP assignment is still set separately. Layering annotations and EP assignments serve different stages of the pipeline and are managed independently. + +## When You Do NOT Need to Propagate Annotations + +- **Level 2+ optimizers** — annotations have already been consumed and cleared (but EP assignments must still be propagated, see above). +- **Training optimizers** — training runs after partitioning. +- **Optimizers that only remove nodes** (e.g., identity elimination) — no new nodes are created. +- **Optimizers that modify nodes in-place** — the annotation remains on the existing node. + +## Examples + +### Fusion (replacing multiple nodes with one) + +```cpp +// GeluFusion: fusing Div + Erf + Add + Mul + Mul into a single Gelu +Node& gelu_node = graph.AddNode( + graph.GenerateNodeName("Gelu"), + "Gelu", "fused Gelu subgraphs", + {gelu_input}, {gelu_output}, + div_node); // propagate annotation from the root matched node +``` + +### Decomposition (replacing one node with many) + +```cpp +// STFT decomposition: each new node inherits from the original STFT node +auto [reshape_node, reshape_out] = AddNode(graph, "Reshape", ep, inputs, &stft); +auto [conv_node, conv_out] = AddNode(graph, "Conv", ep, conv_inputs, &stft); +auto [concat_node, concat_out] = AddNode(graph, "Concat", ep, concat_inputs, &stft); +``` + +### Conditional source (use DuplicateNodeAnnotation) + +```cpp +Node& q_node = graph.AddNode(...); +if (src_node) { + optimizer_utils::DuplicateNodeAnnotation(*src_node, q_node); +} +``` + +## Checklist for New Level 1 Optimizers + +1. Identify the "source" node whose annotation should propagate (typically the root of the matched pattern). +2. For every `graph.AddNode(...)` call that creates a replacement node, use the `annotation_source` overload. +3. If the source is conditional (may be null), use `optimizer_utils::DuplicateNodeAnnotation` after the `AddNode` call. +4. Test with an annotated model to verify annotations survive the transformation. diff --git a/include/onnxruntime/core/graph/graph.h b/include/onnxruntime/core/graph/graph.h index 3f2cfae71c394..c5351bc5dfef7 100644 --- a/include/onnxruntime/core/graph/graph.h +++ b/include/onnxruntime/core/graph/graph.h @@ -1060,6 +1060,41 @@ class Graph { // NOLINT(clang-analyzer-optin.performance.Padding): preserve exi gsl::span output_args, NodeAttributes&& attributes, const std::string& domain = kOnnxDomain); + + /** Add a Node to this Graph, propagating the layering annotation from an existing node. + This is the preferred way to create new nodes in Level 1 (pre-partitioning) graph optimizers. + The new node automatically inherits the layering annotation from @p annotation_source, which + ensures correct layer-based partitioning when annotations are present. + @param name The Node name. Must be unique in this Graph. + @param op_type The operator type. e.g. ONNX operator name. + @param description Arbitrary description of the Node. + @param input_args The explicit inputs to this Node. + @param output_args The outputs from this Node. + @param annotation_source The node from which to inherit the layering annotation. + @param attributes Optional NodeAttributes to add. + @param domain The domain for the op_type. + @returns Reference to the new Node. + @remarks Use this overload in Level 1 optimizers that create nodes replacing or derived from + existing annotated nodes. See docs/Optimizer_Layering_Annotations.md for details. + */ + Node& AddNode(const std::string& name, + const std::string& op_type, + const std::string& description, + gsl::span input_args, + gsl::span output_args, + const Node& annotation_source, + const NodeAttributes* attributes = nullptr, + const std::string& domain = kOnnxDomain); + + Node& AddNode(const std::string& name, + const std::string& op_type, + const std::string& description, + gsl::span input_args, + gsl::span output_args, + const Node& annotation_source, + NodeAttributes&& attributes, + const std::string& domain = kOnnxDomain); + Node& AddNode(const std::string& name, const std::string& op_type, const std::string& description, @@ -1073,16 +1108,59 @@ class Graph { // NOLINT(clang-analyzer-optin.performance.Padding): preserve exi attributes, domain); } + Node& AddNode(const std::string& name, + const std::string& op_type, + const std::string& description, + std::initializer_list input_args, + std::initializer_list output_args, + const Node& annotation_source, + const NodeAttributes* attributes = nullptr, + const std::string& domain = kOnnxDomain) { + return AddNode(name, op_type, description, + AsSpan(input_args), + AsSpan(output_args), + annotation_source, + attributes, domain); + } + + Node& AddNode(const std::string& name, + const std::string& op_type, + const std::string& description, + gsl::span input_args, + std::initializer_list output_args, + const NodeAttributes* attributes = nullptr, + const std::string& domain = kOnnxDomain) { + return AddNode(name, op_type, description, + input_args, + AsSpan(output_args), + attributes, domain); + } + Node& AddNode(const std::string& name, const std::string& op_type, const std::string& description, gsl::span input_args, std::initializer_list output_args, + const Node& annotation_source, const NodeAttributes* attributes = nullptr, const std::string& domain = kOnnxDomain) { return AddNode(name, op_type, description, input_args, AsSpan(output_args), + annotation_source, + attributes, domain); + } + + Node& AddNode(const std::string& name, + const std::string& op_type, + const std::string& description, + std::initializer_list input_args, + gsl::span output_args, + const NodeAttributes* attributes = nullptr, + const std::string& domain = kOnnxDomain) { + return AddNode(name, op_type, description, + AsSpan(input_args), + output_args, attributes, domain); } @@ -1091,11 +1169,13 @@ class Graph { // NOLINT(clang-analyzer-optin.performance.Padding): preserve exi const std::string& description, std::initializer_list input_args, gsl::span output_args, + const Node& annotation_source, const NodeAttributes* attributes = nullptr, const std::string& domain = kOnnxDomain) { return AddNode(name, op_type, description, AsSpan(input_args), output_args, + annotation_source, attributes, domain); } diff --git a/onnxruntime/core/graph/graph.cc b/onnxruntime/core/graph/graph.cc index 56a3bd3771c94..e7da5a16930c6 100644 --- a/onnxruntime/core/graph/graph.cc +++ b/onnxruntime/core/graph/graph.cc @@ -4658,6 +4658,38 @@ Node& Graph::AddNode(const std::string& name, return *node; } +Node& Graph::AddNode(const std::string& name, + const std::string& op_type, + const std::string& description, + gsl::span input_args, + gsl::span output_args, + const Node& annotation_source, + const NodeAttributes* attributes, + const std::string& domain) { + auto& new_node = AddNode(name, op_type, description, input_args, output_args, attributes, domain); + const auto& annotation = annotation_source.GetLayeringAnnotation(); + if (!annotation.empty()) { + new_node.SetLayeringAnnotation(annotation); + } + return new_node; +} + +Node& Graph::AddNode(const std::string& name, + const std::string& op_type, + const std::string& description, + gsl::span input_args, + gsl::span output_args, + const Node& annotation_source, + NodeAttributes&& attributes, + const std::string& domain) { + auto& new_node = AddNode(name, op_type, description, input_args, output_args, std::move(attributes), domain); + const auto& annotation = annotation_source.GetLayeringAnnotation(); + if (!annotation.empty()) { + new_node.SetLayeringAnnotation(annotation); + } + return new_node; +} + bool Graph::RemoveNode(NodeIndex p_index) { auto node = GetNode(p_index); if (nullptr == node) { diff --git a/onnxruntime/core/optimizer/dq_matmulnbits_fusion.cc b/onnxruntime/core/optimizer/dq_matmulnbits_fusion.cc index f9ae13808cf2c..f3956d5e9e0f3 100644 --- a/onnxruntime/core/optimizer/dq_matmulnbits_fusion.cc +++ b/onnxruntime/core/optimizer/dq_matmulnbits_fusion.cc @@ -605,7 +605,7 @@ void ApplyReshapeTransposeFusions( graph.GenerateNodeName("DQFusedMatMulNBits"), "MatMulNBits", "Fused from DQ+Reshape+Transpose+MatMul", - mnb_inputs, mnb_outputs, &mnb_attrs, kMSDomain); + mnb_inputs, mnb_outputs, *mm_node, &mnb_attrs, kMSDomain); mnb_node.SetExecutionProviderType(mm_node->GetExecutionProviderType()); graph_utils::RemoveNodeOutputEdges(graph, *graph.GetNode(match.matmul_idx)); @@ -784,7 +784,7 @@ void ApplyDirectDQFusions( graph.GenerateNodeName("DirectDQFusedMatMulNBits"), "MatMulNBits", "Fused from direct DQ(axis=0)+MatMul", - mnb_inputs, mnb_outputs, &mnb_attrs, kMSDomain); + mnb_inputs, mnb_outputs, *mm_node, &mnb_attrs, kMSDomain); mnb_node.SetExecutionProviderType(mm_node->GetExecutionProviderType()); graph_utils::RemoveNodeOutputEdges(graph, *graph.GetNode(match.matmul_idx)); diff --git a/onnxruntime/core/optimizer/embed_layer_norm_fusion.cc b/onnxruntime/core/optimizer/embed_layer_norm_fusion.cc index e13b8a16b8c81..606e91ce91bbb 100644 --- a/onnxruntime/core/optimizer/embed_layer_norm_fusion.cc +++ b/onnxruntime/core/optimizer/embed_layer_norm_fusion.cc @@ -36,13 +36,12 @@ static NodeArg* CastToInt32(Graph& graph, NodeArg* input, const Node& source_nod "Cast Input from int64 to int32", std::array{input}, std::array{&cast32}, + source_node, nullptr, kOnnxDomain); // Add attribute: "to" = 6 node.AddAttribute("to", int64_t{ONNX_NAMESPACE::TensorProto_DataType_INT32}); - - optimizer_utils::DuplicateNodeAnnotation(source_node, node); node.SetExecutionProviderType(source_node.GetExecutionProviderType()); return &cast32; } @@ -515,7 +514,7 @@ static void CreateEmbedLayernormNode(Graph& graph, "fused EmbedLayerNorm subgraphs ", embed_layer_norm_input_defs, std::array{layer_norm_node.MutableOutputDefs()[0], &mask_index}, - {}, kMSDomain); + layer_norm_node, nullptr, kMSDomain); // Get attribute "epsilon" from "LayerNormalization" node if available. Else, default value // will be used. @@ -528,7 +527,6 @@ static void CreateEmbedLayernormNode(Graph& graph, } // Assign provider to this new node. Provider should be same as the provider for old node. - optimizer_utils::DuplicateNodeAnnotation(layer_norm_node, embed_layer_norm_node); embed_layer_norm_node.SetExecutionProviderType(layer_norm_node.GetExecutionProviderType()); } diff --git a/onnxruntime/core/optimizer/gelu_fusion.cc b/onnxruntime/core/optimizer/gelu_fusion.cc index 641bfbf388623..e2f448bf70734 100644 --- a/onnxruntime/core/optimizer/gelu_fusion.cc +++ b/onnxruntime/core/optimizer/gelu_fusion.cc @@ -178,7 +178,7 @@ Status GeluFusion::ApplyImpl(Graph& graph, bool& modified, int graph_level, cons "Gelu", "fused Gelu subgraphs ", gelu_input_defs, - {}, {}, op_domain); + {}, div, nullptr, op_domain); // Assign provider to this new node. Provider should be same as the provider for old node. gelu_node.SetExecutionProviderType(div.GetExecutionProviderType()); diff --git a/onnxruntime/core/optimizer/gemm_sum_fusion.cc b/onnxruntime/core/optimizer/gemm_sum_fusion.cc index be3c90a822fe2..c84e34a6d0dbe 100644 --- a/onnxruntime/core/optimizer/gemm_sum_fusion.cc +++ b/onnxruntime/core/optimizer/gemm_sum_fusion.cc @@ -41,7 +41,8 @@ Status GemmSumFusion::Apply(Graph& graph, Node& gemm_node, RewriteRuleEffect& mo "Fused Gemm with Sum", new_gemm_input_defs, new_gemm_output_defs, - {}, + gemm_node, + nullptr, gemm_node.Domain()); new_gemm_node.AddAttribute("transA", static_cast(transA)); new_gemm_node.AddAttribute("transB", static_cast(transB)); diff --git a/onnxruntime/core/optimizer/gemm_transpose_fusion.cc b/onnxruntime/core/optimizer/gemm_transpose_fusion.cc index da454b67aecf4..a66ad987cfaef 100644 --- a/onnxruntime/core/optimizer/gemm_transpose_fusion.cc +++ b/onnxruntime/core/optimizer/gemm_transpose_fusion.cc @@ -80,7 +80,8 @@ Status GemmTransposeFusion::Apply(Graph& graph, Node& node, RewriteRuleEffect& m "Fused Gemm with Transpose", new_gemm_input_defs, {}, - {}, + gemm_node, + nullptr, gemm_node.Domain()); new_gemm_node.AddAttribute("transA", static_cast(transA)); new_gemm_node.AddAttribute("transB", static_cast(transB)); diff --git a/onnxruntime/core/optimizer/layer_norm_fusion.cc b/onnxruntime/core/optimizer/layer_norm_fusion.cc index 3ade3864255ea..c10e070ef8f09 100644 --- a/onnxruntime/core/optimizer/layer_norm_fusion.cc +++ b/onnxruntime/core/optimizer/layer_norm_fusion.cc @@ -474,7 +474,7 @@ Status LayerNormFusion::ApplyImpl(Graph& graph, bool& modified, int graph_level, "LayerNormalization", "fused LayerNorm subgraphs ", layer_norm_input_defs, - {}, {}, kOnnxDomain); + {}, mul_node, nullptr, kOnnxDomain); // Get constant "epsilon" from "Add2" node if available. Else, default value will be used. const ONNX_NAMESPACE::TensorProto* tensor_proto = graph_utils::GetConstantInitializer(graph, add2_node.MutableInputDefs()[1]->Name()); @@ -719,7 +719,7 @@ Status SimplifiedLayerNormFusion::ApplyImpl(Graph& graph, bool& modified, int gr InlinedVector layer_norm_input_defs{x_input, scale}; Node& layer_norm_node = graph.AddNode(graph.GenerateNodeName(mul_node.Name() + "/SimplifiedLayerNormFusion/"), "SimplifiedLayerNormalization", - "fused LayerNorm subgraphs ", layer_norm_input_defs, {}, {}, kOnnxDomain); + "fused LayerNorm subgraphs ", layer_norm_input_defs, {}, mul_node, nullptr, kOnnxDomain); // Get constant "epsilon" from "Add" node if available. Else, default value will be used. const ONNX_NAMESPACE::TensorProto* tensor_proto = diff --git a/onnxruntime/core/optimizer/matmul_add_fusion.cc b/onnxruntime/core/optimizer/matmul_add_fusion.cc index 095e4b7f9c317..f567609c979a9 100644 --- a/onnxruntime/core/optimizer/matmul_add_fusion.cc +++ b/onnxruntime/core/optimizer/matmul_add_fusion.cc @@ -205,9 +205,8 @@ Status MatMulAddFusion::ApplyImpl(Graph& graph, bool& modified, int graph_level, NodeArg* new_arg = &graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(name + "_reshape_arg"), &new_arg_type); Node& reshape_node = graph.AddNode(graph.GenerateNodeName(name + "_reshape"), "Reshape", "Reshape for " + name, {is_input ? gemm_input_defs[0] : new_arg, shape_arg}, - {is_input ? new_arg : gemm_output_defs[0]}); - // Runs before partitioning - optimizer_utils::DuplicateNodeAnnotation(matmul_node, reshape_node); + {is_input ? new_arg : gemm_output_defs[0]}, + matmul_node); reshape_node.SetExecutionProviderType(matmul_node.GetExecutionProviderType()); return &reshape_node; }; @@ -220,9 +219,8 @@ Status MatMulAddFusion::ApplyImpl(Graph& graph, bool& modified, int graph_level, } Node& gemm_node = graph.AddNode(graph.GenerateNodeName(matmul_node.Name() + "/MatMulAddFusion"), "Gemm", - "fused Matmul and Add", gemm_input_defs, gemm_output_defs); - // Runs before partitioning - optimizer_utils::DuplicateNodeAnnotation(matmul_node, gemm_node); + "fused Matmul and Add", gemm_input_defs, gemm_output_defs, + matmul_node); gemm_node.SetExecutionProviderType(matmul_node.GetExecutionProviderType()); if (need_reshape) { diff --git a/onnxruntime/core/optimizer/matmul_bn_fusion.cc b/onnxruntime/core/optimizer/matmul_bn_fusion.cc index 871571ea64881..be52e26a2901f 100644 --- a/onnxruntime/core/optimizer/matmul_bn_fusion.cc +++ b/onnxruntime/core/optimizer/matmul_bn_fusion.cc @@ -227,6 +227,7 @@ Status MatmulBNFusion::Apply(Graph& graph, Node& matmul_node, RewriteRuleEffect& "Generated from Matmul BatchNormalization fusion", {matmul_node.MutableInputDefs()[0], &new_gemm_b_node_arg, &new_gemm_bias_node_arg}, matmul_node.MutableOutputDefs(), + matmul_node, nullptr, kOnnxDomain); diff --git a/onnxruntime/core/optimizer/qdq_transformer/ensure_unique_dq_for_node_unit.cc b/onnxruntime/core/optimizer/qdq_transformer/ensure_unique_dq_for_node_unit.cc index 4dd8dbd45a255..c79e4142a9ee2 100644 --- a/onnxruntime/core/optimizer/qdq_transformer/ensure_unique_dq_for_node_unit.cc +++ b/onnxruntime/core/optimizer/qdq_transformer/ensure_unique_dq_for_node_unit.cc @@ -54,10 +54,10 @@ Status DuplicateDQForOutputEdge(const graph_utils::GraphEdge& original_dq_output MakeString("Added by ", kTransformerName), dq_inputs, {&new_dq_output_nodearg}, + original_dq_node, &original_dq_node.GetAttributes(), original_dq_node.Domain()); - optimizer_utils::DuplicateNodeAnnotation(original_dq_node, new_dq_node); // set up edges // remove DQ -> Y graph_utils::GraphEdge::RemoveGraphEdges(graph, {original_dq_output_edge}); diff --git a/onnxruntime/core/optimizer/qdq_transformer/weight_bias_quantization.cc b/onnxruntime/core/optimizer/qdq_transformer/weight_bias_quantization.cc index 2019f27f7b5b3..ba3ea09564c17 100644 --- a/onnxruntime/core/optimizer/qdq_transformer/weight_bias_quantization.cc +++ b/onnxruntime/core/optimizer/qdq_transformer/weight_bias_quantization.cc @@ -189,16 +189,14 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(node.Name() + "_weight_q"), &weight_q_type_proto); Node& weight_q_node = graph.AddNode( graph.GenerateNodeArgName(node.Name() + "_weight_q"), QDQ::QOpName, "Weight Q node", - {node.MutableInputDefs()[1], weight_scale_arg, &weight_zp_arg}, {&weight_q_arg}, nullptr, node.Domain()); - optimizer_utils::DuplicateNodeAnnotation(node, weight_q_node); + {node.MutableInputDefs()[1], weight_scale_arg, &weight_zp_arg}, {&weight_q_arg}, node, nullptr, node.Domain()); // DQ from int8 to float32. NodeArg& weight_dq_arg = graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(node.Name() + "_weight_dq"), weight_arg->TypeAsProto()); Node& weight_dq_node = graph.AddNode(graph.GenerateNodeArgName(node.Name() + "_weight_dq"), QDQ::DQOpName, "Weight DQ node", - {&weight_q_arg, weight_scale_arg, &weight_zp_arg}, {&weight_dq_arg}, nullptr, node.Domain()); - optimizer_utils::DuplicateNodeAnnotation(node, weight_dq_node); + {&weight_q_arg, weight_scale_arg, &weight_zp_arg}, {&weight_dq_arg}, node, nullptr, node.Domain()); graph.AddEdge(weight_q_node.Index(), weight_dq_node.Index(), 0, 0); node.MutableInputDefs()[1] = &weight_dq_arg; graph.AddEdge(weight_dq_node.Index(), node.Index(), 0, 1); @@ -213,16 +211,14 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph weight_scale_arg->TypeAsProto()); Node& mul_node = graph.AddNode(graph.GenerateNodeName(node.Name() + "_scale"), "Mul", "Bias scale node", - {dq_0.MutableInputDefs()[1], weight_scale_arg}, {&bias_scale_arg}, nullptr, node.Domain()); - optimizer_utils::DuplicateNodeAnnotation(node, mul_node); + {dq_0.MutableInputDefs()[1], weight_scale_arg}, {&bias_scale_arg}, node, nullptr, node.Domain()); // fp_bias / scale. NodeArg& bias_div_arg = graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(node.Name() + "_bias_div"), bias_arg->TypeAsProto()); Node& div_node = graph.AddNode(graph.GenerateNodeName(node.Name() + "_bias_div"), "Div", "Bias div node", - {node.MutableInputDefs()[2], &bias_scale_arg}, {&bias_div_arg}, nullptr, node.Domain()); - optimizer_utils::DuplicateNodeAnnotation(node, div_node); + {node.MutableInputDefs()[2], &bias_scale_arg}, {&bias_div_arg}, node, nullptr, node.Domain()); graph.AddEdge(mul_node.Index(), div_node.Index(), 0, 1); // Round(fp_bias / scale). @@ -230,8 +226,7 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(node.Name() + "_bias_div_round"), bias_arg->TypeAsProto()); Node& round_node = graph.AddNode(graph.GenerateNodeName(node.Name() + "_bias_div_round"), "Round", "Bias div round node", - {&bias_div_arg}, {&bias_div_round_arg}, nullptr, node.Domain()); - optimizer_utils::DuplicateNodeAnnotation(node, round_node); + {&bias_div_arg}, {&bias_div_round_arg}, node, nullptr, node.Domain()); graph.AddEdge(div_node.Index(), round_node.Index(), 0, 0); // Cast(Round(fp_bias / scale)) to int32. @@ -241,8 +236,7 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph NodeArg& bias_int32_arg = graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(node.Name() + "_bias_int32"), &bias_int32_type_proto); Node& cast_node = graph.AddNode(graph.GenerateNodeName(node.Name() + "_bias_int32"), "Cast", "Bias INT32 node", - {&bias_div_round_arg}, {&bias_int32_arg}, nullptr, node.Domain()); - optimizer_utils::DuplicateNodeAnnotation(node, cast_node); + {&bias_div_round_arg}, {&bias_int32_arg}, node, nullptr, node.Domain()); cast_node.AddAttribute("to", static_cast(ONNX_NAMESPACE::TensorProto_DataType_INT32)); graph.AddEdge(round_node.Index(), cast_node.Index(), 0, 0); @@ -251,8 +245,7 @@ Status WeightBiasQuantization::ApplyImpl(Graph& graph, bool& modified, int graph graph.GetOrCreateNodeArg(graph.GenerateNodeArgName(node.Name() + "_bias_dq"), bias_arg->TypeAsProto()); Node& bias_dq_node = graph.AddNode(graph.GenerateNodeName(node.Name() + "_bias_dq"), QDQ::DQOpName, "Bias DQ node", - {&bias_int32_arg, &bias_scale_arg}, {&bias_dq_arg}, nullptr, node.Domain()); - optimizer_utils::DuplicateNodeAnnotation(node, bias_dq_node); + {&bias_int32_arg, &bias_scale_arg}, {&bias_dq_arg}, node, nullptr, node.Domain()); if (!is_per_tensor_scale) { bias_dq_node.AddAttribute("axis", static_cast(0)); } diff --git a/onnxruntime/core/optimizer/qdq_transformer/where_dummy_dq.cc b/onnxruntime/core/optimizer/qdq_transformer/where_dummy_dq.cc index 082a42b32a963..94fc7f6c03fa1 100644 --- a/onnxruntime/core/optimizer/qdq_transformer/where_dummy_dq.cc +++ b/onnxruntime/core/optimizer/qdq_transformer/where_dummy_dq.cc @@ -134,11 +134,10 @@ Status WhereDummyDq::InsertDummyDQ(Node& node, Graph& graph, bool& modified, con "DeQuantizeLinear from WhereDummyDq GraphTransformer", {&dummy_data_arg, &dummy_scale_arg, &dummy_zp_arg}, {&dummy_dq_arg}, + node, nullptr, dq_node->Domain()); - optimizer_utils::DuplicateNodeAnnotation(node, dummy_dq_node); - node.MutableInputDefs()[const_idx] = &dummy_dq_arg; if (graph.GetConsumerNodes(where_inputs[const_idx]->Name()).size() == 0) { graph.RemoveInitializedTensor(where_inputs[const_idx]->Name()); diff --git a/onnxruntime/core/optimizer/reshape_fusion.cc b/onnxruntime/core/optimizer/reshape_fusion.cc index be6e310cecc4a..167952356ff58 100644 --- a/onnxruntime/core/optimizer/reshape_fusion.cc +++ b/onnxruntime/core/optimizer/reshape_fusion.cc @@ -495,8 +495,8 @@ bool ReshapeFusion::FuseContiguousReshapes(Node& reshape, Graph& graph) { NodeArg* shape_arg = &graph_utils::AddInitializerWithOrtValue(graph, shape_initializer_proto); Node& reshape_node = graph.AddNode(graph.GenerateNodeName(name + "_new_reshape"), "Reshape", "Reshape for " + name, {contiguous_reshapes[0].get().MutableInputDefs()[0], shape_arg}, - {contiguous_reshapes.back().get().MutableOutputDefs()[0]}); - optimizer_utils::DuplicateNodeAnnotation(reshape, reshape_node); + {contiguous_reshapes.back().get().MutableOutputDefs()[0]}, + reshape); reshape_node.SetExecutionProviderType(contiguous_reshapes[0].get().GetExecutionProviderType()); graph_utils::FinalizeNodeFusion(graph, contiguous_reshapes, reshape_node); diff --git a/onnxruntime/core/optimizer/slice_concat_to_space_to_depth_fusion.cc b/onnxruntime/core/optimizer/slice_concat_to_space_to_depth_fusion.cc index f72f74e3b4a5c..8caea2c150990 100644 --- a/onnxruntime/core/optimizer/slice_concat_to_space_to_depth_fusion.cc +++ b/onnxruntime/core/optimizer/slice_concat_to_space_to_depth_fusion.cc @@ -492,6 +492,7 @@ bool FuseSliceConcatToSpaceToDepth(Node& concat, Graph& graph, const logging::Lo : "Fused Slice*4 + Concat into SpaceToDepth + channel permutation", {space_to_depth_input}, space_to_depth_outputs, + concat, nullptr, kOnnxDomain); space_to_depth.AddAttribute("blocksize", kBlockSize); @@ -517,6 +518,7 @@ bool FuseSliceConcatToSpaceToDepth(Node& concat, Graph& graph, const logging::Lo "Reorder SpaceToDepth channels to preserve Slice+Concat block order", {space_to_depth.MutableOutputDefs()[0], gather_indices_arg}, {}, + concat, nullptr, kOnnxDomain); gather.AddAttribute("axis", static_cast(kChannelAxis)); diff --git a/onnxruntime/core/optimizer/stft_decomposition.cc b/onnxruntime/core/optimizer/stft_decomposition.cc index 60ab064465f2f..c84e60e64bd2d 100644 --- a/onnxruntime/core/optimizer/stft_decomposition.cc +++ b/onnxruntime/core/optimizer/stft_decomposition.cc @@ -58,27 +58,43 @@ NodeArg* AddShapeInitializer(Graph& graph, const char* name, const int64_t (&sha std::pair AddNode(Graph& graph, const char* op_type, ProviderType execution_provider_type, - gsl::span inputs) { + gsl::span inputs, + const Node* annotation_source = nullptr) { auto def_name = graph.GenerateNodeArgName(op_type); auto node_arg = &graph.GetOrCreateNodeArg(def_name, nullptr); - Node& node = graph.AddNode(graph.GenerateNodeName(op_type), - op_type, - "", - inputs, - {node_arg}); + Node& node = annotation_source + ? graph.AddNode(graph.GenerateNodeName(op_type), + op_type, + "", + inputs, + {node_arg}, + *annotation_source) + : graph.AddNode(graph.GenerateNodeName(op_type), + op_type, + "", + inputs, + {node_arg}); node.SetExecutionProviderType(execution_provider_type); return std::make_pair(&node, node_arg); } std::pair AddNodeCast(Graph& graph, NodeArg* in, - ONNX_NAMESPACE::TensorProto_DataType data_type) { + ONNX_NAMESPACE::TensorProto_DataType data_type, + const Node* annotation_source = nullptr) { auto def_name = graph.GenerateNodeArgName("Cast"); auto node_arg = &graph.GetOrCreateNodeArg(def_name, nullptr); - Node& node = graph.AddNode(graph.GenerateNodeName("Cast"), - "Cast", - "", - {in}, - {node_arg}); + Node& node = annotation_source + ? graph.AddNode(graph.GenerateNodeName("Cast"), + "Cast", + "", + {in}, + {node_arg}, + *annotation_source) + : graph.AddNode(graph.GenerateNodeName("Cast"), + "Cast", + "", + {in}, + {node_arg}); node.AddAttribute("to", static_cast(data_type)); node.SetExecutionProviderType(kCpuExecutionProvider); return std::make_pair(&node, node_arg); @@ -238,7 +254,7 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve Node* reshape_signal_node = nullptr; NodeArg* reshape_output = nullptr; std::tie(reshape_signal_node, reshape_output) = - AddNode(graph, "Reshape", stft.GetExecutionProviderType(), signal_reshaped_inputs); + AddNode(graph, "Reshape", stft.GetExecutionProviderType(), signal_reshaped_inputs, &stft); NodeArg* real_weights_final = real_weights; NodeArg* imag_weights_final = imaginary_weights; @@ -246,11 +262,11 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve // When we are missing a window function if (real_weights_final->TypeAsProto()->tensor_type().elem_type() != data_type) { std::tie(std::ignore, real_weights_final) = - AddNodeCast(graph, real_weights_final, data_type); + AddNodeCast(graph, real_weights_final, data_type, &stft); } if (imag_weights_final->TypeAsProto()->tensor_type().elem_type() != data_type) { std::tie(std::ignore, imag_weights_final) = - AddNodeCast(graph, imag_weights_final, data_type); + AddNodeCast(graph, imag_weights_final, data_type, &stft); } } else { // When we have a window function @@ -261,7 +277,7 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve if (window->TypeAsProto()->tensor_type().elem_type() != GetDataType()) { Node* window_cast_node = nullptr; std::tie(window_cast_node, window_final) = - AddNodeCast(graph, window, GetDataType()); + AddNodeCast(graph, window, GetDataType(), &stft); window_recipient = window_cast_node; } @@ -269,7 +285,7 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve Node* window_reshape_node; NodeArg* window_reshaped = nullptr; std::tie(window_reshape_node, window_reshaped) = - AddNode(graph, "Reshape", kCpuExecutionProvider, window_reshaped_inputs); + AddNode(graph, "Reshape", kCpuExecutionProvider, window_reshaped_inputs, &stft); if (!window_recipient) { window_recipient = window_reshape_node; } @@ -277,17 +293,17 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve NodeArg* scale_real_weights_inputs[] = {real_weights, window_reshaped}; NodeArg* windowed_real_weights_output = nullptr; std::tie(std::ignore, windowed_real_weights_output) = - AddNode(graph, "Mul", kCpuExecutionProvider, scale_real_weights_inputs); + AddNode(graph, "Mul", kCpuExecutionProvider, scale_real_weights_inputs, &stft); NodeArg* scale_imag_weights_inputs[] = {imaginary_weights, window_reshaped}; NodeArg* windowed_imag_weights_output = nullptr; std::tie(std::ignore, windowed_imag_weights_output) = - AddNode(graph, "Mul", kCpuExecutionProvider, scale_imag_weights_inputs); + AddNode(graph, "Mul", kCpuExecutionProvider, scale_imag_weights_inputs, &stft); std::tie(std::ignore, real_weights_final) = - AddNodeCast(graph, windowed_real_weights_output, data_type); + AddNodeCast(graph, windowed_real_weights_output, data_type, &stft); std::tie(std::ignore, imag_weights_final) = - AddNodeCast(graph, windowed_imag_weights_output, data_type); + AddNodeCast(graph, windowed_imag_weights_output, data_type, &stft); } // Add Convolution (reals) @@ -295,7 +311,7 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve Node* real_conv_node = nullptr; NodeArg* real_conv_output = nullptr; std::tie(real_conv_node, real_conv_output) = - AddNode(graph, "Conv", stft.GetExecutionProviderType(), conv_real_inputs); + AddNode(graph, "Conv", stft.GetExecutionProviderType(), conv_real_inputs, &stft); real_conv_node->AddAttribute("strides", std::vector{1, frame_step_value}); // Add Convolution (imaginary) @@ -303,7 +319,7 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve Node* imag_conv_node = nullptr; NodeArg* imag_conv_output = nullptr; std::tie(imag_conv_node, imag_conv_output) = - AddNode(graph, "Conv", stft.GetExecutionProviderType(), conv_imag_inputs); + AddNode(graph, "Conv", stft.GetExecutionProviderType(), conv_imag_inputs, &stft); imag_conv_node->AddAttribute("strides", std::vector{1, frame_step_value}); // Concatenate @@ -311,21 +327,21 @@ Status STFTDecomposition::ApplyImpl(Graph& graph, bool& modified, int graph_leve Node* concat_node = nullptr; NodeArg* concatenated_conv_output = nullptr; std::tie(concat_node, concatenated_conv_output) = - AddNode(graph, "Concat", stft.GetExecutionProviderType(), concatenate_inputs); + AddNode(graph, "Concat", stft.GetExecutionProviderType(), concatenate_inputs, &stft); concat_node->AddAttribute("axis", static_cast(0)); // Unsqueeze Reshape NodeArg* unsqueeze_reshape_inputs[] = {concatenated_conv_output, unsqueezed_shape}; NodeArg* unsqueezed_output = nullptr; std::tie(std::ignore, unsqueezed_output) = - AddNode(graph, "Reshape", stft.GetExecutionProviderType(), unsqueeze_reshape_inputs); + AddNode(graph, "Reshape", stft.GetExecutionProviderType(), unsqueeze_reshape_inputs, &stft); // Transpose NodeArg* transpose_inputs[] = {unsqueezed_output}; Node* transpose_node = nullptr; NodeArg* transpose_output = nullptr; std::tie(transpose_node, transpose_output) = - AddNode(graph, "Transpose", stft.GetExecutionProviderType(), transpose_inputs); + AddNode(graph, "Transpose", stft.GetExecutionProviderType(), transpose_inputs, &stft); transpose_node->AddAttribute("perm", std::vector{1, 3, 2, 0}); signal_recipient = reshape_signal_node; diff --git a/onnxruntime/test/framework/session_state_test.cc b/onnxruntime/test/framework/session_state_test.cc index de302131a2fd3..656b0ef86289d 100644 --- a/onnxruntime/test/framework/session_state_test.cc +++ b/onnxruntime/test/framework/session_state_test.cc @@ -434,9 +434,10 @@ static void CollectNodeNames(const Graph& graph, std::vector& names // all nodes so callers can choose a threshold relative to the actual total. // This avoids relying on a pre-baked stats file whose node name hashes // become stale when graph optimizers change node input/output names. -static size_t GenerateDynamicNodeStatsFile(const ORTCHAR_T* model_path, - const std::filesystem::path& output_path, - size_t cost_per_node = 1024) { +static void GenerateDynamicNodeStatsFile(const ORTCHAR_T* model_path, + const std::filesystem::path& output_path, + size_t& total_cost, + size_t cost_per_node = 1024) { const auto& default_logger = DefaultLoggingManager().DefaultLogger(); std::shared_ptr model; ASSERT_STATUS_OK(Model::Load(model_path, model, nullptr, default_logger)); @@ -454,7 +455,7 @@ static size_t GenerateDynamicNodeStatsFile(const ORTCHAR_T* model_path, } ofs.close(); - return node_names.size() * cost_per_node; + total_cost = node_names.size() * cost_per_node; } void LoadWithResourceAwarePartitioning(const ORTCHAR_T* model_path, @@ -549,7 +550,8 @@ TEST(SessionStateTest, TestResourceAwarePartitioning_LargeLimit) { // Generate node stats dynamically so names always match the current graph constexpr size_t cost_per_node = 1024; - size_t total_cost = GenerateDynamicNodeStatsFile(model_path, stats_path, cost_per_node); + size_t total_cost = 0; + GenerateDynamicNodeStatsFile(model_path, stats_path, total_cost, cost_per_node); ASSERT_GT(total_cost, 0U); // Use a limit much larger than total cost so all nodes are assigned CUDA. @@ -571,7 +573,8 @@ TEST(SessionStateTest, TestResourceAwarePartitioning_LargeLimit) { } }); - std::filesystem::remove(stats_path); + std::error_code remove_ec; + std::filesystem::remove(stats_path, remove_ec); } TEST(SessionStateTest, TestResourceAwarePartitioning_CPUOffloaded) { @@ -583,7 +586,8 @@ TEST(SessionStateTest, TestResourceAwarePartitioning_CPUOffloaded) { // Generate node stats dynamically so names always match the current graph. constexpr size_t cost_per_node = 1024; - size_t total_cost = GenerateDynamicNodeStatsFile(model_path, stats_path, cost_per_node); + size_t total_cost = 0; + GenerateDynamicNodeStatsFile(model_path, stats_path, total_cost, cost_per_node); ASSERT_GT(total_cost, 0U); // Set threshold to half the total cost so some nodes must be offloaded to CPU. @@ -611,7 +615,8 @@ TEST(SessionStateTest, TestResourceAwarePartitioning_CPUOffloaded) { EXPECT_TRUE(cpu_node_found); }); - std::filesystem::remove(stats_path); + std::error_code remove_ec; + std::filesystem::remove(stats_path, remove_ec); } TEST(SessionStateTest, TestLayeringPartitioning) {