diff --git a/examples/recipes/google_owlv2-base-patch16/cpu/cpu/zero-shot-object-detection_fp32_config.json b/examples/recipes/google_owlv2-base-patch16/cpu/cpu/zero-shot-object-detection_fp32_config.json new file mode 100644 index 000000000..fcd8f5868 --- /dev/null +++ b/examples/recipes/google_owlv2-base-patch16/cpu/cpu/zero-shot-object-detection_fp32_config.json @@ -0,0 +1,76 @@ +{ + "export": { + "opset_version": 17, + "batch_size": 1, + "export_params": true, + "do_constant_folding": true, + "verbose": false, + "dynamo": false, + "enable_hierarchy_tags": true, + "clean_onnx": false, + "hierarchy_tag_format": "full", + "input_tensors": [ + { + "name": "input_ids", + "dtype": "int32", + "shape": [ + 1, + 16 + ], + "value_range": [ + 0, + 49408 + ] + }, + { + "name": "pixel_values", + "dtype": "float32", + "shape": [ + 1, + 3, + 960, + 960 + ], + "value_range": [ + 0, + 1 + ] + }, + { + "name": "attention_mask", + "dtype": "int32", + "shape": [ + 1, + 16 + ], + "value_range": [ + 0, + 2 + ] + } + ], + "output_tensors": [ + { + "name": "logits" + }, + { + "name": "pred_boxes" + }, + { + "name": "text_embeds" + }, + { + "name": "image_embeds" + } + ] + }, + "optim": { + "clamp_constant_values": true + }, + "quant": null, + "loader": { + "task": "zero-shot-object-detection", + "model_class": "AutoModelForZeroShotObjectDetection", + "model_type": "owlv2" + } +} \ No newline at end of file diff --git a/src/winml/modelkit/export/htp/exporter.py b/src/winml/modelkit/export/htp/exporter.py index c61a162d3..99c839e1c 100644 --- a/src/winml/modelkit/export/htp/exporter.py +++ b/src/winml/modelkit/export/htp/exporter.py @@ -20,6 +20,7 @@ from __future__ import annotations import contextlib +import inspect import logging import sys import time @@ -445,9 +446,25 @@ def _convert_model_to_onnx( output_path = str(Path(output_path).resolve()) Path(output_path).parent.mkdir(parents=True, exist_ok=True) - # Input names from config, fallback to inputs dict keys + # Input names from config, fallback to inputs dict keys. input_names = export_config.get_input_names() or list(inputs.keys()) + if not hasattr(model, "get_export_args"): + # torch.onnx.export binds kwargs by name when invoking forward(), but + # applies input_names positionally to the resulting graph inputs in + # forward-signature order. Keep kwargs for safe invocation and align + # the ONNX names independently so recipe order cannot swap bindings. + try: + parameters = inspect.signature(model.forward).parameters + forward_input_names = [name for name in parameters if name in input_names] + if len(forward_input_names) == len(input_names): + input_names = forward_input_names + except (TypeError, ValueError): + logger.debug( + "Could not inspect %s.forward; preserving configured input order.", + type(model).__name__, + ) + # Output names: infer from traced hierarchy, validate against config traced_outputs = self._hierarchy_builder.get_outputs() if self._hierarchy_builder else None inferred_names = infer_output_names(traced_outputs) if traced_outputs is not None else [] diff --git a/tests/unit/export/test_pytorch_export.py b/tests/unit/export/test_pytorch_export.py index 5b6aeae26..31018c88f 100644 --- a/tests/unit/export/test_pytorch_export.py +++ b/tests/unit/export/test_pytorch_export.py @@ -350,12 +350,12 @@ def test_normalize_false_skips_normalization(self, tmp_path) -> None: assert not _all_value_info_have_shape(onnx_model) def test_mismatched_input_order_exports_successfully(self, tmp_path) -> None: - """Export succeeds when InputTensorSpec order differs from forward() param order. + """ONNX names bind correctly when specs differ from forward() param order. Regression test for CLIP bug: OnnxConfig listed pixel_values before input_ids, - but CLIPModel.forward() expected input_ids first. With positional args this - caused 'not enough values to unpack (expected 4, got 2)'. The fix uses - kwargs= in torch.onnx.export so name-based binding makes order irrelevant. + but CLIPModel.forward() expected input_ids first. kwargs make model invocation + order-independent, while input_names must separately follow forward order + because torch.onnx.export applies those names positionally to graph inputs. """ model = MismatchedInputOrderModel() @@ -374,9 +374,14 @@ def test_mismatched_input_order_exports_successfully(self, tmp_path) -> None: onnx_model = onnx.load(str(tmp_path / "model.onnx")) onnx.checker.check_model(onnx_model) - input_names = {i.name for i in onnx_model.graph.input} - assert "pixel_values" in input_names - assert "text_input" in input_names + input_shapes = { + tensor.name: tuple(dim.dim_value for dim in tensor.type.tensor_type.shape.dim) + for tensor in onnx_model.graph.input + } + assert input_shapes == { + "text_input": (1, 16), + "pixel_values": (1, 3, 8, 8), + } class TestStaleExternalDataCleanup: