From c36380b3facf093368f36e980dbdce299c0e741b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Dupr=C3=A9?= Date: Thu, 22 Jan 2026 15:08:43 +0100 Subject: [PATCH 1/7] add function to partition a model into local functions --- _doc/api/api.rst | 7 - _doc/api/index.rst | 2 +- _doc/api/typing.rst | 6 + _unittests/ut_helpers/test_onnx_helper.py | 187 +++++++++++++++++++++ onnx_diagnostic/helpers/onnx_helper.py | 191 +++++++++++++++++++++- 5 files changed, 380 insertions(+), 13 deletions(-) delete mode 100644 _doc/api/api.rst create mode 100644 _doc/api/typing.rst diff --git a/_doc/api/api.rst b/_doc/api/api.rst deleted file mode 100644 index 47811c34..00000000 --- a/_doc/api/api.rst +++ /dev/null @@ -1,7 +0,0 @@ - -onnx_diagnostic.api -=================== - -.. automodule:: onnx_diagnostic.api - :members: - :no-undoc-members: diff --git a/_doc/api/index.rst b/_doc/api/index.rst index 52b01674..3b32e2ce 100644 --- a/_doc/api/index.rst +++ b/_doc/api/index.rst @@ -20,7 +20,7 @@ API of onnx_diagnostic :maxdepth: 1 :caption: modules - api + typing ext_test_case .. automodule:: onnx_diagnostic diff --git a/_doc/api/typing.rst b/_doc/api/typing.rst new file mode 100644 index 00000000..962cf1b1 --- /dev/null +++ b/_doc/api/typing.rst @@ -0,0 +1,6 @@ + +onnx_diagnostic.typing +====================== + +.. automodule:: onnx_diagnostic.typing + :members: diff --git a/_unittests/ut_helpers/test_onnx_helper.py b/_unittests/ut_helpers/test_onnx_helper.py index e9f0a1fb..a0a4312a 100644 --- a/_unittests/ut_helpers/test_onnx_helper.py +++ b/_unittests/ut_helpers/test_onnx_helper.py @@ -28,9 +28,12 @@ shadowing_names, onnx_dtype_name, extract_subset_of_nodes, + make_subfunction, make_submodel, + make_model_with_local_functions, select_model_inputs_outputs, _enumerate_model_node_outputs, + pretty_onnx, ) TFLOAT = TensorProto.FLOAT @@ -537,6 +540,46 @@ def _type_rank_fn(name): check_model(new_model) self.check_ort(new_model) + def test_make_subfunction(self): + model = oh.make_model( + oh.make_graph( + [ + oh.make_node("Unsqueeze", ["X", "zero"], ["xu1"]), + oh.make_node("Unsqueeze", ["xu1", "un"], ["xu2"]), + oh.make_node("Reshape", ["xu2", "shape1"], ["xm1"]), + oh.make_node("Reshape", ["Y", "shape2"], ["xm2c"]), + oh.make_node("Cast", ["xm2c"], ["xm2"], to=1), + oh.make_node("MatMul", ["xm1", "xm2"], ["xm"]), + oh.make_node("Reshape", ["xm", "shape3"], ["Z"]), + ], + "dummy", + [oh.make_tensor_value_info("X", TFLOAT, [320, 1280])], + [oh.make_tensor_value_info("Z", TFLOAT, [3, 5, 320, 640])], + [ + onh.from_array( + np.random.rand(3, 5, 1280, 640).astype(np.float32), name="Y" + ), + onh.from_array(np.array([0], dtype=np.int64), name="zero"), + onh.from_array(np.array([1], dtype=np.int64), name="un"), + onh.from_array(np.array([1, 320, 1280], dtype=np.int64), name="shape1"), + onh.from_array(np.array([15, 1280, 640], dtype=np.int64), name="shape2"), + onh.from_array(np.array([3, 5, 320, 640], dtype=np.int64), name="shape3"), + ], + ), + opset_imports=[oh.make_opsetid("", 18)], + ir_version=9, + ) + new_function = make_subfunction( + "localf", + model.graph.node[:4], + opset_imports=model.opset_import, + output_names=["xm1", "xm2c"], + ) + self.assertIsInstance(new_function, FunctionProto) + self.assertEqual(len(new_function.node), 4) + self.assertEqual(new_function.output, ["xm1", "xm2c"]) + self.assertEqual(new_function.input, ["X", "Y", "shape1", "shape2", "un", "zero"]) + def test_extract_subset_of_nodes_bigger(self): model = onnx.load( os.path.join( @@ -670,6 +713,150 @@ def enumerate_model_tensors(model): got = sess.run(None, {"X": x})[0] self.assertEqual((x**2 + y).tolist(), got.tolist()) + def test_make_model_with_local_functions(self): + model = oh.make_model( + oh.make_graph( + [ + oh.make_node("Unsqueeze", ["X", "zero"], ["xu1"]), + oh.make_node("Unsqueeze", ["xu1", "un"], ["xu2"]), + oh.make_node("Reshape", ["xu2", "shape1"], ["xm1"]), + oh.make_node("Reshape", ["Y", "shape2"], ["xm2c"]), + oh.make_node("Cast", ["xm2c"], ["xm2"], to=1), + oh.make_node("MatMul", ["xm1", "xm2"], ["xm"]), + oh.make_node("Reshape", ["xm", "shape3"], ["Z"]), + ], + "dummy", + [oh.make_tensor_value_info("X", TFLOAT, [320, 1280])], + [oh.make_tensor_value_info("Z", TFLOAT, [3, 5, 320, 640])], + [ + onh.from_array( + np.random.rand(3, 5, 1280, 640).astype(np.float32), name="Y" + ), + onh.from_array(np.array([0], dtype=np.int64), name="zero"), + onh.from_array(np.array([1], dtype=np.int64), name="un"), + onh.from_array(np.array([1, 320, 1280], dtype=np.int64), name="shape1"), + onh.from_array(np.array([15, 1280, 640], dtype=np.int64), name="shape2"), + onh.from_array(np.array([3, 5, 320, 640], dtype=np.int64), name="shape3"), + ], + ), + opset_imports=[oh.make_opsetid("", 18)], + ir_version=9, + ) + for i_node in [0, 1, 2, 3]: + node = model.graph.node[i_node] + meta = node.metadata_props.add() + meta.key = "namespace" + meta.value = "LLL" + new_model = make_model_with_local_functions(model, "^LLL$") + check_model(model) + self.assertEqual(len(new_model.functions), 1) + self.assertEqual( + ["X", "Y", "shape1", "shape2", "un", "zero"], new_model.functions[0].input + ) + self.assertEqual(["xm1", "xm2c"], new_model.functions[0].output) + self.assertEqual("LLL", new_model.functions[0].name) + self.assertEqual("local_function", new_model.functions[0].domain) + self.assertIn("LLL[local_function]", pretty_onnx(new_model)) + check_model(new_model) + + def test_make_model_with_local_functions_bug(self): + model = oh.make_model( + oh.make_graph( + [ + oh.make_node("Unsqueeze", ["X", "zero"], ["xu1"]), + oh.make_node("Unsqueeze", ["xu1", "un"], ["xu2"]), + oh.make_node("Reshape", ["xu2", "shape1"], ["xm1"]), + oh.make_node("Reshape", ["Y", "shape2"], ["xm2c"]), + oh.make_node("Cast", ["xm2c"], ["xm2"], to=1), + oh.make_node("MatMul", ["xm1", "xm2"], ["xm"]), + oh.make_node("Reshape", ["xm", "shape3"], ["Z"]), + ], + "dummy", + [oh.make_tensor_value_info("X", TFLOAT, [320, 1280])], + [oh.make_tensor_value_info("Z", TFLOAT, [3, 5, 320, 640])], + [ + onh.from_array( + np.random.rand(3, 5, 1280, 640).astype(np.float32), name="Y" + ), + onh.from_array(np.array([0], dtype=np.int64), name="zero"), + onh.from_array(np.array([1], dtype=np.int64), name="un"), + onh.from_array(np.array([1, 320, 1280], dtype=np.int64), name="shape1"), + onh.from_array(np.array([15, 1280, 640], dtype=np.int64), name="shape2"), + onh.from_array(np.array([3, 5, 320, 640], dtype=np.int64), name="shape3"), + ], + ), + opset_imports=[oh.make_opsetid("", 18)], + ir_version=9, + ) + for i_node in [0, 2, 3, 4]: + node = model.graph.node[i_node] + meta = node.metadata_props.add() + meta.key = "namespace" + meta.value = "LLL" + self.assertRaise( + lambda: make_model_with_local_functions(model, "^LLL$"), + ValueError, + "Results {'xu1'} are needed for inputs ['X', 'Y', 'shape1', " + "'shape2', 'xu2', 'zero'] but also requires ['xm1', 'xm2', 'xu1'] " + "which is not allowed.", + ) + check_model(model) + + def test_make_model_with_local_functions_2(self): + model = oh.make_model( + oh.make_graph( + [ + oh.make_node("Unsqueeze", ["X", "zero"], ["xu1"]), + oh.make_node("Unsqueeze", ["xu1", "un"], ["xu2"]), + oh.make_node("Reshape", ["xu2", "shape1"], ["xm1"]), + oh.make_node("Reshape", ["Y", "shape2"], ["xm2c"]), + oh.make_node("Cast", ["xm2c"], ["xm2"], to=1), + oh.make_node("MatMul", ["xm1", "xm2"], ["xm"]), + oh.make_node("Reshape", ["xm", "shape3"], ["Z"]), + ], + "dummy", + [oh.make_tensor_value_info("X", TFLOAT, [320, 1280])], + [oh.make_tensor_value_info("Z", TFLOAT, [3, 5, 320, 640])], + [ + onh.from_array( + np.random.rand(3, 5, 1280, 640).astype(np.float32), name="Y" + ), + onh.from_array(np.array([0], dtype=np.int64), name="zero"), + onh.from_array(np.array([1], dtype=np.int64), name="un"), + onh.from_array(np.array([1, 320, 1280], dtype=np.int64), name="shape1"), + onh.from_array(np.array([15, 1280, 640], dtype=np.int64), name="shape2"), + onh.from_array(np.array([3, 5, 320, 640], dtype=np.int64), name="shape3"), + ], + ), + opset_imports=[oh.make_opsetid("", 18)], + ir_version=9, + ) + for i_node in [0, 1, 2, 3]: + node = model.graph.node[i_node] + meta = node.metadata_props.add() + meta.key = "namespace" + meta.value = f"LLL{i_node//3}" + new_model = make_model_with_local_functions(model, "^LLL[01]$") + check_model(model) + self.assertEqual(len(new_model.functions), 2) + p = pretty_onnx(new_model) + self.assertIn("LLL0[local_function]", p) + self.assertIn("LLL1[local_function]", p) + + self.assertEqual(["X", "shape1", "un", "zero"], new_model.functions[0].input) + self.assertEqual(["xm1"], new_model.functions[0].output) + self.assertEqual("LLL0", new_model.functions[0].name) + self.assertEqual("local_function", new_model.functions[0].domain) + self.assertEqual(len(new_model.functions[0].node), 3) + + self.assertEqual(["Y", "shape2"], new_model.functions[1].input) + self.assertEqual(["xm2c"], new_model.functions[1].output) + self.assertEqual("LLL1", new_model.functions[1].name) + self.assertEqual("local_function", new_model.functions[1].domain) + self.assertEqual(len(new_model.functions[1].node), 1) + + check_model(new_model) + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/onnx_diagnostic/helpers/onnx_helper.py b/onnx_diagnostic/helpers/onnx_helper.py index 83920ddc..38cd924d 100644 --- a/onnx_diagnostic/helpers/onnx_helper.py +++ b/onnx_diagnostic/helpers/onnx_helper.py @@ -1,6 +1,7 @@ import functools import json import os +import re import sys import warnings from typing import ( @@ -1313,14 +1314,13 @@ def make_submodel( nodes: List[NodeProto], ir_version: int, opset_imports: List[OperatorSetIdProto], - output_names: List[str], + output_names: Optional[List[str]], type_rank_fn: Callable[[str], Tuple[int, int]], ) -> ModelProto: """ Creates a model with the given list of nodes. It computes the minimum list of inputs needed for this model. The function assumes the nodes are sorted. - It does not handle yet subgraphs. :param nodes: list of nodes :param ir_version: ir version @@ -1343,17 +1343,55 @@ def _mkv_(name, itype, irank): if att.type == onnx.AttributeProto.GRAPH: not_known |= get_hidden_inputs(att.g) - model = oh.make_model( + return oh.make_model( oh.make_graph( nodes, "submodel", [_mkv_(n, *type_rank_fn(n)) for n in sorted(not_known) if n], - [_mkv_(n, *type_rank_fn(n)) for n in sorted(output_names) if n], + [_mkv_(n, *type_rank_fn(n)) for n in output_names if n], ), ir_version=ir_version, opset_imports=opset_imports, ) - return model + + +def make_subfunction( + name: str, + nodes: List[NodeProto], + opset_imports: List[OperatorSetIdProto], + output_names: List[str], + domain: str = "local_function", +) -> FunctionProto: + """ + Creates a function with the given list of nodes. + It computes the minimum list of inputs needed for this model. + The function assumes the nodes are sorted. + + :param name: function name + :param nodes: list of nodes + :param opset_imports: opset import + :param output_names: desired outputs + :param domain: function domain + :return: model proto + """ + not_known: Set[str] = set() + for node in nodes[::-1]: + not_known -= {o for o in node.output if o} + not_known |= {i for i in node.input if i} + if node.op_type in {"Scan", "If", "Loop"}: + # there are hidden inputs + for att in node.attribute: + if att.type == onnx.AttributeProto.GRAPH: + not_known |= get_hidden_inputs(att.g) + + return oh.make_function( + domain, + name, + nodes=nodes, + inputs=sorted(not_known), + outputs=output_names, + opset_imports=opset_imports, + ) def get_tensor_shape( @@ -1693,3 +1731,146 @@ def select_model_inputs_outputs( op_set.version = oimp.version return onnx_model + + +def _find_used_names(node_list, node_indices): + # find all the outputs the subset of nodes produces + possible_outputs = set() + for i_node in node_indices: + if not node_list[i_node]: + continue + possible_outputs |= {o for o in node_list[i_node].output if o} + # find all requires input from the other nodes + set_indices = set(node_indices) + not_known: Set[str] = set() + ranges = list(range(len(node_list))) + for i_node in ranges[::-1]: + if i_node in set_indices: + continue + node = node_list[i_node] + if not node: + continue + not_known -= {o for o in node.output if o} + not_known |= {i for i in node.input if i} + if node.op_type in {"Scan", "If", "Loop"}: + # there are hidden inputs + for att in node.attribute: + if att.type == onnx.AttributeProto.GRAPH: + not_known |= get_hidden_inputs(att.g) + # output + selection = possible_outputs & not_known + assert selection, ( + f"No output is needed, possible_outputs={sorted(possible_outputs)}, " + f"not_known={sorted(not_known)}" + ) + return sorted(selection) + + +def check_for_non_recursivity( + node_list: List[NodeProto], inputs: List[str], outputs: List[str] +): + """ + We finally need to check that any of this output is not required + by one input from the function itself, that would mean one node + needs an output of the function and is also required by the function: + it is probably missing from the initial set. + + + + :param node_list: list of nodes + :param inputs: input names to consider + :param outputs: output names which cannot be involved in input names + """ + set_inputs = set(inputs) + set_outputs = set(outputs) + for node in node_list[::-1]: + if not node: + continue + si = set(node.output) + if si & set_inputs: + set_inputs |= set(node.input) + if node.op_type in {"Scan", "If", "Loop"}: + # there are hidden inputs + for att in node.attribute: + if att.type == onnx.AttributeProto.GRAPH: + set_inputs |= get_hidden_inputs(att.g) + if set_outputs & set_inputs: + raise ValueError( + f"Results {set_outputs & set_inputs} are needed for inputs {inputs} " + f"but also requires {outputs} which is not allowed." + ) + + +def make_model_with_local_functions( + model: ModelProto, + regex: str, + domain: str = "local_function", + metadata_keys: Tuple[str, ...] = ("namespace",), +) -> FunctionProto: + """ + Selects nodes based on a regular expression, using metadata + ``'namespace'``. It is going to look into every value + matching the regular expression and partition the nodes based + on the unique values the regular expression finds. + Every set of nodes it replaced by a call to a local function. + + :param model: model proto + :param regex: regular expression + :param domain: function domain + :param metadata_keys: list of metadata keys to consider, + every value is split into multiple ones. + :return: model proto + """ + set_keys = set(metadata_keys) + reg = re.compile(regex) + unique: Dict[str, List[int]] = {} + for i, node in enumerate(model.graph.node): + selected = False + for data in node.metadata_props: + if data.key in set_keys: + values = data.value.split(",") + for v in values: + if reg.match(v): + if v not in unique: + unique[v] = [] + unique[v].append(i) + selected = True + break + if selected: + break + # sets of nodes. + if not unique: + return model + + functions = [] + new_nodes = list(model.graph.node) + for key, node_indices in unique.items(): + outputs = _find_used_names(new_nodes, node_indices) + nodes = [new_nodes[i] for i in node_indices] + lf = make_subfunction(key, nodes, model.opset_import, outputs, domain=domain) + check_for_non_recursivity(new_nodes, lf.input, lf.output) + functions.append(lf) + maxi = max(node_indices) + for i in node_indices: + new_nodes[i] = None + new_nodes[maxi] = oh.make_node(key, lf.input, lf.output, domain=domain) + + return oh.make_model( + oh.make_graph( + [n for n in new_nodes if n], + model.graph.name, + model.graph.input, + model.graph.output, + model.graph.initializer, + doc_string=model.graph.doc_string, + value_info=model.graph.value_info, + sparse_initializer=model.graph.sparse_initializer, + ), + ir_version=model.ir_version, + opset_imports=( + model.opset_import + if domain in {d.domain for d in model.opset_import} + else [*model.opset_import, oh.make_opsetid(domain, 1)] + ), + functions=[*model.functions, *functions], + ) From b16ef5a89e47a8206cf7ed9e854223d753665e25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Dupr=C3=A9?= Date: Thu, 22 Jan 2026 15:16:10 +0100 Subject: [PATCH 2/7] changelogs --- .github/workflows/ci.yml | 12 +++++------- CHANGELOGS.rst | 2 ++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd1479f3..68707605 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,33 +17,31 @@ jobs: matrix: os: [ubuntu-latest] python: ['3.10', '3.11', '3.12', '3.13'] - transformers: ['4.48.3', '4.51.3', '4.52.4', '4.55.4', '4.56.2', '4.57.6', 'main'] + transformers: ['4.48.3', '4.51.3', '4.55.4', '4.56.2', '4.57.6', 'main'] torch: ['2.9', 'main'] exclude: - python: '3.10' # 3.10 torch: 'main' - python: '3.10' torch: '2.9' - - python: '3.10' - transformers: 'main' - - python: '3.10' - transformers: '4.52.4' - python: '3.10' transformers: '4.55.4' - python: '3.10' transformers: '4.56.2' - python: '3.10' transformers: '4.57.6' + - python: '3.10' + transformers: 'main' - python: '3.11' # 3.11 torch: 'main' - - python: '3.11' - transformers: 'main' - python: '3.11' transformers: '4.55.4' - python: '3.11' transformers: '4.56.2' - python: '3.11' transformers: '4.57.6' + - python: '3.11' + transformers: 'main' - python: '3.13' # 3.11 torch: '2.9' - python: '3.13' diff --git a/CHANGELOGS.rst b/CHANGELOGS.rst index eee79a0d..d600abe3 100644 --- a/CHANGELOGS.rst +++ b/CHANGELOGS.rst @@ -4,6 +4,8 @@ Change Logs 0.8.11 ++++++ +* :pr:`394`: add function make_model_with_local_functions to partition a model into local functions + 0.8.10 ++++++ From e324aa94b09b21cd8b6239b4a71bd3ed1b6d96d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Dupr=C3=A9?= Date: Thu, 22 Jan 2026 15:24:14 +0100 Subject: [PATCH 3/7] support prefixes --- _unittests/ut_helpers/test_onnx_helper.py | 6 ++++-- onnx_diagnostic/helpers/onnx_helper.py | 10 +++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/_unittests/ut_helpers/test_onnx_helper.py b/_unittests/ut_helpers/test_onnx_helper.py index a0a4312a..791c684d 100644 --- a/_unittests/ut_helpers/test_onnx_helper.py +++ b/_unittests/ut_helpers/test_onnx_helper.py @@ -834,9 +834,11 @@ def test_make_model_with_local_functions_2(self): for i_node in [0, 1, 2, 3]: node = model.graph.node[i_node] meta = node.metadata_props.add() - meta.key = "namespace" + meta.key = f"source[{i_node}]" meta.value = f"LLL{i_node//3}" - new_model = make_model_with_local_functions(model, "^LLL[01]$") + new_model = make_model_with_local_functions( + model, "^LLL[01]$", metadata_key_prefix="source[" + ) check_model(model) self.assertEqual(len(new_model.functions), 2) p = pretty_onnx(new_model) diff --git a/onnx_diagnostic/helpers/onnx_helper.py b/onnx_diagnostic/helpers/onnx_helper.py index 38cd924d..5e8dc127 100644 --- a/onnx_diagnostic/helpers/onnx_helper.py +++ b/onnx_diagnostic/helpers/onnx_helper.py @@ -1805,7 +1805,7 @@ def make_model_with_local_functions( model: ModelProto, regex: str, domain: str = "local_function", - metadata_keys: Tuple[str, ...] = ("namespace",), + metadata_key_prefix: Union[str, Tuple[str, ...]] = ("namespace", "source["), ) -> FunctionProto: """ Selects nodes based on a regular expression, using metadata @@ -1821,13 +1821,17 @@ def make_model_with_local_functions( every value is split into multiple ones. :return: model proto """ - set_keys = set(metadata_keys) + prefix = ( + metadata_key_prefix + if isinstance(metadata_key_prefix, tuple) + else (metadata_key_prefix,) + ) reg = re.compile(regex) unique: Dict[str, List[int]] = {} for i, node in enumerate(model.graph.node): selected = False for data in node.metadata_props: - if data.key in set_keys: + if data.key.startswith(prefix): values = data.value.split(",") for v in values: if reg.match(v): From 3b445a8e720ad7f8cc629c56541025a2e4a8fef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Dupr=C3=A9?= Date: Thu, 22 Jan 2026 16:21:43 +0100 Subject: [PATCH 4/7] documentation and verbosity --- _doc/cmds/index.rst | 1 + _doc/cmds/partition.rst | 47 ++++++++++++ _unittests/ut_helpers/test_onnx_helper.py | 3 +- _unittests/ut_xrun_doc/test_command_lines.py | 8 ++ .../ut_xrun_doc/test_command_lines_exe.py | 8 ++ onnx_diagnostic/_command_lines_parser.py | 74 +++++++++++++++++++ onnx_diagnostic/helpers/onnx_helper.py | 24 +++++- 7 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 _doc/cmds/partition.rst diff --git a/_doc/cmds/index.rst b/_doc/cmds/index.rst index a357777a..a211ba04 100644 --- a/_doc/cmds/index.rst +++ b/_doc/cmds/index.rst @@ -11,5 +11,6 @@ Command Lines compare config optimize + partition sbs validate diff --git a/_doc/cmds/partition.rst b/_doc/cmds/partition.rst new file mode 100644 index 00000000..46f9a1ac --- /dev/null +++ b/_doc/cmds/partition.rst @@ -0,0 +1,47 @@ +-m onnx_diagnostic partition ... move layer nodes in local functions +==================================================================== + +The command line leverages the metadata added by the exporter. +Every node is tagged with information indicating which part of the model +it comes from. In particular the eky `namespace`: + +:: + + transformers.models.llama.modeling_llama.LlamaForCausalLM/model: + transformers.models.llama.modeling_llama.LlamaModel/model.layers.0: + transformers.models.llama.modeling_llama.LlamaDecoderLayer/model.layers.0.self_attn: + transformers.models.llama.modeling_llama.LlamaAttention/unsqueeze_15: + aten.unsqueeze.default + +Description ++++++++++++ + +See :func:`onnx_diagnostic.helpers.optim_helper.make_model_with_local_functions`. + +.. runpython:: + + from onnx_diagnostic._command_lines_parser import get_parser_partition + + get_parser_partition().print_help() + +Example ++++++++ + +.. code-block:: bash + + python -m onnx_diagnostic partition arnir0_Tiny-LLM-onnx-dynamo-ir-f16-cuda-op18.onnx partition.onnx -r ".*[.]layers[.][0-9]+$" -v 1 + +This produces the following output: + +:: + + -- load 'arnir0_Tiny-LLM-onnx-dynamo-ir-f16-cuda-op18.onnx' + -- partition + [make_model_with_local_functions] matched 1 partitions + [make_model_with_local_functions] move 89 nodes in partition 'transformers_models_llama_modeling_llama_LlamaModel/model_layers_0' + -- save into 'partition.onnx' + -- done + +The partioned model includes the following node: + +.. image:: _img_partition.png diff --git a/_unittests/ut_helpers/test_onnx_helper.py b/_unittests/ut_helpers/test_onnx_helper.py index 791c684d..5d83a711 100644 --- a/_unittests/ut_helpers/test_onnx_helper.py +++ b/_unittests/ut_helpers/test_onnx_helper.py @@ -802,6 +802,7 @@ def test_make_model_with_local_functions_bug(self): ) check_model(model) + @hide_stdout() def test_make_model_with_local_functions_2(self): model = oh.make_model( oh.make_graph( @@ -837,7 +838,7 @@ def test_make_model_with_local_functions_2(self): meta.key = f"source[{i_node}]" meta.value = f"LLL{i_node//3}" new_model = make_model_with_local_functions( - model, "^LLL[01]$", metadata_key_prefix="source[" + model, "^LLL[01]$", metadata_key_prefix="source[", verbose=1 ) check_model(model) self.assertEqual(len(new_model.functions), 2) diff --git a/_unittests/ut_xrun_doc/test_command_lines.py b/_unittests/ut_xrun_doc/test_command_lines.py index 60f37180..07644e21 100644 --- a/_unittests/ut_xrun_doc/test_command_lines.py +++ b/_unittests/ut_xrun_doc/test_command_lines.py @@ -11,6 +11,7 @@ get_parser_find, get_parser_lighten, get_parser_optimize, + get_parser_partition, get_parser_print, get_parser_sbs, get_parser_stats, @@ -186,6 +187,13 @@ def test_parser_optimize(self): text = st.getvalue() self.assertIn("default", text) + def test_parser_partition(self): + st = StringIO() + with redirect_stdout(st): + get_parser_partition().print_help() + text = st.getvalue() + self.assertIn("regex", text) + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/_unittests/ut_xrun_doc/test_command_lines_exe.py b/_unittests/ut_xrun_doc/test_command_lines_exe.py index e88ecb87..bd78aae7 100644 --- a/_unittests/ut_xrun_doc/test_command_lines_exe.py +++ b/_unittests/ut_xrun_doc/test_command_lines_exe.py @@ -219,6 +219,14 @@ def test_l_parser_optimize(self): self.assertIn("default", text) self.assertExists(output) + def test_m_parser_partition(self): + output = self.get_dump_file("test_parser_partition.onnx") + st = StringIO() + with redirect_stdout(st): + main(["partition", self.dummy_path, output, "-v", "1"]) + text = st.getvalue() + self.assertIn("-- done", text) + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/onnx_diagnostic/_command_lines_parser.py b/onnx_diagnostic/_command_lines_parser.py index 7177ddb9..25897600 100644 --- a/onnx_diagnostic/_command_lines_parser.py +++ b/onnx_diagnostic/_command_lines_parser.py @@ -1572,6 +1572,76 @@ def _cmd_optimize(argv: List[Any]): ) +def get_parser_partition() -> ArgumentParser: + parser = ArgumentParser( + prog="partition", + formatter_class=RawTextHelpFormatter, + description=textwrap.dedent(""" + Partitions an onnx model by moving nodes into local functions. + Exporters may add metadata to the onnx nodes telling which part + of the model it comes from (namespace, source, ...). + This nodes are moved into local functions. + """), + epilog=textwrap.dedent(""" + The regular may match the following values, + 'model.layers.0.forward', 'model.layers.1.forward', ... + A local function will be created for each distinct layer. + """), + ) + parser.add_argument("input", help="input model") + parser.add_argument("output", help="output model") + parser.add_argument( + "-r", + "--regex", + default=".*[.]layers[.][0-9]+[.]forward$", + help=textwrap.dedent(""" + merges all nodes sharing the same value in node metadata, + these values must match the regular expression specified by + this parameter, the default value matches what transformers + usually to define a layer + """).strip("\n"), + ) + parser.add_argument( + "-p", + "--meta-prefix", + default="namespace,source[", + help="allowed prefixes for keys in the metadata", + ) + parser.add_argument( + "-v", + "--verbose", + default=0, + required=False, + type=int, + help="verbosity", + ) + return parser + + +def _cmd_partition(argv: List[Any]): + from .helpers.onnx_helper import make_model_with_local_functions + + parser = get_parser_partition() + args = parser.parse_args(argv[1:]) + + if args.verbose: + print(f"-- load {args.input!r}") + onx = onnx.load(args.input, load_external_data=False) + if args.verbose: + print("-- partition") + onx2 = make_model_with_local_functions( + onx, + regex=args.regex, + metadata_key_prefix=tuple(args.meta_prefix.split(",")), + verbose=args.verbose, + ) + if args.verbose: + print(f"-- save into {args.output!r}") + onnx.save(onx2, args.output) + if args.verbose: + print("-- done") + + ############# # main parser ############# @@ -1593,6 +1663,7 @@ def get_main_parser() -> ArgumentParser: find - find node consuming or producing a result lighten - makes an onnx model lighter by removing the weights optimize - optimizes an onnx model + partition - partition a model, each partition appears as local function print - prints the model on standard output sbs - compares an exported program and a onnx model stats - produces statistics on a model @@ -1610,6 +1681,7 @@ def get_main_parser() -> ArgumentParser: "find", "lighten", "optimize", + "partition", "print", "sbs", "stats", @@ -1631,6 +1703,7 @@ def main(argv: Optional[List[Any]] = None): find=_cmd_find, lighten=_cmd_lighten, optimize=_cmd_optimize, + partition=_cmd_partition, print=_cmd_print, sbs=_cmd_sbs, stats=_cmd_stats, @@ -1658,6 +1731,7 @@ def main(argv: Optional[List[Any]] = None): find=get_parser_find, lighten=get_parser_lighten, optimize=get_parser_optimize, + partition=get_parser_partition, print=get_parser_print, sbs=get_parser_sbs, stats=get_parser_stats, diff --git a/onnx_diagnostic/helpers/onnx_helper.py b/onnx_diagnostic/helpers/onnx_helper.py index 5e8dc127..b403f4a5 100644 --- a/onnx_diagnostic/helpers/onnx_helper.py +++ b/onnx_diagnostic/helpers/onnx_helper.py @@ -1803,9 +1803,10 @@ def check_for_non_recursivity( def make_model_with_local_functions( model: ModelProto, - regex: str, + regex: str = ".*[.]layers[.][0-9]+[.]forward$", domain: str = "local_function", metadata_key_prefix: Union[str, Tuple[str, ...]] = ("namespace", "source["), + verbose: int = 0, ) -> FunctionProto: """ Selects nodes based on a regular expression, using metadata @@ -1819,6 +1820,7 @@ def make_model_with_local_functions( :param domain: function domain :param metadata_keys: list of metadata keys to consider, every value is split into multiple ones. + :param verbose: verbosity :return: model proto """ prefix = ( @@ -1827,37 +1829,51 @@ def make_model_with_local_functions( else (metadata_key_prefix,) ) reg = re.compile(regex) + unique_values = set() unique: Dict[str, List[int]] = {} for i, node in enumerate(model.graph.node): selected = False for data in node.metadata_props: if data.key.startswith(prefix): - values = data.value.split(",") + values = re.split("[,:]", data.value) for v in values: + if not v: + continue if reg.match(v): if v not in unique: unique[v] = [] unique[v].append(i) selected = True break + unique_values.add(v) if selected: break # sets of nodes. if not unique: + if verbose: + print(f"[make_model_with_local_functions] no match in {sorted(unique_values)}") return model + if verbose: + print(f"[make_model_with_local_functions] matched {len(unique)} partitions") functions = [] new_nodes = list(model.graph.node) for key, node_indices in unique.items(): + function_name = key.strip().replace(".", "_") + if verbose: + print( + f"[make_model_with_local_functions] move {len(node_indices)} " + f"nodes in partition {function_name!r}" + ) outputs = _find_used_names(new_nodes, node_indices) nodes = [new_nodes[i] for i in node_indices] - lf = make_subfunction(key, nodes, model.opset_import, outputs, domain=domain) + lf = make_subfunction(function_name, nodes, model.opset_import, outputs, domain=domain) check_for_non_recursivity(new_nodes, lf.input, lf.output) functions.append(lf) maxi = max(node_indices) for i in node_indices: new_nodes[i] = None - new_nodes[maxi] = oh.make_node(key, lf.input, lf.output, domain=domain) + new_nodes[maxi] = oh.make_node(lf.name, lf.input, lf.output, domain=lf.domain) return oh.make_model( oh.make_graph( From 9ac12df3650dee411b3ef3d2af7450b0c13c9f90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Dupr=C3=A9?= Date: Thu, 22 Jan 2026 16:26:48 +0100 Subject: [PATCH 5/7] speel --- _doc/cmds/partition.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_doc/cmds/partition.rst b/_doc/cmds/partition.rst index 46f9a1ac..85a779fa 100644 --- a/_doc/cmds/partition.rst +++ b/_doc/cmds/partition.rst @@ -42,6 +42,6 @@ This produces the following output: -- save into 'partition.onnx' -- done -The partioned model includes the following node: +The partitioned model includes the following node: .. image:: _img_partition.png From dbee819a3933fe1d23e624dbc519c7b55a312914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Dupr=C3=A9?= Date: Thu, 22 Jan 2026 16:42:12 +0100 Subject: [PATCH 6/7] refly --- onnx_diagnostic/helpers/onnx_helper.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/onnx_diagnostic/helpers/onnx_helper.py b/onnx_diagnostic/helpers/onnx_helper.py index b403f4a5..07a61f4d 100644 --- a/onnx_diagnostic/helpers/onnx_helper.py +++ b/onnx_diagnostic/helpers/onnx_helper.py @@ -1314,7 +1314,7 @@ def make_submodel( nodes: List[NodeProto], ir_version: int, opset_imports: List[OperatorSetIdProto], - output_names: Optional[List[str]], + output_names: List[str], type_rank_fn: Callable[[str], Tuple[int, int]], ) -> ModelProto: """ @@ -1358,7 +1358,7 @@ def _mkv_(name, itype, irank): def make_subfunction( name: str, nodes: List[NodeProto], - opset_imports: List[OperatorSetIdProto], + opset_imports: Sequence[OperatorSetIdProto], output_names: List[str], domain: str = "local_function", ) -> FunctionProto: @@ -1767,7 +1767,7 @@ def _find_used_names(node_list, node_indices): def check_for_non_recursivity( - node_list: List[NodeProto], inputs: List[str], outputs: List[str] + node_list: List[Optional[NodeProto]], inputs: Sequence[str], outputs: Sequence[str] ): """ We finally need to check that any of this output is not required @@ -1807,7 +1807,7 @@ def make_model_with_local_functions( domain: str = "local_function", metadata_key_prefix: Union[str, Tuple[str, ...]] = ("namespace", "source["), verbose: int = 0, -) -> FunctionProto: +) -> ModelProto: """ Selects nodes based on a regular expression, using metadata ``'namespace'``. It is going to look into every value @@ -1857,7 +1857,7 @@ def make_model_with_local_functions( if verbose: print(f"[make_model_with_local_functions] matched {len(unique)} partitions") functions = [] - new_nodes = list(model.graph.node) + new_nodes: List[Optional[NodeProto]] = list(model.graph.node) for key, node_indices in unique.items(): function_name = key.strip().replace(".", "_") if verbose: @@ -1866,8 +1866,14 @@ def make_model_with_local_functions( f"nodes in partition {function_name!r}" ) outputs = _find_used_names(new_nodes, node_indices) - nodes = [new_nodes[i] for i in node_indices] - lf = make_subfunction(function_name, nodes, model.opset_import, outputs, domain=domain) + function_nodes = [new_nodes[i] for i in node_indices] + lf = make_subfunction( + function_name, + [n for n in function_nodes if n], + model.opset_import, + outputs, + domain=domain, + ) check_for_non_recursivity(new_nodes, lf.input, lf.output) functions.append(lf) maxi = max(node_indices) From aa64836696453322876c59e55826f34577369d19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Xavier=20Dupr=C3=A9?= Date: Thu, 22 Jan 2026 17:43:35 +0100 Subject: [PATCH 7/7] fix doc --- _doc/cmds/_img_partition.png | Bin 0 -> 34428 bytes _doc/cmds/partition.rst | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 _doc/cmds/_img_partition.png diff --git a/_doc/cmds/_img_partition.png b/_doc/cmds/_img_partition.png new file mode 100644 index 0000000000000000000000000000000000000000..6b0f8e7c1416009736a01c703efbff0372473e83 GIT binary patch literal 34428 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 qE{G*~$f0r)koiEa`4*L%u457sU literal 0 HcmV?d00001 diff --git a/_doc/cmds/partition.rst b/_doc/cmds/partition.rst index 85a779fa..dcde17be 100644 --- a/_doc/cmds/partition.rst +++ b/_doc/cmds/partition.rst @@ -16,7 +16,7 @@ it comes from. In particular the eky `namespace`: Description +++++++++++ -See :func:`onnx_diagnostic.helpers.optim_helper.make_model_with_local_functions`. +See :func:`onnx_diagnostic.helpers.onnx_helper.make_model_with_local_functions`. .. runpython::