Derive layer names from ONNX node and CaptureSplitInfo emit assignmen… - #2641
Open
neilmsft wants to merge 2 commits into
Open
Derive layer names from ONNX node and CaptureSplitInfo emit assignmen…#2641neilmsft wants to merge 2 commits into
neilmsft wants to merge 2 commits into
Conversation
…ts into model_attributes
Contributor
There was a problem hiding this comment.
Pull request overview
This PR extends Olive’s split-assignment workflow to better support ONNX-first pipelines by deriving layer namespaces from ONNX node names in CaptureSplitInfo, persisting assignments into model_attributes, and teaching SplitModel to consume those attributes (plus adding a retry path for ORT symbolic shape inference when Constant attributes are encoded in alternate forms).
Changes:
- Add ONNX support to
CaptureSplitInfoviasplit_using_num_splits_onnx, inferringblock_to_splitfrom ONNX node names. - Store
split_assignmentsintomodel_attributesfor ONNX models, and updateSplitModelto read them from there when metadata/config doesn’t provide them. - Add Constant-node attribute normalization and a retry path for ORT symbolic shape inference in
SplitModel.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
olive/passes/pytorch/capture_split_info.py |
Adds ONNXModelHandler support and ONNX node-name based split assignment derivation; persists assignments into model_attributes. |
olive/passes/onnx/split.py |
Reads split assignments from model_attributes and retries shape inference after normalizing Constant attributes. |
Suppressed comments (1)
olive/passes/pytorch/capture_split_info.py:102
num_splitsis treated as a truthy value here (if not config.num_splits/elif config.num_splits), which makesnum_splits=0passvalidate_config(it’s notNone) but fail at runtime with a misleading error path. It also makes the PyTorch/HF branch silently ignorenum_splits=0and fall through tocost_model/error. Prefer explicitis Nonechecks and validate thatnum_splitsis a positive integer before callingnp.array_split.
split_assignments = None
if isinstance(model, ONNXModelHandler):
if not config.num_splits:
raise ValueError("num_splits is required to split an ONNX model. cost_model is not supported.")
split_assignments = self.split_using_num_splits_onnx(model, config)
elif config.num_splits:
split_assignments = self.split_using_num_splits(model, config)
elif config.cost_model:
split_assignments = self.split_using_cost_model(model, config)
else:
raise ValueError("One of num_splits or cost_model is required.")
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+34
to
+44
| for attribute in list(node.attribute): | ||
| if attribute.name == "value_int": | ||
| elem_type, dims, vals = onnx.TensorProto.INT64, [], [attribute.i] | ||
| elif attribute.name == "value_ints": | ||
| elem_type, dims, vals = onnx.TensorProto.INT64, [len(attribute.ints)], list(attribute.ints) | ||
| elif attribute.name == "value_float": | ||
| elem_type, dims, vals = onnx.TensorProto.FLOAT, [], [attribute.f] | ||
| elif attribute.name == "value_floats": | ||
| elem_type, dims, vals = onnx.TensorProto.FLOAT, [len(attribute.floats)], list(attribute.floats) | ||
| else: | ||
| continue |
Comment on lines
+133
to
+171
| def split_using_num_splits_onnx(self, model: ONNXModelHandler, config: type[BasePassConfig]) -> dict[str, int]: | ||
| # node names carry the module namespace, so the weights are never needed here | ||
| model_proto = onnx.load(model.model_path, load_external_data=False) | ||
| node_names = [node.name for node in model_proto.graph.node if node.name] | ||
|
|
||
| blocks = self._get_indexed_blocks(node_names) | ||
| block_to_split = config.block_to_split | ||
| # check for None specifically since "" is a valid value | ||
| if block_to_split is None: | ||
| if not blocks: | ||
| raise ValueError("block_to_split is not set and could not be inferred. Please set it manually.") | ||
| # the transformer layer block is the namespace with the most numbered children | ||
| block_to_split = max(blocks, key=lambda name: len(blocks[name])) | ||
| logger.debug("Inferred block_to_split as '%s' with %d members.", block_to_split, len(blocks[block_to_split])) | ||
|
|
||
| block_to_splits = block_to_split if isinstance(block_to_split, list) else [block_to_split] | ||
|
|
||
| block_members = [] | ||
| for block_name in block_to_splits: | ||
| member_indices = blocks.get(block_name) | ||
| if not member_indices: | ||
| raise ValueError(f"Could not find any members for block '{block_name}' in the ONNX model.") | ||
| block_members.extend(f"{block_name}.{index}".lstrip(".") for index in sorted(member_indices)) | ||
|
|
||
| split_assignments, used_splits, modules_to_exclude = self._init_split_assignments( | ||
| model, None, config.unique_embeds_lm_head_splits | ||
| ) | ||
|
|
||
| for split_idx, split_members in enumerate(np.array_split(block_members, config.num_splits)): | ||
| for member_name in split_members: | ||
| if member_name in modules_to_exclude: | ||
| continue | ||
| split_assignments[member_name] = split_idx + used_splits | ||
|
|
||
| if config.unique_embeds_lm_head_splits: | ||
| # assign lm_head layer to its own split | ||
| split_assignments["lm_head"] = config.num_splits + used_splits | ||
|
|
||
| return split_assignments |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Describe your changes
What the two changes do
Instead of a torch module tree, derive the layer list from ONNX node names. Mobius names nodes after their originating module ( model/layers.0/self_attn/q_proj/MatMul ), so the structure is already there. The pass normalizes / → . , groups by namespace, and picks the one with the most numbered children → model.layers , indices 0–34.
This is safe rather than a guess: SplitModel.get_assignment ( split.py:255 ) already normalizes node names the exact same way to match assignments back to nodes. I'm just reading the naming convention the consumer side already assumes.
CaptureSplitInfo writes assignments to model_attributes ; SplitModel didn't look there. Without this the passes can't chain. The file carried an explicit TODO(jambayk): Should we allow split assignments in the model attributes too? So this closes a sanctioned gap.