Skip to content

Commit 0c888a8

Browse files
CopilotxadupreCopilotgithub-advanced-security[bot]
authored
Add --test HF CLI path for 2-layer random model configs, olive run and ModelBuilder support, Qwen how-to/layer-types fix, and merge conflict resolution (microsoft#2459)
## Describe your changes Adds a CLI test path for Hugging Face models so generated `config.json` can carry a lightweight random-model definition instead of always using pretrained weights. When `--test` is passed, Olive now preserves the source architecture, instantiates a random model with 2 hidden layers, and can persist that test model for reuse. - **CLI/config support** - Added `--test` to HF-backed CLI commands using shared input-model options. - `--test` now accepts an optional folder path where the generated test model is saved for reuse. - Emitted `input_model.test_model_config` into generated run configs, and now also emits `input_model.test_model_path` when a save folder is provided or derived. ```json { "input_model": { "type": "HfModel", "model_path": "model-id", "test_model_config": { "hidden_layers": 2 }, "test_model_path": "path/to/test_model" } } ``` - When `--test` is used without an explicit folder, Olive uses `<output_path>/test_model`. - If `--test` is used in a context where no output path is available, Olive now fails clearly instead of silently skipping persistence. - **`olive run` support** - Extended `olive run --test` so it can apply the same lightweight HF test-model override to an existing Hugging Face `input_model` already present in a workflow config. - When `olive run --test` is used without an explicit folder, it derives the saved test-model location from the effective workflow output path. - `olive run --test` now fails clearly when the workflow config does not contain a Hugging Face `input_model`. - **HF model loading** - Extended HF config loading to accept `test_model_config`. - Derived a lightweight config from the original model config by overriding the architecture-specific hidden-layer field (`num_hidden_layers`, `num_layers`, `n_layer`, `n_layers`). - For Qwen-style configs that carry per-layer metadata, Olive now also trims `layer_types` to match the reduced hidden-layer count so the saved reduced config remains valid when reloaded. - Switched test-model loading to instantiate from config (`from_config`) so the model is random-initialized rather than loaded from pretrained weights. - Updated the test-model path to fail fast if the selected model class cannot be instantiated from the reduced config, instead of falling back to another candidate class that could produce a misleading larger model. - Refactored the `from_config` loading path to avoid nested `try/except` handling by only passing `trust_remote_code` when the model class signature supports it. - Added persistence/reuse support for test models: if `test_model_path` already contains a saved HF model, Olive loads that model instead of recreating it; otherwise it creates the reduced model once and saves it there. - **ModelBuilder support** - Updated the ModelBuilder pass so `--test` workflows export from the saved reduced Hugging Face test checkpoint instead of still using the original full checkpoint. - When `test_model_config` is present, ModelBuilder now materializes or reuses `test_model_path` before export and passes that saved checkpoint to the builder. - This fixes the smoke-test flow so it avoids the original full-model dtype path instead of only deferring the same failure. - **IO config / dummy input propagation** - Threaded `test_model_config` through HF IO-config and dummy-input generation so the reduced-layer model shape metadata stays consistent with the generated test model. - **Documentation** - Added a new how-to page showing how to convert a Qwen LLM with a quick `--test` smoke check first, then rerun the full conversion. - Linked the new how-to page from the docs How Tos index. - Updated the how-to flow to use `olive optimize --dry_run` followed by `olive run --test`, matching the new CLI support. - Clarified the smoke-test commands so the generated ONNX artifacts are written to a dedicated output folder and are easy to find. - Updated the example model from Phi to `Qwen/Qwen3-0.6B` and renamed the how-to page and index entry to match. - **Merge conflict resolution** - Merged `origin/main` into this PR branch and resolved the conflict in `test/passes/onnx/test_model_builder.py`. - Preserved both the upstream `ModelBuilder` fallback/multi-file output test coverage and this PR's saved test-model-path coverage. - Updated the upstream mock-based `ModelBuilder` tests to define the new Hugging Face test-model attributes used by this PR (`test_model_config` and `test_model_path`). - **Targeted coverage** - Added focused tests for: - CLI config generation with `--test` - input-model config serialization of `test_model_config` - input-model config serialization of `test_model_path` - validation when `--test` needs an explicit folder - `olive run --test` overriding an existing HF `input_model` from a workflow config - validation when `olive run --test` is used on a non-HF workflow config - HF random-model instantiation for multiple config naming conventions - fail-fast behavior when test-model instantiation cannot use the expected model class - conditional `trust_remote_code` handling for supported, omitted, and unsupported `from_config` signatures - saving and reusing a persisted HF test model - ModelBuilder exporting from the saved reduced test-model checkpoint when `test_model_config` is active - a CLI smoke-flow test that follows the documented `olive optimize --dry_run` then `olive run --test` commands with `hf-internal-testing/tiny-random-LlamaForCausalLM` and verifies an ONNX artifact is produced - a Qwen3 regression test that verifies reduced test-model configs keep `layer_types` aligned with the reduced hidden-layer count and can be reloaded successfully - conflict-resolved `ModelBuilder` tests covering saved test-model reuse, single-file annotation fallback, and multi-file output component naming Examples: ```bash olive optimize \ -m Qwen/Qwen3-0.6B \ --test out/test_model \ --dry_run \ -o out ``` ```bash olive run \ --config out/config.json \ --test out/test_model \ --output_path out/qwen-smoke-run ``` ## Checklist before requesting a review - [x] Add unit tests for this change. - [x] Make sure all tests can pass. - [x] Update documents if necessary. - [x] Lint and apply fixes to your code by running `lintrunner -a` - [x] Is this a user-facing change? If yes, give a description of this change to be included in the release notes. Release notes: Added a `--test` option for Hugging Face CLI workflows that can take a folder path, writes a lightweight 2-layer random-model config, saves and reuses the generated HF test model from that folder, fails fast if the reduced test model cannot be instantiated from the expected model class, only passes `trust_remote_code` when the target `from_config` supports it, and now keeps Qwen-style `layer_types` metadata aligned with reduced test-model layer counts so saved reduced configs reload cleanly. Also added `olive run --test` support for workflow configs with Hugging Face input models, updated ModelBuilder to export from the saved reduced test checkpoint for `--test` flows, and added a Qwen3 0.6B how-to page for running a quick smoke test before the full conversion with an explicit output path for the generated ONNX files. ## (Optional) Issue link --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: xadupre <22452781+xadupre@users.noreply.github.com> Co-authored-by: Xavier Dupré <xadupre@microsoft.com> Co-authored-by: Xavier Dupré <xadupre@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
1 parent 04ef7d2 commit 0c888a8

16 files changed

Lines changed: 1009 additions & 20 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# This job is run as a github action.
2+
# It checks Olive works on random and small models.
3+
name: Test model fast
4+
5+
on:
6+
workflow_dispatch:
7+
push:
8+
branches:
9+
- main
10+
pull_request:
11+
branches:
12+
- main
13+
14+
jobs:
15+
ubuntu-test-model-fast:
16+
name: Ubuntu test model fast
17+
runs-on: ubuntu-latest
18+
permissions:
19+
contents: read
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
- name: Setup Python
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.12"
27+
28+
- name: Install dependencies
29+
run: |
30+
python -m pip install -r requirements.txt
31+
python -m pip install -r test/requirements-test-cpu.txt
32+
33+
- name: pip freeze
34+
run: |
35+
python -m pip freeze
36+
37+
- name: Run fast test
38+
run: |
39+
python -m pytest -v -s -p no:warnings --disable-warnings --log-cli-level=WARNING test/cli/test_cli_test_model_smoke.py
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# How to convert a Qwen model with a quick `--test` smoke check
2+
3+
If you are converting a large language model, it is often useful to validate the Olive command, environment, and conversion recipe on a much smaller model before spending time on the full checkpoint.
4+
5+
The `--test` option does that for Hugging Face models. Olive keeps the same model architecture, reduces it to a random 2-layer test model, saves it to the folder you provide, and reuses that folder on later runs.
6+
7+
This example uses [`Qwen/Qwen3-0.6B`](https://huggingface.co/Qwen/Qwen3-0.6B), but the same pattern works for other supported Hugging Face LLMs.
8+
9+
## Step 1: generate the workflow config
10+
11+
Start by generating the config that Olive will run for the Qwen conversion.
12+
13+
```bash
14+
olive optimize \
15+
--model_name_or_path Qwen/Qwen3-0.6B \
16+
--device cpu \
17+
--provider CPUExecutionProvider \
18+
--precision int4 \
19+
--output_path out/qwen \
20+
--dry_run
21+
```
22+
23+
This creates `out/qwen/config.json` without launching the full conversion yet.
24+
25+
## Step 2: run a fast smoke test with `olive run --test`
26+
27+
Use the generated config with `olive run` and pass `--test` so Olive swaps in a reduced random Qwen model.
28+
29+
```bash
30+
olive run \
31+
--config out/qwen/config.json \
32+
--test out/qwen-test-model \
33+
--output_path out/qwen-test-run
34+
```
35+
36+
What this does:
37+
38+
- `--test out/qwen-test-model` creates a reduced random Qwen model and saves it in `out/qwen-test-model`
39+
- later runs reuse the same saved test model instead of recreating it
40+
- `--output_path out/qwen-test-run` gives the smoke test its own output folder, so the generated ONNX artifacts are easy to find
41+
- Olive marks that output folder as a test-only run and refuses to reuse a non-test conversion folder for `--test`
42+
43+
After the smoke test finishes, look under `out/qwen-test-run` for the exported ONNX model and related files.
44+
45+
This is a quick way to confirm that:
46+
47+
- Olive can load the source model
48+
- the selected optimization recipe is valid for your setup
49+
- the conversion path completes before you run the full model
50+
51+
If you omit the folder and just pass `--test`, `olive run` will save the reduced model under `<output_path>/test_model`.
52+
53+
## Step 3: run the full conversion
54+
55+
Once the smoke test succeeds, rerun the conversion on the full Qwen checkpoint by removing `--test`.
56+
57+
```bash
58+
olive run \
59+
--config out/qwen/config.json \
60+
--output_path out/qwen-full
61+
```
62+
63+
At this point you know the Olive command and the conversion recipe already worked on the lightweight test model, so you can focus on the full-model run instead of debugging both at once.
64+
65+
## Why keep the test model folder?
66+
67+
The saved test model is useful beyond the first smoke test:
68+
69+
- you can rerun the reduced conversion quickly while iterating on options
70+
- you can reuse the same HF test model later when comparing the Hugging Face model against the exported ONNX model
71+
- you avoid recreating a new random test checkpoint every time
72+
73+
## Related docs
74+
75+
- [How to use the `olive optimize` command to optimize a Pytorch model](cli-optimize)
76+
- [How to write a new workflow from scratch](../configure-workflows/build-workflow)
77+
- [CLI reference](../../reference/cli)

docs/source/how-to/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ The Olive CLI provides a set of primitives such as `quantize`, `finetune`, `onnx
1212
- [how to use the `olive finetune` command to create (Q)LoRA adapters](cli/cli-finetune)
1313
- [How to use the `olive quantize` command to quantize your model with different precisions and techniques such as AWQ](cli/cli-quantize)
1414
- [How to use the `olive run` command to execute an Olive workflow.](cli/cli-run)
15+
- [How to convert a Qwen model with a quick `--test` fast check](cli/cli-fast-test)
1516

1617
# Olive Python API
1718

@@ -43,6 +44,7 @@ The Olive CLI provides a set of primitives such as `quantize`, `finetune`, `onnx
4344
4445
installation
4546
cli/cli-optimize
47+
cli/cli-fast-test
4648
cli/cli-auto-opt
4749
cli/cli-finetune
4850
cli/cli-quantize

olive/cli/base.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,52 @@
1717
from olive.hardware.constants import DEVICE_TO_EXECUTION_PROVIDERS
1818
from olive.resource_path import OLIVE_RESOURCE_ANNOTATIONS
1919

20+
TEST_OUTPUT_MARKER_FILE = "olive_test_output.json"
21+
22+
23+
def _get_test_output_marker_path(output_path: str) -> Path:
24+
return Path(output_path) / TEST_OUTPUT_MARKER_FILE
25+
26+
27+
def is_test_output_dir(output_path: str) -> bool:
28+
marker_path = _get_test_output_marker_path(output_path)
29+
if not marker_path.is_file():
30+
return False
31+
32+
try:
33+
marker = json.loads(marker_path.read_text())
34+
except (OSError, TypeError, ValueError):
35+
return False
36+
37+
return marker.get("type") == "olive_hf_test_output"
38+
39+
40+
def validate_test_output_path(output_path: Optional[str], test_value) -> None:
41+
if test_value in (None, False) or not output_path:
42+
return
43+
44+
output_dir = Path(output_path)
45+
if not output_dir.exists():
46+
return
47+
if not output_dir.is_dir():
48+
raise ValueError(f"--output_path {output_path} must be a directory.")
49+
if any(output_dir.iterdir()) and not is_test_output_dir(output_path):
50+
raise ValueError(
51+
f"--output_path {output_path} already exists and is not marked as an Olive test output directory. "
52+
"Use a dedicated output folder for --test runs."
53+
)
54+
55+
56+
def mark_test_output_path(output_path: Optional[str]) -> None:
57+
if not output_path:
58+
return
59+
60+
output_dir = Path(output_path)
61+
if not output_dir.is_dir():
62+
return
63+
64+
_get_test_output_marker_path(output_path).write_text(json.dumps({"type": "olive_hf_test_output"}, indent=2))
65+
2066

2167
class BaseOliveCLICommand(ABC):
2268
allow_unknown_args: ClassVar[bool] = False
@@ -33,16 +79,21 @@ def _run_workflow(self):
3379

3480
from olive.workflows import run as olive_run
3581

82+
validate_test_output_path(self.args.output_path, getattr(self.args, "test", None))
3683
Path(self.args.output_path).mkdir(parents=True, exist_ok=True)
3784

3885
with tempfile.TemporaryDirectory(prefix="olive-cli-tmp-", dir=self.args.output_path) as tempdir:
3986
run_config = self._get_run_config(tempdir)
4087
if self.args.save_config_file or self.args.dry_run:
4188
self._save_config_file(run_config)
4289
if self.args.dry_run:
90+
if getattr(self.args, "test", None) not in (None, False):
91+
mark_test_output_path(self.args.output_path)
4392
print("Dry run mode enabled. Configuration file is generated but no optimization is performed.")
4493
return None
4594
workflow_output = olive_run(run_config)
95+
if getattr(self.args, "test", None) not in (None, False):
96+
mark_test_output_path(self.args.output_path)
4697
if not workflow_output.has_output_model():
4798
print("No output model produced. Please check the log for details.")
4899
else:
@@ -82,6 +133,21 @@ def run(self):
82133
raise NotImplementedError
83134

84135

136+
def add_hf_test_model_config(input_model: dict, test_value, output_path: Optional[str] = None) -> dict:
137+
if test_value in (None, False):
138+
return input_model
139+
140+
test_model_output_path = test_value
141+
# Use 2 layers to keep the test model fast and lightweight while preserving the original architecture family.
142+
input_model["test_model_config"] = {"hidden_layers": 2}
143+
if test_model_output_path is True:
144+
if not output_path:
145+
raise ValueError("--test requires an explicit folder when output_path is not available.")
146+
test_model_output_path = str(Path(output_path) / "test_model")
147+
input_model["test_model_path"] = test_model_output_path
148+
return input_model
149+
150+
85151
def _get_hf_input_model(args: Namespace, model_path: OLIVE_RESOURCE_ANNOTATIONS) -> dict:
86152
"""Get the input model config for HuggingFace model.
87153
@@ -105,7 +171,7 @@ def _get_hf_input_model(args: Namespace, model_path: OLIVE_RESOURCE_ANNOTATIONS)
105171
input_model["adapter_path"] = args.adapter_path
106172
if getattr(args, "trust_remote_code", None) is not None:
107173
input_model["load_kwargs"]["trust_remote_code"] = args.trust_remote_code
108-
return input_model
174+
return add_hf_test_model_config(input_model, getattr(args, "test", None), getattr(args, "output_path", None))
109175

110176

111177
def _get_onnx_input_model(args: Namespace, model_path: str) -> dict:
@@ -371,6 +437,16 @@ def add_input_model_options(
371437
model_group.add_argument(
372438
"--trust_remote_code", action="store_true", help="Trust remote code when loading a huggingface model."
373439
)
440+
model_group.add_argument(
441+
"--test",
442+
type=str,
443+
nargs="?",
444+
const=True,
445+
help=(
446+
"Use a randomly initialized test model with the same Hugging Face architecture and 2 hidden layers. "
447+
"Optionally provide a folder where the generated test model should be saved and reused."
448+
),
449+
)
374450

375451
if enable_hf_adapter:
376452
assert enable_hf, "enable_hf must be True when enable_hf_adapter is True."

olive/cli/run.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@
66

77
from olive.cli.base import (
88
BaseOliveCLICommand,
9+
add_hf_test_model_config,
910
add_input_model_options,
1011
add_logging_options,
1112
add_telemetry_options,
1213
get_input_model_config,
14+
mark_test_output_path,
15+
validate_test_output_path,
1316
)
1417
from olive.telemetry import action
1518

@@ -59,6 +62,14 @@ def run(self):
5962
if input_model_config := get_input_model_config(self.args, required=False):
6063
print("Replacing input model config in run config")
6164
run_config["input_model"] = input_model_config
65+
elif self.args.test not in (None, False):
66+
input_model = run_config.get("input_model")
67+
if not isinstance(input_model, dict) or input_model.get("type", "").lower() != "hfmodel":
68+
raise ValueError("--test for olive run requires a Hugging Face input_model in the run config.")
69+
output_path = (
70+
self.args.output_path or run_config.get("output_dir") or run_config.get("engine", {}).get("output_dir")
71+
)
72+
run_config["input_model"] = add_hf_test_model_config(input_model, self.args.test, output_path)
6273

6374
for arg_key, rc_key in [("output_path", "output_dir"), ("log_level", "log_severity_level")]:
6475
if (arg_value := getattr(self.args, arg_key)) is not None:
@@ -68,12 +79,16 @@ def run(self):
6879
# add value to run config directly
6980
run_config[rc_key] = arg_value
7081

82+
output_path = run_config.get("output_dir") or run_config.get("engine", {}).get("output_dir")
83+
validate_test_output_path(output_path, self.args.test)
7184
workflow_output = olive_run(
7285
run_config,
7386
list_required_packages=self.args.list_required_packages,
7487
tempdir=self.args.tempdir,
7588
package_config=self.args.package_config,
7689
)
90+
if self.args.test not in (None, False):
91+
mark_test_output_path(output_path)
7792

7893
if self.args.list_required_packages is True:
7994
print("Required packages listed!")

olive/common/hf/model_io.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def get_model_io_config(
2727
model_name: str,
2828
task: str,
2929
model: Optional["PreTrainedModel"] = None,
30+
test_model_config: Optional[dict[str, Any]] = None,
3031
**kwargs,
3132
) -> Optional[dict[str, Any]]:
3233
"""Get the input/output config for the model and task.
@@ -35,6 +36,7 @@ def get_model_io_config(
3536
model_name: The model name or path.
3637
task: The task type (e.g., "text-generation", "text-classification").
3738
model: Optional loaded model for input signature inspection.
39+
test_model_config: Optional overrides for creating a lightweight random test model from the same config.
3840
**kwargs: Additional arguments including use_cache.
3941
4042
Returns:
@@ -68,7 +70,7 @@ def get_model_io_config(
6870
return None
6971

7072
# Get model config
71-
model_config = get_model_config(model_name, **kwargs)
73+
model_config = get_model_config(model_name, test_model_config=test_model_config, **kwargs)
7274

7375
# Handle PEFT models
7476
actual_model = model
@@ -92,6 +94,7 @@ def get_model_dummy_input(
9294
model_name: str,
9395
task: str,
9496
model: Optional["PreTrainedModel"] = None,
97+
test_model_config: Optional[dict[str, Any]] = None,
9598
**kwargs,
9699
) -> Optional[dict[str, Any]]:
97100
"""Get dummy inputs for the model and task.
@@ -100,6 +103,7 @@ def get_model_dummy_input(
100103
model_name: The model name or path.
101104
task: The task type.
102105
model: Optional loaded model for input signature inspection.
106+
test_model_config: Optional overrides for creating a lightweight random test model from the same config.
103107
**kwargs: Additional arguments including use_cache, batch_size, sequence_length.
104108
105109
Returns:
@@ -133,7 +137,7 @@ def get_model_dummy_input(
133137
return None
134138

135139
# Get model config (handles MLflow paths)
136-
model_config = get_model_config(model_name, **kwargs)
140+
model_config = get_model_config(model_name, test_model_config=test_model_config, **kwargs)
137141

138142
# Handle PEFT models
139143
actual_model = model

0 commit comments

Comments
 (0)