Skip to content

Commit 1738550

Browse files
xiaoyu-workCopilotCopilot
authored
Add mobius to capture onnx graph cli (microsoft#2471)
## Describe your changes Add `--use_mobius_builder` flag to the `capture-onnx-graph` CLI command to support MobiusBuilder (mobius-ai) for capturing ONNX models, with multi-component multimodal model (VLM) support. The three mutually exclusive exporter flags (`--use_dynamo_exporter`, `--use_model_builder`, `--use_mobius_builder`) are now enforced as mutually exclusive at the argparse layer via `add_mutually_exclusive_group`, providing immediate and consistent CLI feedback instead of a deferred runtime error. ## Checklist before requesting a review - [ ] Add unit tests for this change. - [ ] Make sure all tests can pass. - [ ] Update documents if necessary. - [ ] Lint and apply fixes to your code by running `lintrunner -a` - [ ] Is this a user-facing change? If yes, give a description of this change to be included in the release notes. ## (Optional) Issue link --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0c888a8 commit 1738550

2 files changed

Lines changed: 144 additions & 16 deletions

File tree

olive/cli/capture_onnx.py

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,30 @@ def register_subcommand(parser: ArgumentParser):
6666
help="The device used to run the model to capture the ONNX graph.",
6767
)
6868

69-
# PyTorch Exporter options
70-
pte_group = sub_parser.add_argument_group("PyTorch Exporter options")
71-
pte_group.add_argument(
69+
# Mutually exclusive exporter flags
70+
exporter_group = sub_parser.add_mutually_exclusive_group()
71+
exporter_group.add_argument(
7272
"--use_dynamo_exporter",
7373
action="store_true",
7474
help="Whether to use dynamo_export API to export ONNX model.",
7575
)
76+
exporter_group.add_argument(
77+
"--use_model_builder",
78+
action="store_true",
79+
help="Whether to use Model Builder to capture ONNX model.",
80+
)
81+
exporter_group.add_argument(
82+
"--use_mobius_builder",
83+
action="store_true",
84+
help=(
85+
"Whether to use MobiusBuilder (mobius-ai) to capture ONNX model. "
86+
"Supports multi-component multimodal models (VLMs). "
87+
"Requires 'pip install mobius-ai'."
88+
),
89+
)
90+
91+
# PyTorch Exporter options
92+
pte_group = sub_parser.add_argument_group("PyTorch Exporter options")
7693
pte_group.add_argument(
7794
"--fixed_param_dict",
7895
type=parse_dim_dict,
@@ -108,11 +125,6 @@ def register_subcommand(parser: ArgumentParser):
108125

109126
# Model Builder options
110127
mb_group = sub_parser.add_argument_group("Model Builder options")
111-
mb_group.add_argument(
112-
"--use_model_builder",
113-
action="store_true",
114-
help="Whether to use Model Builder to capture ONNX model.",
115-
)
116128
mb_group.add_argument(
117129
"--precision",
118130
type=str,
@@ -182,12 +194,16 @@ def run(self):
182194
def _get_run_config(self, tempdir: str) -> dict:
183195
config = deepcopy(TEMPLATE)
184196

185-
# Check if diffusers model detection is needed
186-
is_diffusers = is_valid_diffusers_model(self.args.model_name_or_path) if self.args.model_name_or_path else False
187-
if is_diffusers:
188-
input_model_config = get_diffusers_input_model(self.args, self.args.model_name_or_path)
189-
else:
197+
if self.args.use_mobius_builder:
190198
input_model_config = get_input_model_config(self.args)
199+
else:
200+
is_diffusers = (
201+
is_valid_diffusers_model(self.args.model_name_or_path) if self.args.model_name_or_path else False
202+
)
203+
if is_diffusers:
204+
input_model_config = get_diffusers_input_model(self.args, self.args.model_name_or_path)
205+
else:
206+
input_model_config = get_input_model_config(self.args)
191207
assert input_model_config["type"].lower() in {
192208
"hfmodel",
193209
"pytorchmodel",
@@ -197,8 +213,14 @@ def _get_run_config(self, tempdir: str) -> dict:
197213
is_diffusers_model = input_model_config["type"].lower() == "diffusersmodel"
198214

199215
# whether model is in fp16 or bf16 (currently not supported by CPU EP)
200-
is_fp16_or_bf16 = (not self.args.use_model_builder and self.args.torch_dtype == "float16") or (
201-
self.args.use_model_builder and self.args.precision in ("fp16", "bf16")
216+
is_fp16_or_bf16 = (
217+
(
218+
not self.args.use_model_builder
219+
and not self.args.use_mobius_builder
220+
and self.args.torch_dtype == "float16"
221+
)
222+
or (self.args.use_model_builder and self.args.precision in ("fp16", "bf16"))
223+
or (self.args.use_mobius_builder and self.args.precision in ("fp16", "bf16"))
202224
)
203225
to_replace = [
204226
("input_model", input_model_config),
@@ -211,8 +233,26 @@ def _get_run_config(self, tempdir: str) -> dict:
211233
),
212234
]
213235

214-
if is_diffusers_model:
236+
if self.args.use_mobius_builder:
237+
if self.args.precision not in ("fp32", "fp16", "bf16"):
238+
raise ValueError(
239+
f"MobiusBuilder supports precisions fp32/fp16/bf16; got '{self.args.precision}'. "
240+
"For INT4, capture in fp32/fp16/bf16 first and run a quantization pass afterwards."
241+
)
242+
del config["passes"]["c"]
243+
del config["passes"]["m"]
244+
to_replace.extend(
245+
[
246+
(("passes", "b", "precision"), self.args.precision),
247+
(
248+
("passes", "b", "runtime"),
249+
"ort-genai" if self.args.use_ort_genai else "none",
250+
),
251+
]
252+
)
253+
elif is_diffusers_model:
215254
del config["passes"]["m"]
255+
del config["passes"]["b"]
216256
to_replace.extend(
217257
[
218258
(
@@ -225,6 +265,7 @@ def _get_run_config(self, tempdir: str) -> dict:
225265
)
226266
elif self.args.use_model_builder:
227267
del config["passes"]["c"]
268+
del config["passes"]["b"]
228269
to_replace.extend(
229270
[
230271
(("passes", "m", "precision"), self.args.precision),
@@ -245,6 +286,7 @@ def _get_run_config(self, tempdir: str) -> dict:
245286
if self.args.int4_accuracy_level is not None:
246287
to_replace.append((("passes", "m", "int4_accuracy_level"), self.args.int4_accuracy_level))
247288
else:
289+
del config["passes"]["b"]
248290
to_replace.extend(
249291
[
250292
(
@@ -300,6 +342,7 @@ def _get_run_config(self, tempdir: str) -> dict:
300342
"type": "OnnxConversion",
301343
},
302344
"m": {"type": "ModelBuilder", "metadata_only": False},
345+
"b": {"type": "MobiusBuilder"},
303346
"f": {"type": "DynamicToFixedShape"},
304347
},
305348
"host": "local_system",

test/cli/test_cli.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,91 @@ def test_capture_onnx_command_fix_shape(_, mock_run, use_model_builder, tmp_path
440440
assert mock_run.call_count == 1
441441

442442

443+
@patch("olive.workflows.run")
444+
@patch("huggingface_hub.repo_exists", return_value=True)
445+
@pytest.mark.parametrize(
446+
("precision", "use_ort_genai"),
447+
[
448+
("fp16", True),
449+
("fp32", False),
450+
("bf16", True),
451+
],
452+
)
453+
def test_capture_onnx_command_use_mobius_builder(_, mock_run, precision, use_ort_genai, tmp_path):
454+
# setup
455+
output_dir = tmp_path / "output_dir"
456+
model_id = "dummy-model-id"
457+
command_args = [
458+
"capture-onnx-graph",
459+
"-m",
460+
model_id,
461+
"-o",
462+
str(output_dir),
463+
"--use_mobius_builder",
464+
"--precision",
465+
precision,
466+
]
467+
if use_ort_genai:
468+
command_args.append("--use_ort_genai")
469+
470+
# execute
471+
cli_main(command_args)
472+
473+
config = mock_run.call_args[0][0]
474+
assert config["input_model"]["model_path"] == model_id
475+
# MobiusBuilder ("b") is the only conversion pass; "c" (OnnxConversion) and "m" (ModelBuilder) are removed.
476+
assert "b" in config["passes"]
477+
assert "c" not in config["passes"]
478+
assert "m" not in config["passes"]
479+
assert config["passes"]["b"]["type"] == "MobiusBuilder"
480+
assert config["passes"]["b"]["precision"] == precision
481+
assert config["passes"]["b"]["runtime"] == ("ort-genai" if use_ort_genai else "none")
482+
assert mock_run.call_count == 1
483+
484+
485+
@patch("olive.workflows.run")
486+
@patch("huggingface_hub.repo_exists", return_value=True)
487+
def test_capture_onnx_command_use_mobius_builder_rejects_int4(_, __, tmp_path):
488+
# setup
489+
output_dir = tmp_path / "output_dir"
490+
command_args = [
491+
"capture-onnx-graph",
492+
"-m",
493+
"dummy-model-id",
494+
"-o",
495+
str(output_dir),
496+
"--use_mobius_builder",
497+
"--precision",
498+
"int4",
499+
]
500+
501+
# execute / verify
502+
with pytest.raises(ValueError, match="MobiusBuilder supports precisions fp32/fp16/bf16"):
503+
cli_main(command_args)
504+
505+
506+
@patch("olive.workflows.run")
507+
@patch("huggingface_hub.repo_exists", return_value=True)
508+
@pytest.mark.parametrize("conflicting_flag", ["--use_model_builder", "--use_dynamo_exporter"])
509+
def test_capture_onnx_command_use_mobius_builder_rejects_conflicts(_, __, conflicting_flag, tmp_path):
510+
# setup
511+
output_dir = tmp_path / "output_dir"
512+
command_args = [
513+
"capture-onnx-graph",
514+
"-m",
515+
"dummy-model-id",
516+
"-o",
517+
str(output_dir),
518+
"--use_mobius_builder",
519+
conflicting_flag,
520+
]
521+
522+
# execute / verify
523+
with pytest.raises(SystemExit) as exc_info:
524+
cli_main(command_args)
525+
assert exc_info.value.code == 2
526+
527+
443528
@patch("olive.cli.shared_cache.AzureContainerClientFactory")
444529
def test_shared_cache_command(mock_AzureContainerClientFactory):
445530
# setup

0 commit comments

Comments
 (0)