From feb04f0af7467eb157c863fa81c302a318596f0e Mon Sep 17 00:00:00 2001 From: fangyangci Date: Wed, 12 Aug 2026 20:37:09 +0800 Subject: [PATCH 1/2] Handle missing schema as unsupported in analyze Catch ONNX SchemaError during node schema lookup and reclassify as OpUnsupportedError so analyze continues and marks unknown/unsupported ops instead of failing. Also downgrade per-node OpUnsupportedError logging to debug to reduce noisy error output, and add regression coverage. --- .../analyze/core/runtime_checker_query.py | 17 +++++++++++-- .../test_runtime_checker_query_helpers.py | 25 ++++++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/winml/modelkit/analyze/core/runtime_checker_query.py b/src/winml/modelkit/analyze/core/runtime_checker_query.py index a26d6e832..68a322ca1 100644 --- a/src/winml/modelkit/analyze/core/runtime_checker_query.py +++ b/src/winml/modelkit/analyze/core/runtime_checker_query.py @@ -20,6 +20,7 @@ import onnx import pandas as pd from onnx import numpy_helper +from onnx.defs import SchemaError from ...onnx import ( ONNXDomain, @@ -590,7 +591,18 @@ def get_query_conditions_for_node( - is_qdq: True if node has QDQ quantization on inputs or outputs. """ conditions = {} - schema = domain.get_op_schema(node.op_type, opset_version) + try: + schema = domain.get_op_schema(node.op_type, opset_version) + except SchemaError as e: + # Some runtime-specific models emit extension ops in the default ONNX + # domain where no standard schema exists (for example + # SimplifiedLayerNormalization). Treat this as unsupported instead of + # aborting the whole analyze command. + raise OpUnsupportedError( + "Node " + f"{node.op_type} has no registered schema for domain " + f"'{domain.schema_domain}' at opset {opset_version}: {e}" + ) from e input_names, variadic_input_name, attribute_names, type_annotations = get_op_input_properties( schema ) @@ -2685,7 +2697,8 @@ def get_pattern_id(is_qdq: bool) -> str: ) as e: conditions_ms = _elapsed_ms(conditions_start) exception_type = type(e).__name__ - logger.error( + log_fn = logger.debug if isinstance(e, OpUnsupportedError) else logger.error + log_fn( "%s caught for op %s (node: %s): %s", exception_type, node.op_type, diff --git a/tests/unit/analyze/core/test_runtime_checker_query_helpers.py b/tests/unit/analyze/core/test_runtime_checker_query_helpers.py index f3d0b73c2..931cec2e7 100644 --- a/tests/unit/analyze/core/test_runtime_checker_query_helpers.py +++ b/tests/unit/analyze/core/test_runtime_checker_query_helpers.py @@ -22,7 +22,7 @@ node_to_pattern_match, try_load_external_initializer_array, ) -from winml.modelkit.analyze.exceptions import OpOptionalInputSupportError +from winml.modelkit.analyze.exceptions import OpOptionalInputSupportError, OpUnsupportedError from winml.modelkit.analyze.utils.model_utils import DUMMY_FLOAT from winml.modelkit.analyze.utils.node_key_utils import resolve_stable_node_key from winml.modelkit.onnx import ONNXDomain @@ -129,6 +129,29 @@ def test_preserves_column_order_for_present_entries(self): class TestGetQueryConditionsForNode: """Test condition extraction for runtime rule lookups.""" + def test_missing_schema_is_reported_as_unsupported_error(self): + """Unknown schema should be classified as unsupported instead of crashing analyze.""" + node = helper.make_node( + "SimplifiedLayerNormalization", + ["X", "gamma"], + ["Y"], + name="rms_norm", + ) + + with pytest.raises(OpUnsupportedError) as exc_info: + get_query_conditions_for_node( + node=node, + opset_version=21, + valueinfo={}, + initializers={}, + constants={}, + domain=ONNXDomain.AI_ONNX, + input_to_dq={}, + output_to_q={}, + ) + + assert "has no registered schema" in str(exc_info.value) + def test_external_initializer_without_payload_is_not_marked_constant(self): """External-data initializers without loaded values keep shape but not constant status.""" node = helper.make_node("Add", ["weight", "input"], ["output"], name="add_node") From d2c35ab57888d28976081f6310fca76d931c6f9b Mon Sep 17 00:00:00 2001 From: fangyangci Date: Thu, 13 Aug 2026 10:00:43 +0800 Subject: [PATCH 2/2] Preserve unsupported reasons in runtime results Map run_for_node condition-extraction exceptions to specific reasons so schema misses and generic unsupported ops are distinguishable in non-debug payloads. Add end-to-end RuntimeCheckerQuery tests asserting returned reasons for schema-miss and unsupported-op paths. --- .../analyze/core/runtime_checker_query.py | 16 ++++- .../test_runtime_checker_query_helpers.py | 64 +++++++++++++++++++ 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/src/winml/modelkit/analyze/core/runtime_checker_query.py b/src/winml/modelkit/analyze/core/runtime_checker_query.py index 68a322ca1..6d4f9a45e 100644 --- a/src/winml/modelkit/analyze/core/runtime_checker_query.py +++ b/src/winml/modelkit/analyze/core/runtime_checker_query.py @@ -2697,7 +2697,19 @@ def get_pattern_id(is_qdq: bool) -> str: ) as e: conditions_ms = _elapsed_ms(conditions_start) exception_type = type(e).__name__ - log_fn = logger.debug if isinstance(e, OpUnsupportedError) else logger.error + reason = "optional_input_properties_not_found" + log_fn = logger.error + + if isinstance(e, OpUnsupportedError): + error_message = str(e) + if "has no registered schema for domain" in error_message: + reason = f"schema_not_registered:{node.op_type}" + else: + reason = f"unsupported_op:{node.op_type}" + log_fn = logger.debug + elif isinstance(e, OpLackOfRequiredInformationError): + reason = "required_information_missing" + log_fn( "%s caught for op %s (node: %s): %s", exception_type, @@ -2723,7 +2735,7 @@ def get_pattern_id(is_qdq: bool) -> str: compile=False, run=False, no_data=True, - reason="optional_input_properties_not_found", + reason=reason, node_tags=node_tags, debug_details=conditions_error_debug_details, ), diff --git a/tests/unit/analyze/core/test_runtime_checker_query_helpers.py b/tests/unit/analyze/core/test_runtime_checker_query_helpers.py index 931cec2e7..4a91f6852 100644 --- a/tests/unit/analyze/core/test_runtime_checker_query_helpers.py +++ b/tests/unit/analyze/core/test_runtime_checker_query_helpers.py @@ -281,6 +281,70 @@ def test_try_load_external_initializer_array_returns_plain_ndarray( assert renamed_sidecar_path.exists() +class TestRunForNodeUnsupportedReasons: + """End-to-end reason mapping tests for RuntimeCheckerQuery.run_for_node.""" + + @staticmethod + def _build_identity_model() -> onnx.ModelProto: + input_info = helper.make_tensor_value_info("input", TensorProto.FLOAT, [1]) + output_info = helper.make_tensor_value_info("output", TensorProto.FLOAT, [1]) + node = helper.make_node("Identity", ["input"], ["output"], name="identity_node") + graph = helper.make_graph([node], "identity_graph", [input_info], [output_info]) + return helper.make_model(graph, opset_imports=[helper.make_opsetid("", 21)]) + + def test_run_for_node_preserves_schema_missing_reason(self, monkeypatch): + """Schema lookup misses should surface schema-specific reason in returned result.""" + model = self._build_identity_model() + node = model.graph.node[0] + query = RuntimeCheckerQuery(model, ep_name="QNNExecutionProvider", device_type="NPU") + query.node_checkers = [] + + def _raise_schema_miss(*args, **kwargs): + del args, kwargs + raise OpUnsupportedError( + "Node Identity has no registered schema for domain '' at opset 21" + ) + + monkeypatch.setattr( + runtime_checker_query_module, + "get_query_conditions_for_node", + _raise_schema_miss, + ) + + result = query.run_for_node(node, for_debug=False, run_unknown_op=False) + + assert result.result.no_data is True + assert result.result.compile is False + assert result.result.run is False + assert result.result.reason == "schema_not_registered:Identity" + assert result.result.debug_details is None + + def test_run_for_node_preserves_generic_unsupported_reason(self, monkeypatch): + """Unsupported-op path should keep a specific unsupported reason in result payload.""" + model = self._build_identity_model() + node = model.graph.node[0] + query = RuntimeCheckerQuery(model, ep_name="QNNExecutionProvider", device_type="NPU") + query.node_checkers = [] + + def _raise_generic_unsupported(*args, **kwargs): + del args, kwargs + raise OpUnsupportedError("Node Identity is not supported") + + monkeypatch.setattr( + runtime_checker_query_module, + "get_query_conditions_for_node", + _raise_generic_unsupported, + ) + + result = query.run_for_node(node, for_debug=False, run_unknown_op=False) + + assert result.result.no_data is True + assert result.result.compile is False + assert result.result.run is False + assert result.result.reason == "unsupported_op:Identity" + assert result.result.debug_details is None + + class TestLocalEPFallback: """Test local EP fallback helpers for single-node execution."""