diff --git a/onnxscript/optimizer/_constant_folding.py b/onnxscript/optimizer/_constant_folding.py index bad159dfae..1d253d242a 100644 --- a/onnxscript/optimizer/_constant_folding.py +++ b/onnxscript/optimizer/_constant_folding.py @@ -189,6 +189,7 @@ class Replacement: class OptimizerState: def __init__(self): + self.opset_imports: dict[str, int] = {} self._sym_value_map: dict[ir.Value, SymbolicValue] = {} self._initializer_inputs: list[set[ir.Value]] = [] @@ -796,13 +797,21 @@ def concat_from_sequence(node: ir.Node, op, state: OptimizerState) -> ReturnValu logger.debug("ConcatFromSequence => Concat: %s", [x.name for x in inputs]) return op.Concat(*inputs, axis=axis) if new_axis == 1: - # Unsqueeze the inputs with concat axis if new_axis is 1 - axis_value = op.Constant(value_int=axis) + opset_version = state.opset_imports.get(node.domain) + if opset_version is None: + return None + # Unsqueeze takes axes as an attribute before opset 13. + axis_value = op.Constant(value_int=axis) if opset_version >= 13 else None unsqueezed_inputs = [] for node_input in inputs: - unsqueezed_input = op.Unsqueeze( - node_input, axis_value, _outputs=[f"{node_input.name}_unsqueeze"] - ) + if opset_version < 13: + unsqueezed_input = op.Unsqueeze( + node_input, axes=[axis], _outputs=[f"{node_input.name}_unsqueeze"] + ) + else: + unsqueezed_input = op.Unsqueeze( + node_input, axis_value, _outputs=[f"{node_input.name}_unsqueeze"] + ) unsqueezed_inputs.append(unsqueezed_input) # Send unsqueezed outputs to Concat logger.debug( @@ -1450,6 +1459,7 @@ def visit_function(self, function: ir.Function) -> None: def call(self, model: ir.Model) -> FoldConstantsResult: self._reset() self._opset_imports = model.opset_imports + self._state.opset_imports = self._opset_imports self.visit_graph(model.graph) for function in model.functions.values(): # TODO(rama): Should we specialize functions? diff --git a/onnxscript/optimizer/_constant_folding_test.py b/onnxscript/optimizer/_constant_folding_test.py index 6b8c0ad7f9..879c043aef 100644 --- a/onnxscript/optimizer/_constant_folding_test.py +++ b/onnxscript/optimizer/_constant_folding_test.py @@ -608,6 +608,94 @@ def test_static_split_to_sequence_with_list_split_no_keepdims_and_squence_at_is_ ReferenceEvaluator(onnx.parser.parse_model(model)).run(None, {"x": data})[0], ) + def test_concat_from_sequence_preserves_opset_axes_signature(self): + a = np.arange(6, dtype=np.float32).reshape(2, 3) + b = a + 10 + for opset in (11, 12, 13, 18): + for new_axis in (0, 1): + for axis in (1, -1): + with self.subTest(opset=opset, new_axis=new_axis, axis=axis): + expected = ( + np.stack([a, b], axis=axis) + if new_axis + else np.concatenate([a, b], axis=axis) + ) + model = onnx.helper.make_model( + onnx.helper.make_graph( + [ + onnx.helper.make_node( + "SequenceConstruct", ["a", "b"], ["seq"] + ), + onnx.helper.make_node( + "ConcatFromSequence", + ["seq"], + ["y"], + axis=axis, + new_axis=new_axis, + ), + ], + "concat_sequence", + [ + onnx.helper.make_tensor_value_info( + name, onnx.TensorProto.FLOAT, [2, 3] + ) + for name in ("a", "b") + ], + [ + onnx.helper.make_tensor_value_info( + "y", onnx.TensorProto.FLOAT, expected.shape + ) + ], + ), + opset_imports=[onnx.helper.make_opsetid("", opset)], + ir_version=10, + ) + onnx.checker.check_model(model, full_check=True) + optimized = optimizer.optimize(model) + onnx.checker.check_model(optimized, full_check=True) + actual = ReferenceEvaluator(optimized).run(None, {"a": a, "b": b})[0] + np.testing.assert_array_equal(actual, expected) + self.assertEqual( + {o.domain: o.version for o in optimized.opset_import}, {"": opset} + ) + + def test_concat_from_sequence_inherits_opset_in_if_branches(self): + a = np.arange(6, dtype=np.float32).reshape(2, 3) + b = a + 10 + for opset in (11, 12, 13, 18): + with self.subTest(opset=opset): + model = onnx.parser.parse_model( + f""" + + main (float[2,3] a, float[2,3] b, bool cond) => (float[2,2,3] y) {{ + y = If (cond) < + then_branch = then_graph () => (then_y) {{ + seq = SequenceConstruct (a, b) + then_y = ConcatFromSequence (seq) + }}, + else_branch = else_graph () => (else_y) {{ + seq = SequenceConstruct (b, a) + else_y = ConcatFromSequence (seq) + }} + > + }} + """ + ) + onnx.checker.check_model(model, full_check=True) + optimized = optimizer.optimize(model) + onnx.checker.check_model(optimized, full_check=True) + for attr in optimized.graph.node[0].attribute: + self.assertEqual( + [node.op_type for node in attr.g.node], + ["Unsqueeze", "Unsqueeze", "Concat"], + ) + for cond in (True, False): + actual = ReferenceEvaluator(optimized).run( + None, {"a": a, "b": b, "cond": np.array(cond)} + )[0] + expected = np.stack([a, b] if cond else [b, a], axis=1) + np.testing.assert_array_equal(actual, expected) + def test_split_to_sequence_and_concat_from_sequence_with_new_axis_0( self, ):