@@ -1141,19 +1141,23 @@ def __init__(
11411141 dynamic axis indices.
11421142 """
11431143 self .dynamic_axis_strict_mode = dynamic_axis_strict_mode
1144+ self .model_proto : onnx .ModelProto = model_proto
11441145 self .model_path = Path (model_path ) if model_path is not None else None
11451146 # Try shape inference: standard ONNX first, then symbolic (onnxruntime)
11461147 try :
11471148 # Standard ONNX shape inference — uses temp file for models
11481149 # with external data (avoids silent empty-graph result).
1149- self .model_proto = infer_onnx_shapes (model_proto )
1150+ inferred_model = infer_onnx_shapes (model_proto )
1151+ self .model_proto = inferred_model if inferred_model is not None else model_proto
11501152
11511153 # Then try to enhance with symbolic shape inference
11521154 # if available which supports Microsoft domain
11531155 try :
11541156 from onnxruntime .tools .symbolic_shape_infer import SymbolicShapeInference
11551157
1156- self .model_proto = SymbolicShapeInference .infer_shapes (self .model_proto )
1158+ symbolic_model = SymbolicShapeInference .infer_shapes (self .model_proto )
1159+ if symbolic_model is not None :
1160+ self .model_proto = symbolic_model
11571161 except Exception as e :
11581162 # If symbolic shape inference fails, continue with standard inference result
11591163 logger .debug (
@@ -1357,6 +1361,196 @@ def _get_ep_checker(self) -> EPChecker:
13571361 )
13581362 return self ._ep_checker
13591363
1364+ @staticmethod
1365+ def _clone_node_proto (node : onnx .NodeProto ) -> onnx .NodeProto :
1366+ """Clone a node proto so extracted test models do not reuse graph objects."""
1367+ cloned = onnx .NodeProto ()
1368+ cloned .CopyFrom (node )
1369+ return cloned
1370+
1371+ def _find_producer_node (self , tensor_name : str ) -> onnx .NodeProto | None :
1372+ """Return the node that produces a tensor, if any."""
1373+ if not tensor_name :
1374+ return None
1375+
1376+ for candidate in self .model_proto .graph .node :
1377+ if tensor_name in candidate .output :
1378+ return candidate
1379+ return None
1380+
1381+ def _find_consumer_nodes (self , tensor_name : str ) -> list [onnx .NodeProto ]:
1382+ """Return nodes that consume a tensor."""
1383+ if not tensor_name :
1384+ return []
1385+
1386+ return [
1387+ candidate for candidate in self .model_proto .graph .node if tensor_name in candidate .input
1388+ ]
1389+
1390+ def _build_opset_imports (
1391+ self ,
1392+ nodes : list [onnx .NodeProto ],
1393+ fallback_op_domain : ONNXDomain ,
1394+ fallback_opset_version : int ,
1395+ ) -> list [onnx .OperatorSetIdProto ]:
1396+ """Build opset imports for an extracted runtime-test model."""
1397+ opset_imports : list [onnx .OperatorSetIdProto ] = []
1398+ added_domains : set [str ] = set ()
1399+ saw_non_default_domain = False
1400+
1401+ def add_domain (domain_str : str , version : int ) -> None :
1402+ canonical_domain = "" if domain_str in {"" , ONNXDomain .AI_ONNX .value } else domain_str
1403+ if canonical_domain in added_domains :
1404+ return
1405+
1406+ added_domains .add (canonical_domain )
1407+ effective_version = max (version , 7 ) if canonical_domain == "" else version
1408+ opset_imports .append (onnx .helper .make_opsetid (canonical_domain , effective_version ))
1409+
1410+ for included_node in nodes :
1411+ raw_domain = included_node .domain or ""
1412+ try :
1413+ node_domain = ONNXDomain .from_str (raw_domain )
1414+ add_domain (node_domain .schema_domain , self .opset_versions .get (node_domain , 1 ))
1415+ saw_non_default_domain = saw_non_default_domain or node_domain != ONNXDomain .AI_ONNX
1416+ except ValueError :
1417+ add_domain (raw_domain , 1 )
1418+ saw_non_default_domain = saw_non_default_domain or bool (raw_domain )
1419+
1420+ if not opset_imports :
1421+ add_domain (fallback_op_domain .schema_domain , fallback_opset_version )
1422+ saw_non_default_domain = fallback_op_domain != ONNXDomain .AI_ONNX
1423+
1424+ if saw_non_default_domain and "" not in added_domains :
1425+ default_opset = self .opset_versions .get (ONNXDomain .AI_ONNX , 17 )
1426+ add_domain ("" , default_opset )
1427+
1428+ return opset_imports
1429+
1430+ def _build_runtime_test_model (
1431+ self ,
1432+ node : onnx .NodeProto ,
1433+ op_domain : ONNXDomain ,
1434+ opset_version : int ,
1435+ include_adjacent_qdq : bool = False ,
1436+ ) -> onnx .ModelProto :
1437+ """Build the model used for local EP fallback and failed-node artifacts.
1438+
1439+ For QDQ operators, include the adjacent DequantizeLinear and QuantizeLinear
1440+ nodes so the local test model preserves the same quantized context.
1441+ """
1442+ if not include_adjacent_qdq :
1443+ return self ._build_single_node_model (node , op_domain , opset_version )
1444+
1445+ graph_inputs : list [onnx .ValueInfoProto ] = []
1446+ graph_initializers : list [onnx .TensorProto ] = []
1447+ graph_outputs : list [onnx .ValueInfoProto ] = []
1448+ pre_nodes : list [onnx .NodeProto ] = []
1449+ post_nodes : list [onnx .NodeProto ] = []
1450+ seen_inputs : set [str ] = set ()
1451+ seen_initializers : set [str ] = set ()
1452+ seen_outputs : set [str ] = set ()
1453+ seen_pre_nodes : set [str ] = set ()
1454+ seen_post_nodes : set [str ] = set ()
1455+
1456+ def add_graph_source (name : str ) -> None :
1457+ if not name :
1458+ return
1459+
1460+ if name in self .initializers :
1461+ if name not in seen_initializers :
1462+ graph_initializers .append (self .initializers [name ])
1463+ seen_initializers .add (name )
1464+ return
1465+
1466+ if name in self .constants :
1467+ if name not in seen_initializers :
1468+ graph_initializers .append (self .constants [name ])
1469+ seen_initializers .add (name )
1470+ return
1471+
1472+ vi = self .valueinfo .get (name )
1473+ if vi is None :
1474+ raise ValueError (f"Tensor '{ name } ' not found in valueinfo or initializers" )
1475+ if name not in seen_inputs :
1476+ graph_inputs .append (vi )
1477+ seen_inputs .add (name )
1478+
1479+ def add_graph_output (name : str ) -> None :
1480+ if not name or name in seen_outputs :
1481+ return
1482+
1483+ vi = self .valueinfo .get (name )
1484+ if vi is not None :
1485+ graph_outputs .append (vi )
1486+ else :
1487+ graph_outputs .append (
1488+ onnx .helper .make_tensor_value_info (name , onnx .TensorProto .UNDEFINED , None )
1489+ )
1490+ seen_outputs .add (name )
1491+
1492+ for inp_name in node .input :
1493+ if not inp_name :
1494+ continue
1495+
1496+ producer = self ._find_producer_node (inp_name )
1497+ if producer is not None and producer .op_type == "DequantizeLinear" :
1498+ producer_key = producer .name or "|" .join (producer .output )
1499+ if producer_key not in seen_pre_nodes :
1500+ pre_nodes .append (self ._clone_node_proto (producer ))
1501+ seen_pre_nodes .add (producer_key )
1502+ for producer_input in producer .input :
1503+ add_graph_source (producer_input )
1504+ continue
1505+
1506+ add_graph_source (inp_name )
1507+
1508+ for out_name in node .output :
1509+ if not out_name :
1510+ continue
1511+
1512+ quantize_consumers = [
1513+ consumer
1514+ for consumer in self ._find_consumer_nodes (out_name )
1515+ if consumer .op_type == "QuantizeLinear"
1516+ and consumer .input
1517+ and consumer .input [0 ] == out_name
1518+ ]
1519+ if quantize_consumers :
1520+ for consumer in quantize_consumers :
1521+ consumer_key = consumer .name or "|" .join (consumer .output )
1522+ if consumer_key not in seen_post_nodes :
1523+ post_nodes .append (self ._clone_node_proto (consumer ))
1524+ seen_post_nodes .add (consumer_key )
1525+ for consumer_input in consumer .input [1 :]:
1526+ add_graph_source (consumer_input )
1527+ for consumer_output in consumer .output :
1528+ add_graph_output (consumer_output )
1529+ continue
1530+
1531+ add_graph_output (out_name )
1532+
1533+ nodes = [* pre_nodes , self ._clone_node_proto (node ), * post_nodes ]
1534+ graph = onnx .helper .make_graph (
1535+ nodes ,
1536+ f"runtime_test_{ node .op_type } " ,
1537+ graph_inputs ,
1538+ graph_outputs ,
1539+ initializer = graph_initializers ,
1540+ )
1541+
1542+ model = onnx .helper .make_model (
1543+ graph ,
1544+ opset_imports = self ._build_opset_imports (nodes , op_domain , opset_version ),
1545+ )
1546+
1547+ try :
1548+ model = infer_onnx_shapes (model )
1549+ except Exception as e :
1550+ logger .debug ("Shape inference failed for runtime-test model: %s" , e )
1551+
1552+ return model
1553+
13601554 def _build_single_node_model (
13611555 self , node : onnx .NodeProto , op_domain : ONNXDomain , opset_version : int
13621556 ) -> onnx .ModelProto :
@@ -1454,6 +1648,33 @@ def _build_single_node_model(
14541648
14551649 return model
14561650
1651+ def _generate_model_inputs (self , model : onnx .ModelProto ) -> dict [str , np .ndarray ]:
1652+ """Generate dummy input data for a runtime-test model."""
1653+ input_feed : dict [str , np .ndarray ] = {}
1654+ default_dim_size = 2 # Replace dynamic/unknown dims with this size
1655+ initializer_names = {initializer .name for initializer in model .graph .initializer }
1656+
1657+ for graph_input in model .graph .input :
1658+ if graph_input .name in initializer_names :
1659+ continue
1660+
1661+ shape , dtype_str = shape_and_dtype_from_valueinfo (graph_input )
1662+ if dtype_str is None :
1663+ raise ValueError (f"Input '{ graph_input .name } ' has no dtype information" )
1664+
1665+ np_dtype = SupportedONNXType .from_annotation (dtype_str ).np_type
1666+
1667+ if shape is None :
1668+ concrete_shape = (default_dim_size ,)
1669+ else :
1670+ concrete_shape = tuple (
1671+ dim if isinstance (dim , int ) and dim > 0 else default_dim_size for dim in shape
1672+ )
1673+
1674+ input_feed [graph_input .name ] = np .zeros (concrete_shape , dtype = np_dtype )
1675+
1676+ return input_feed
1677+
14571678 def _generate_node_inputs (self , node : onnx .NodeProto ) -> dict [str , np .ndarray ]:
14581679 """Generate dummy input data for a single-node model.
14591680
@@ -1531,6 +1752,7 @@ def _try_local_ep_check(
15311752 pattern_match : PatternMatchResult ,
15321753 node_tags : list [NodeTag ],
15331754 fallback_reason : str ,
1755+ include_adjacent_qdq : bool = False ,
15341756 save_node_types : set [str ] | None = None ,
15351757 conditions : Any | None = None ,
15361758 ) -> PatternRuntime | None :
@@ -1572,11 +1794,16 @@ def _try_local_ep_check(
15721794 )
15731795
15741796 try :
1575- model = self ._build_single_node_model (node , op_domain , opset_version )
1576- input_feed = self ._generate_node_inputs (node )
1797+ model = self ._build_runtime_test_model (
1798+ node ,
1799+ op_domain ,
1800+ opset_version ,
1801+ include_adjacent_qdq = include_adjacent_qdq ,
1802+ )
1803+ input_feed = self ._generate_model_inputs (model )
15771804 except Exception as e :
15781805 logger .debug (
1579- "Failed to build single-node model for local EP check on %s (%s): %s" ,
1806+ "Failed to build runtime-test model for local EP check on %s (%s): %s" ,
15801807 node .name ,
15811808 node .op_type ,
15821809 e ,
@@ -1808,6 +2035,7 @@ def _maybe_save_failed_node_result(
18082035 opset_version : int ,
18092036 result : RuntimeTestResult ,
18102037 cache_key : Any ,
2038+ include_adjacent_qdq : bool = False ,
18112039 save_node_types : set [str ] | None = None ,
18122040 ) -> None :
18132041 """Save unsupported or partial node models without re-running result computation."""
@@ -1820,7 +2048,12 @@ def _maybe_save_failed_node_result(
18202048 if not (is_unsupported or is_partial ):
18212049 return
18222050
1823- node_model = self ._build_single_node_model (node , op_domain , opset_version )
2051+ node_model = self ._build_runtime_test_model (
2052+ node ,
2053+ op_domain ,
2054+ opset_version ,
2055+ include_adjacent_qdq = include_adjacent_qdq ,
2056+ )
18242057 self ._save_failed_node (
18252058 node ,
18262059 node_model ,
@@ -2079,6 +2312,7 @@ def get_pattern_id(is_qdq):
20792312 pattern_match ,
20802313 node_tags ,
20812314 fallback_reason ,
2315+ include_adjacent_qdq = is_qdq ,
20822316 save_node_types = save_node_types ,
20832317 # conditions not available when domain/op
20842318 # rules are missing
@@ -2187,6 +2421,7 @@ def get_pattern_id(is_qdq):
21872421 pattern_match ,
21882422 node_tags ,
21892423 fallback_reason ,
2424+ include_adjacent_qdq = is_qdq ,
21902425 conditions = cache_key ,
21912426 )
21922427 if local_result is not None :
@@ -2223,6 +2458,7 @@ def get_pattern_id(is_qdq):
22232458 pattern_match ,
22242459 node_tags ,
22252460 fallback_reason ,
2461+ include_adjacent_qdq = is_qdq ,
22262462 conditions = None ,
22272463 )
22282464 if local_result is not None :
@@ -2335,6 +2571,7 @@ def get_pattern_id(is_qdq):
23352571 opset_version ,
23362572 result ,
23372573 cache_key ,
2574+ include_adjacent_qdq = is_qdq ,
23382575 save_node_types = save_node_types ,
23392576 )
23402577
0 commit comments