feat: Add export override options to winml eval command#1141
Conversation
Bring winml eval to parity with build/perf by adding --shape-config, --input-specs, --export-config, and --dynamic-axes. These shape the ONNX export when eval builds from a HuggingFace model ID and are ignored (with a warning) for pre-built ONNX inputs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
| # Passing a dict rather than a WinMLBuildConfig avoids clobbering the | ||
| # auto-resolved export config with default fields. Mirrors winml build/perf. | ||
| build_override: Any = quant_override | ||
| if config.export_overrides: |
There was a problem hiding this comment.
Updated todo list
Here's a PR-ready comment:
Review: input option precedence
I checked the implementation against the intended precedence order:
CLI explicit > config-file explicit > CLI default > config-class default
The four new flags (--shape-config, --input-specs, --export-config, --dynamic-axes) bypass the standard collect_cli_overrides/merge_config machinery (they bind to shape_config_path/input_specs/export_config/dynamic_axes, which aren't WinMLEvaluationConfig field names) and are handled entirely by the new _apply_export_overrides.
✅ What's correct
- CLI default > config-class default — the options default to
None;_apply_export_overridesreturns early when no flag is provided, so CLI defaults never clobber anything. - config-file explicit > CLI default — the config-file
evalsection can carryshape_config/export_overrides, and they survive when the CLI omits the flags. They then flow intofrom_pretrained(config={"export": ...})→generate_hf_build_config, which routes them throughmerge_export_overrides(patchinput_tensorsby name, re-derive dynamic axes), correctly preserving the auto-generated/Optimum defaults tier. - CLI explicit > config-file explicit for
shape_config— single-value override, fine.
❌ Precedence bug: export_overrides is replaced wholesale, not merged
In _apply_export_overrides:
export_overrides = cli_utils.load_export_overrides(
export_config=export_config, input_specs=input_specs, dynamic_axes=dynamic_axes,
)
if export_overrides:
cfg.export_overrides = export_overrides # wholesale replaceload_export_overrides returns a sparse dict containing only the CLI-provided sub-keys, so assigning it discards any config-file export_overrides sub-keys the CLI didn't set.
Repro: config file sets export_overrides = {"opset_version": 17, "dynamic_axes": {...}}, then run with only --input-specs foo.json. Result: cfg.export_overrides = {"input_tensors": [...]} — the config-file opset_version and dynamic_axes are lost, even though the CLI never set them. Per the intended ordering, those config-file values should win (config-file explicit > CLI default).
This is also a parity gap with build, which deliberately avoids exactly this by calling merge_export_overrides(cfg, export_overrides) on the loaded config "instead of merge_config replacing the list wholesale".
Suggested fix — merge instead of replace:
if export_overrides:
merged = dict(cfg.export_overrides or {})
merged.update(export_overrides)
cfg.export_overrides = mergedRelated concerns (not strictly precedence)
- Config-file
input_tensorswill crash. Config-fileexport_overridesis merged as a raw dict viamerge_config(cfg, eval_data)(not throughfrom_dict/load_input_tensor_specs), so itsinput_tensorsstay plain dicts. Downstream_patch_input_tensorsaccessespatch.name/patch.dtype, which raisesAttributeErroron a dict — so config-file-suppliedinput_tensorsis effectively unusable. - Pre-built ONNX path: the warn-and-skip only covers CLI flags. If the config file carries
shape_config/export_overridesalongside a pre-built.onnx, they stay oncfg(ignored downstream, but with no warning) — minor inconsistency.
Overall the ordering is correct for shape_config and for the config-file/default tiers, but export_overrides needs field-level merging between the CLI and config-file layers to satisfy the intended precedence.
| "Ignored for pre-built ONNX inputs." | ||
| ), | ||
| ) | ||
| @cli_utils.input_specs_option( |
There was a problem hiding this comment.
An optional suggestion: as we are having more and more options, do you think they can be wrapped into a single "--additional-options" for all those optional command line inputs?
Summary
Brings
winml evalto parity withwinml buildandwinml perfby adding four export override options:--shape-config— JSON shape overrides for the auto-generated HuggingFace export config--input-specs— JSON input tensor specs merged into the HuggingFace export config (symbolic dims infer dynamic axes)--export-config— JSON ONNX export config overrides (opset version, constant folding, etc.)--dynamic-axes— JSON dynamic axes mapping for HuggingFace ONNX exportThese options shape the ONNX export that
evalgenerates when-mis a HuggingFace model ID. When-mis a pre-built.onnxfile there is no export step, so the flags are ignored and the command prints a warning.Changes
eval/config.py— addedshape_configandexport_overridesfields toWinMLEvaluationConfig, JSON-safe serialization ofInputTensorSpecvalues, andto_dict/from_dictsupport.eval/evaluate.py—_load_modelthreads{"export": ...}overrides plusshape_configintoWinMLAutoModel.from_pretrainedon the HuggingFace build path; thefrom_onnxpath is unchanged.commands/eval.py— added the four option decorators/params and an_apply_export_overrideshelper that parses the JSON and warns + skips for pre-built ONNX inputs.docs/commands/eval.md— documented the four flags and added a pitfall note.TestEvalExportOverrides(10 cases) and updated the existingfrom_pretrainedassertion for the newshape_configkwarg.Verification
uv run pytest tests/unit/commands/test_eval.py tests/unit/eval/test_eval.py→ 103 passeduv run ruff check→ clean