Emit ORT-spec-compliant model packages from generate-model-package - #2602
Merged
Conversation
The packages produced by `olive generate-model-package` could not be opened by ONNX Runtime or loaded by ORT-GenAI. Verified against ORT 1.29 and a local onnxruntime-genai build, the previous output failed at the very first step: `OrtModelPackageApi_CreateModelPackageContext` rejected the manifest with "unknown field 'configs_dir'". The directory layout was already fine. Four things were not: 1. `manifest.json` used a schema ORT rejects. ORT enforces a strict top-level whitelist, so `configs_dir` and `producer` were hard errors; `components` must be an object mapping name -> path, not an array; and `schema_version` must be a "<major>.<minor>" string, not an integer. Provenance now lives under `additional_metadata.producer`. 2. `metadata.json` is now `component.json`. When a manifest component entry points at a directory, ORT reads that fixed filename and nothing else. 3. `genai_config_overlay.json` is now a complete `genai_config.json`. ORT-GenAI loads `Config(variant_dir, "")` and explicitly refuses runtime overlays on the package path, so it has no notion of a package-level base config or an RFC 7386 merge patch. Each variant now carries a self-contained config, merged from the base, the model-level defaults, and the variant's own fields. 4. Shared config assets move from `configs/` to a content-addressed `shared_assets/sha256-<hex>/` directory, computed with ORT's `ModelPackage_ComputeDirectoryHash` algorithm. Variants reference tokenizer assets via `model.tokenizer_dir = "sha256:<hex>"`, which is one of the few fields ORT-GenAI routes through the package resolver. Processor configs are resolved as `config_path / filename` instead, so they are copied into each variant directory rather than shared. Also drops the injected `model.<role>.component` markers. The ORT-GenAI config parser has no `component` field and throws `unknown_value_error` on unknown keys, so those markers made every package unloadable. The comment justifying them cited a GenAI error string that does not exist in GenAI. Fixes a latent bug along the way: a variant with no `source_genai` used to lose the model-level scalars that were stripped from the base. Those are now restored from a `model_level_defaults` fallback layer, scoped to `_VARIANT_LEVEL_MODEL_KEYS` so per-role `filename` / `session_options` / `pipeline` never leak across roles in multi-component (VLM) packages. Verified end to end: a CPU+CUDA package generated by the CLI now opens in ORT and loads in ORT-GenAI.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates olive generate-model-package to emit ONNX Runtime model packages that conform to ORT’s strict model-package manifest/component schema and that can be loaded by both ONNX Runtime and ORT-GenAI.
Changes:
- Updates
manifest.jsonto the ORT-accepted schema (stringschema_version, component map object, provenance underadditional_metadata). - Switches per-component metadata from
metadata.jsonto ORT-requiredcomponent.json. - Replaces
genai_config_overlay.jsonwith per-variant, fully self-containedgenai_config.json, and introduces content-addressedshared_assets/sha256-<hex>/for shared tokenizer assets.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
olive/cli/model_package.py |
Reworks package writer to match ORT spec: manifest schema, component.json, per-variant full genai config, and content-addressed shared assets. |
test/cli/test_model_package.py |
Updates and expands tests to validate the new ORT-compliant layout and schema constraints. |
_write_shared_assets pointed every variant's model.tokenizer_dir at the content-addressed shared asset whenever *any* file was staged there, including packages whose source directory contributed only non-tokenizer files. Gate it on tokenizer_config.json. That is the one file onnxruntime-extensions opens unconditionally (tokenizer_jsconfig.hpp Load(): a missing tokenizer_config.json is an immediate "Failed to open a json file" error). Everything else it reads is optional or named by that config -- tokenizer.json, chat_template.jinja, chat_template.json, tokenizer_module.json, and an arbitrarily named tiktoken_file -- so no filename whitelist can be both complete and safe, and a narrower one risks stranding a real tokenizer in shared_assets with nothing pointing at it. Files are still staged either way; only the pointer is withheld, plus a warning naming what was staged. A package with no tokenizer was never loadable and still is not, but the failure now names the variant directory the tokenizer was actually expected in rather than a shared asset that never held one.
ORT-GenAI opens a single component per package and resolves every
genai_config role's `filename` against that component's selected variant
directory (`src/models/multi_modal.cpp` builds the vision / embedding /
decoder sessions from one `config_path`). A package declaring more than
one component is rejected outright:
Model package at "..." declares 3 components;
onnxruntime-genai requires exactly one.
The packager was emitting one component per role, so any multi-role model
(VLM) produced a package ORT-GenAI could not load. Decoder-only models
were unaffected — they have exactly one role and so already yielded one
component, which is why the layout looked correct until a VLM was tried.
Changes:
- `_build_variants` now emits one `VariantSpec` per source (component
`model`) carrying every role's ONNX files, with a new
`onnx_rel_paths_by_role` map so the overlay writer still knows which
in-package path belongs to which role.
- `_collect_artifacts_per_role` keeps each artifact's source-declared
relative path instead of flattening to the basename. All roles now
share one variant directory, and Mobius-style sources name every role's
graph `model.onnx` under `decoder/` / `embedding/` / `vision_encoder/`,
so basenames would collide. Preserving the declared path is inherently
collision-free (the files already coexisted in one source directory)
and means `filename` values need no rewriting at all.
- New `_resolve_ep_for_variant` picks the variant's single EP: the one
non-CPU EP across roles, CPU when there is none. Roles left on CPU are
fine because ORT-GenAI registers CPU implicitly, but two roles wanting
different non-CPU EPs now raise during packaging with an actionable
message instead of failing at load time with "Running a model with
multiple providers is not supported".
- Compatibility-string extraction probes every role's graph and takes the
first declaration, since a producer typically tags only the role that
actually targets the EP.
`VariantSpec.role_name` stays for direct `write_model_package` callers
targeting non-GenAI consumers — the plain ORT model-package spec does
allow multiple components — but the CLI no longer sets it.
Verified end-to-end against a locally built ORT-GenAI: flat and
Mobius-style (subdirectory-per-role) VLM packages both report
`components: {"model": "models/model"}` and load successfully, where the
previous per-role layout failed with the 3-component error.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dd29cf32-ff71-485d-8eb4-898b0d0aa57f
jambayk
approved these changes
Aug 5, 2026
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.
The packages produced by
olive generate-model-packagecould not be opened by ONNX Runtime or loaded by ORT-GenAI. Verified against ORT 1.29 and a local onnxruntime-genai build, the previous output failed at the very first step:OrtModelPackageApi_CreateModelPackageContextrejected the manifest with "unknown field 'configs_dir'".The directory layout was already fine. Four things were not:
manifest.jsonused a schema ORT rejects. ORT enforces a strict top-level whitelist, soconfigs_dirandproducerwere hard errors;componentsmust be an object mapping name -> path, not an array; andschema_versionmust be a "." string, not an integer. Provenance now lives underadditional_metadata.producer.metadata.jsonis nowcomponent.json. When a manifest component entry points at a directory, ORT reads that fixed filename and nothing else.genai_config_overlay.jsonis now a completegenai_config.json. ORT-GenAI loadsConfig(variant_dir, "")and explicitly refuses runtime overlays on the package path, so it has no notion of a package-level base config or an RFC 7386 merge patch. Each variant now carries a self-contained config, merged from the base, the model-level defaults, and the variant's own fields.Shared config assets move from
configs/to a content-addressedshared_assets/sha256-<hex>/directory, computed with ORT'sModelPackage_ComputeDirectoryHashalgorithm. Variants reference tokenizer assets viamodel.tokenizer_dir = "sha256:<hex>", which is one of the few fields ORT-GenAI routes through the package resolver. Processor configs are resolved asconfig_path / filenameinstead, so they are copied into each variant directory rather than shared.Also drops the injected
model.<role>.componentmarkers. The ORT-GenAI config parser has nocomponentfield and throwsunknown_value_erroron unknown keys, so those markers made every package unloadable. The comment justifying them cited a GenAI error string that does not exist in GenAI.Fixes a latent bug along the way: a variant with no
source_genaiused to lose the model-level scalars that were stripped from the base. Those are now restored from amodel_level_defaultsfallback layer, scoped to_VARIANT_LEVEL_MODEL_KEYSso per-rolefilename/session_options/pipelinenever leak across roles in multi-component (VLM) packages.Verified end to end: a CPU+CUDA package generated by the CLI now opens in ORT and loads in ORT-GenAI.
Describe your changes
Checklist before requesting a review
lintrunner -a(Optional) Issue link