Skip to content

Derive layer names from ONNX node and CaptureSplitInfo emit assignmen… - #2641

Open
neilmsft wants to merge 2 commits into
mainfrom
neilmsft/capture-split-info-onnx
Open

Derive layer names from ONNX node and CaptureSplitInfo emit assignmen…#2641
neilmsft wants to merge 2 commits into
mainfrom
neilmsft/capture-split-info-onnx

Conversation

@neilmsft

@neilmsft neilmsft commented Aug 26, 2026

Copy link
Copy Markdown

Describe your changes

What the two changes do

  1. capture_split_info.py : make the pass work on ONNX

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.

  1. split.py : let the two passes hand off

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.

Copilot AI lite review requested due to automatic review settings August 26, 2026 22:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 CaptureSplitInfo via split_using_num_splits_onnx, inferring block_to_split from ONNX node names.
  • Store split_assignments into model_attributes for ONNX models, and update SplitModel to 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_splits is treated as a truthy value here (if not config.num_splits / elif config.num_splits), which makes num_splits=0 pass validate_config (it’s not None) but fail at runtime with a misleading error path. It also makes the PyTorch/HF branch silently ignore num_splits=0 and fall through to cost_model/error. Prefer explicit is None checks and validate that num_splits is a positive integer before calling np.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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants