Skip to content

Emit ORT-spec-compliant model packages from generate-model-package - #2602

Merged
jambayk merged 4 commits into
mainfrom
xiaoyu/model-package-ort-spec
Aug 5, 2026
Merged

Emit ORT-spec-compliant model packages from generate-model-package#2602
jambayk merged 4 commits into
mainfrom
xiaoyu/model-package-ort-spec

Conversation

@xiaoyu-work

Copy link
Copy Markdown
Collaborator

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 "." 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.

Describe your changes

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

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.
Copilot AI review requested due to automatic review settings August 3, 2026 23:38

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 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.json to the ORT-accepted schema (string schema_version, component map object, provenance under additional_metadata).
  • Switches per-component metadata from metadata.json to ORT-required component.json.
  • Replaces genai_config_overlay.json with per-variant, fully self-contained genai_config.json, and introduces content-addressed shared_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.

Comment thread olive/cli/model_package.py
xiaoyu-work and others added 3 commits August 3, 2026 23:49
_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
jambayk merged commit 5ccc4f0 into main Aug 5, 2026
12 checks passed
@jambayk
jambayk deleted the xiaoyu/model-package-ort-spec branch August 5, 2026 16:30
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.

3 participants