Skip to content

Expand model package with authoring tools and schema versioning#28989

Open
jambayk wants to merge 48 commits into
mainfrom
jambayk/model-package-redesign
Open

Expand model package with authoring tools and schema versioning#28989
jambayk wants to merge 48 commits into
mainfrom
jambayk/model-package-redesign

Conversation

@jambayk

@jambayk jambayk commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Description

Builds out the standalone model_package/ C library so a single library
covers the full lifecycle of an ONNX Runtime model package: inspection,
authoring, content-addressed shared assets, commit, prune, and validation.
The library remains free of any dependency on ONNX Runtime itself.

Public C API (model_package/include/model_package.h)

  • Lifecycle: ModelPackage_Open / ModelPackage_New / ModelPackage_Close,
    with ModelPackageOpenOptions controlling external-path access, symlink
    following, and strict unknown-field handling.
  • Inspection: a POD ModelPackageInfo tree (ModelPackage_Info) plus
    by-name lookups for components, variants, and per-namespace
    executor_info entries, and round-trip JSON getters that preserve
    fields unknown to the current build.
  • Path resolution: ModelPackage_ResolveStringRef implements the package's
    resolution rules — relative paths anchored at a base directory,
    sha256:<hex>[/sub/path] for shared-asset content, and portable vs
    installed confinement (absolute paths and .. segments only allowed
    under layout: "installed").
  • Shared assets: SHA-256 directory hashing, ModelPackage_AddSharedAsset
    (with reproducible-build URI check and an optional copy_in staging
    mode), ModelPackage_RemoveSharedAsset, and
    ModelPackage_ResolveAssetUri. Assets under
    <package_root>/shared_assets/ are auto-discovered at Open.
  • Authoring: inline/external component setters, variant upsert/remove,
    per-namespace executor_info setters (inline and external), and
    package-level metadata / layout / additional_metadata setters.
    Mutations invalidate cached pointers in the mutated scope and its
    descendants.
  • Commit / prune / validate: ModelPackage_Commit writes the in-memory
    model to disk either in place or to a fresh dest_root ("save as"),
    with PRESERVE or DENSE write modes; ModelPackage_Prune reclaims
    unreferenced files under shared_assets/ and tracked orphan
    variant/component directories left behind by removals; and
    ModelPackage_Validate runs schema, path-reachability, asset-rehash,
    and unknown-field checks and returns a JSON report.
  • Errors: opaque ModelPackageStatus* with a stable additive
    ModelPackageErrorCode enum (IO, schema, version, path confinement,
    asset missing, asset hash mismatch, not found, invalid arg, state).

Internal layout

The implementation is split into focused translation units:
manifest_parser, model_package_impl, authoring,
commit_prune_validate, path_resolver, asset_hasher, and an
in-tree sha256. Shared error plumbing lives in status_impl.h.

ONNX Runtime integration (onnxruntime/core/session/model_package/)

The ORT-side glue is wired onto the library through the C inspection
and path-resolution entry points. model_package_context now translates
the library's info tree into ORT-internal structs and parses the
executor_info["ort"] payload (model_file, external_data,
session_options, provider_options). When a variant declares
external_data, CreateSessionForModelPackage loads the model from a
memory mapping and passes the resolved folder to the session via
kOrtSessionOptionsModelExternalInitializersFileFolderPath, so external
initializers — including those backed by a shared asset — are picked up
at Initialize time.

The experimental OrtModelPackageApi_*_SinceV28 C entries introduced in
#28990 are unchanged.

Documentation and tests

  • model_package/README.md documents the on-disk layout, manifest and
    component schemas, shared-asset rules, path resolution, the authoring
    flow, and commit / prune / validate semantics.
  • onnxruntime/core/session/model_package/README.md documents the ORT
    consumer-side glue: the executor_info["ort"] schema, the variant
    selection algorithm, the session-creation contract, and the registered
    experimental C entries.
  • New library tests cover inspection, authoring, asset hashing, and
    commit (model_package/tests/). The ORT integration tests in
    onnxruntime/test/autoep/test_model_package.cc are reworked against
    the current C API surface.

Motivation and Context

ORT needs a single library that owns the model package format end to
end — not just reading it, but producing it, validating it, and
maintaining it on disk with content-addressed shared assets.
Consolidating this behind one C API lets ORT, publisher tooling, and
third-party consumers share the same parser, path-resolution rules, and
on-disk invariants without each reimplementing them, and keeps the
library independent of the ORT session runtime.

jambayk and others added 30 commits June 9, 2026 19:33
Adds the minimal JSON utility API (OrtJson_*) the redesign needs so consumers
(ORT CreateSession, GenAI, publisher tools, tests) can parse, navigate, build,
mutate, and serialize JSON values without bringing their own JSON dependency.

Backed by nlohmann::ordered_json so object key order is preserved across parse
and round-trip. Pointer-invalidation is scoped per the design in §11.3 of
model_package_redesign.md: a Set/Remove/Append on container X invalidates
views into X and its descendants, but not into unrelated subtrees.

Also introduces shared status plumbing used by both ModelPackage_* and
OrtJson_*:
  - ModelPackageErrorCode enum (additive)
  - ModelPackage_GetErrorCode accessor
  - Internal status_impl.h shared between translation units
  - Existing ModelPackage_* error sites updated to provide a code

Tests: 15 standalone test cases covering parse, build, round-trip, type
errors, key-order, Unicode, file parse, uint64 overflow, view-rejection on
mutation, and serialize cache stability. Built under MODEL_PACKAGE_BUILD_TESTS.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the new ModelPackage_* read API per §7.1 and §7.2 of the redesign as a
parallel implementation. Coexists with the legacy ModelPackage_CreateContext
surface for now; Phase 5 deletes the old code.

New files:
  include/model_package.h         - public C API (Open, inspection accessors,
                                    round-trip JSON getters, additional_metadata)
  src/model_package_impl.h        - in-memory representation backed by
                                    nlohmann::ordered_json for round-trip
  src/model_package_impl.cc       - C API entry points + view cache
  src/manifest_parser.{h,cc}      - parses manifest.json and external component
                                    files into the in-memory model
  src/path_resolver.{h,cc}        - portable/installed path resolution with
                                    confinement check; sha256: URI validator

Schema coverage (§5):
  - manifest.json: schema_version (must be 1), optional package_name/version/
    description/layout/additional_metadata, components map (string=external,
    object=inline), shared_assets override map
  - external component file or inline component (string ref to file or dir
    auto-appends component.json)
  - variants: optional ep/device/compatibility_string/uses_assets/
    variant_directory/executor_info/additional_metadata
  - executor_info entries: string (external file) or object (inline)
  - uses_assets entries: sha256:<64-hex> validated

Behavioral rules:
  - Portable layout rejects absolute paths and .. segments with
    ERR_PATH_CONFINEMENT.
  - Installed layout (manifest layout=installed or allow_external_paths
    option) permits both.
  - Eager check at Open: any variant with inline executor_info must have a
    resolvable variant_directory; otherwise ERR_STATE (§3 principle 2).
  - Strict mode (default) rejects unknown top-level fields at manifest/
    component/variant scope; strict_unknown_fields=false relaxes for
    round-trip of newer schemas.
  - shared_assets order: declared overrides first, then any URIs referenced
    in variant uses_assets but not declared (default convention path
    <pkg>/shared_assets/sha256-<hex>/).

Tests: 17 standalone cases covering all the above plus round-trip preservation
of unknown fields and key order.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the directory Merkle hash from design spec §4.3.1:

* Clean-room SHA-256 implementation (src/sha256.{h,cc}) verified against
  FIPS 180-4 known-answer vectors.
* Directory hash (src/asset_hasher.{h,cc}) walks the source tree, rejects
  symlinks, builds a sorted manifest of '<sha256_hex>  <relpath>\n' lines
  using POSIX-style relative paths, then hashes the manifest text.
* New public entry point ModelPackage_ComputeDirectoryHash returns the
  resulting 'sha256:<hex>' URI through a thread-local string slot.
* 13 new tests in tests/test_asset_hashing.cc covering known SHA-256
  vectors, the incremental API, reproducibility, sensitivity to name
  changes / content changes / swaps, empty subdir handling, symlink
  rejection, and walk-order independence.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the §7.3 mutation surface in a new src/authoring.cc TU:

* ModelPackage_New: builds a minimal in-memory manifest (schema_version=1,
  layout=portable, components={}) with strict_unknown_fields=true.
* ModelPackage_SetComponentInline / SetComponentExternal / RemoveComponent.
  External components materialize an empty {variants:{}} body when the file
  does not exist; the path becomes library-owned.
* ModelPackage_SetVariant / RemoveVariant. Upsert semantics. The §4.2 eager
  inline-executor-info check fires here: a variant with object-valued
  executor_info entries must have a resolvable variant_directory.
* ModelPackage_SetVariantExecutorInfoInline / SetExternal / Remove.
* ModelPackage_AddSharedAsset / RemoveSharedAsset.
  copy_in=false is eagerly rejected in portable layout; copy_in=true stages
  the source dir in pkg->pending_shared_asset_copies for materialization at
  commit time (Phase 4). expected_uri verification supported.
* ModelPackage_SetMetadata / SetLayout / SetAdditionalMetadataJson.

Plumbing:
* manifest_parser.h gains ParseComponentBody / ParseVariantBody /
  RefreshInfoView / RefreshSharedAssets / PathOptionsFor helpers so that
  authoring re-uses the same validators as Open without re-implementing them.
* DropViewCache is exposed from model_package_impl.cc and invoked after every
  mutation, honoring the §7.2 pointer-invalidation contract (entity handles
  are rebuilt on next access).
* ModelPackage gains pending_shared_asset_copies (URI → source_dir) for the
  Phase 4 commit handoff.

Tests: 24 in tests/test_authoring.cc covering each entry point, strict
field rejection, eager inline-executor-info error, upsert semantics,
metadata clear-on-empty-string, shared-asset portable rejection, expected
uri mismatch, view-cache invalidation, and round-trip via GetComponentJson.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the §7.3 / §7.4 surface in src/commit_vacuum_validate.cc:

* ModelPackage_Commit(pkg, dest_root_or_null, mode)
  In-place PRESERVE: stages pending shared-asset directories under
  shared_assets/sha256-<hex>.tmp.<rand>/, re-hashes after copy (TOCTOU
  guard), renames into the final path, then rewrites external component
  files with write-temp-then-rename, then writes manifest.json last.
  Every staged file is fsync'd before its rename; containing directories
  are fsync'd after. If the final asset directory already exists the
  staging is discarded (the content-addressed name makes this safe).
  In-place DENSE: flattens external components into the manifest before
  writing; rejects external executor_info entries with ERR_STATE since
  the model never loads them in memory.
  dest_root ("save as"): requires empty/nonexistent target, copies all
  shared assets (including overrides) into the dest_root with the
  default convention, drops manifest.shared_assets overrides for a
  self-contained result, enforces portable confinement on component
  paths, then re-parses the freshly written package and swaps the
  result into *pkg so subsequent in-place commits go to the new root.

* ModelPackage_Vacuum: walks <pkg_root>/shared_assets/, reclaiming
  sha256-<hex>/ dirs not referenced by the manifest and *.tmp.* staging
  dirs left behind by crashes. Both gated by a 60s grace threshold to
  avoid stomping in-flight commits. Orphan component dirs are out of
  scope until a convention dir is defined (spec §7.4 future work).

* ModelPackage_Validate(flags, out_report_json) with flags
  SCHEMA | PATHS | ASSET_REACH | ASSET_REHASH | UNKNOWN_FIELDS | ALL.
  Re-parses every component+variant body with strict=true (SCHEMA),
  checks external-file existence (PATHS, warnings), enforces every
  uses_assets URI resolves to a real directory (ASSET_REACH, errors),
  re-hashes each shared asset and compares against its URI
  (ASSET_REHASH), and warns on unknown manifest fields. Returns a JSON
  report cached on pkg->last_validate_report.

Tests: 16 in tests/test_commit.cc covering both modes, dest_root save-as
+ re-parse swap, vacuum grace handling (skip recent, reclaim old
orphans and stale staging), and validate flags for clean / unknown-uri
/ missing-external / mutation-detected cases. Plus a sanity test that
no .tmp.* file remains under <pkg_root> after a successful commit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The ORT-side ModelPackageContext constructor now calls the standalone
library's public C API (ModelPackage_Open + walk via ModelComponent_*/
ModelVariant_*) instead of reaching into the legacy internal types
(model_package::ParsePackage, model_package_internal.h). The ORT-internal
C++ types (VariantInfo, ComponentInfo, ModelPackageInfo) are populated
from the C handles; the rest of ORT's surface is unchanged.

The 'ort' executor_info namespace (§5.3) is parsed in ORT, not the library:
model_file (relative to variant_directory), session_options, provider_options,
external_data (path or sha256: URI, resolved via ModelPackage_ResolveAssetUri).
variant additional_metadata feeds consumer_metadata.

Legacy library code removed: src/api.cc, src/parser.{h,cc},
src/model_package_internal.h. model_package_api.h trimmed to the shared
core types (export macro, ModelPackageStatus opaque, ModelPackageErrorCode).
The status helpers ModelPackage_GetErrorMessage/GetErrorCode/ReleaseStatus
are gone; only the new ModelPackageStatus_Message/Code/Release survive.

cmake/onnxruntime_session.cmake: drop now-unused include of
model_package/src/ and refresh the integration comment. ORT only needs
the public include/ directory.

All 85 standalone library tests still pass; onnxruntime_session links clean.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the opaque ModelComponent / ModelVariant handles and their query
getters with flat POD structs (ModelComponentInfo, ModelVariantInfo,
ModelExecutorInfoEntry, ModelSharedAssetInfo) reached by walking the tree
returned from ModelPackage_Info(). The package owns a lazily built view
cache that is dropped on any mutation; helper functions
ModelPackage_FindComponent / ModelComponentInfo_FindVariant /
ModelVariantInfo_FindExecutorInfo provide ergonomic by-name lookup.

Update the ORT consumer (model_package_context.cc) to walk the new tree
and add a variant.json fallback: if the manifest's variant entry has no
executor_info["ort"], we now read "<variant_directory>/variant.json" if
present so callers can author manifests without inline ORT config.

Rename the internal namespace model_package_v2 back to model_package now
that the original code path is gone, and strip references to the design
doc / phase numbers / 'legacy' / 'v2' from comments throughout the
library and its tests.

Standalone library: all five test binaries (ort_json, inspection,
asset_hashing, authoring, commit) build and pass on CPU.

ORT: libonnxruntime_session.a builds cleanly against the new API.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Inline executor_info is an executor-specific contract; the library has no
business asserting that a variant_directory must exist on disk at parse or
SetVariant time. Executors resolve their own file references (shared assets,
relative paths) at load time and will produce their own errors when files
are missing. Forcing the check here added authoring friction (could not
build a complete package in memory and commit once) for no library-level
guarantee.

Removed the parse-time error path and the corresponding test; updated the
ModelPackage_SetVariant doc to match.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
variant_directory:
- Existence is now required if and only if the field is explicitly declared
  in the variant body. The inferred default (variant_name under component_dir)
  remains allowed to be missing, with no eager check.
- This catches "you declared a path, but the directory is not there" while
  keeping the library out of executor-specific payload validation.
- Updated test_validate_asset_reach_flags_unknown_uri to mkdir the declared
  variant_directory ahead of SetVariant.

Vacuum -> Prune:
- More idiomatic verb (matches git/docker/npm). Renamed the public API
  (ModelPackage_Prune), the implementation file, the header section, and
  the standalone tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prune previously only swept unreferenced shared_assets/ entries.
Extend it to also clean up directories that the library itself
removed from the live tree via RemoveVariant, RemoveComponent,
SetVariant (replace), or SetComponentExternal (re-point), so users
don't have to manage on-disk cleanup after authoring edits.

The library never walks package_root looking for unknown content.
Instead, each mutation that drops a directory pushes the prior
resolved path onto an explicit pending list on the ModelPackage,
and Prune sweeps that list with four guards: inside package_root,
still exists, not currently referenced (or an ancestor of any
currently live dir), and past the existing prune grace window.
Components are swept before variants so a single component_dir
removal reclaims its child variant dirs in one call.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ap load

When the selected variant declares external_data as a shared asset, route the
resolved folder through kOrtSessionOptionsModelExternalInitializersFileFolderPath
so ORT can locate the data file even when it does not live next to model.onnx.

That config key is only honored by the buffer overload of Session::Load
(model_location_ is set on path-load and shortcuts the hint). To preserve
mmap-style behavior, gate on external_data presence: when set, clone the
session options, add the folder hint, mmap the .onnx file via Env::Default and
hand the buffer to CreateSessionAndLoadSingleModelImpl, then release the mmap.
Otherwise keep today's path-load behavior so non-external models are unchanged.

Add ModelPackageComponentContext::GetSelectedVariantExternalDataFolder accessor
(cached) that surfaces shared_files["external_data"] populated by the
manifest resolution step.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Trim verbose multi-line comments across recent edits in commit_prune_validate.cc,
manifest_parser.cc, and model_package_impl.h. Update the Prune docstring in
the public header to reflect that it now also reclaims tracked orphan variant
and component directories alongside shared_assets.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…a_folder_path

The shared_files map on VariantModelInfo was only ever populated with a single
'external_data' entry and read back in one place. Replace it with an explicit
optional<string> external_data_folder_path field on the struct, which makes the
data flow obvious and removes the redundant caching in
ModelPackageComponentContext.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…or_info paths through it

ORT-side parsing of executor_info.ort.model_file and external_data was joining
paths against the variant folder by hand, skipping the portable/installed
confinement and '..' rejection that the rest of the library enforces, and only
external_data understood sha256: URIs (and only as a bare folder).

Add a single library primitive that handles every accepted form of a string
reference inside a model package:
  - bare 'sha256:<hex>'              -> shared-asset folder
  - 'sha256:<hex>/sub/path'          -> file or subdir inside an asset folder
                                        (tail resolved with portable confinement
                                        under the asset folder)
  - relative path                    -> joined against base_dir (or package_root
                                        when base_dir is null) under package
                                        portable/installed semantics
  - absolute path / '..' segments    -> only allowed in installed layout

Switch the ORT model_package context to call ModelPackage_ResolveStringRef for
both model_file and external_data so they now uniformly accept all of the
above and inherit the same confinement rules. Errors surface the underlying
status message.

Add the helper TrySplitAssetUriPrefix on path_resolver to detect the
'sha256:<hex>[/<tail>]' form.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…s==nullptr

When the caller passes a null OrtSessionOptions and the selected variant
does not declare external_data, options_to_use stayed null through to the
RebuildProviderListForSession and LogTelemetry calls that dereference it.
Synthesize a default OrtSessionOptions in that branch so options_to_use is
always non-null, and drop the now-redundant null guard in the policy
telemetry branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The library is the single source of truth for variant configuration via
manifest.json + executor_info. The variant.json shorthand was an ORT-side
legacy convention that read the file with raw std::ifstream, bypassing
ModelPackage path resolution and producing two divergent code paths for
the same logical config. Remove it; callers must declare executor_info
in the manifest (inline or as an external file).

Also fixes a stale error message referring to a variant.json descriptor.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
GetVariantEpCompatibility returns a Status that was being swallowed; an
unknown component or variant returned a null ep with a success status.
Forward the status as an OrtStatus when not OK so callers can distinguish
"variant has no ep declared" from "component/variant not found".

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Previously the view-cache build path (a const function) silently loaded
and parsed external executor_info files, swallowing any I/O or schema
errors and producing an empty body instead. Move the resolution into a
new non-const RefreshExecutorInfoCache called once at Open (strict) and
after every PostMutate (lenient: allows authoring SetExecutorInfoExternal
to record a path before the file exists). The view cache now just maps
pre-resolved strings into ABI structs with no I/O.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…DirName helpers

Replace three hand-rolled 'sha256-' + hex concatenations and one
'rfind("sha256-", 0)' parse with named helpers in path_resolver. Keeps
the on-disk naming convention in one place.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…fo / layout mutations

MutateExecutorInfo and SetLayout cannot change uses_assets references
nor shared_assets entries, so the shared-asset rescan is wasted work.
Falls in line with SetMetadata and SetAdditionalMetadata which already
passed refresh_assets=false.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Paths recorded onto pending_orphan_{variant,component}_dirs by Record*
calls were orphaned by an in-session mutation: there is no concurrent
writer to protect against, so making the user wait kPruneGrace before
the next Prune actually removes them is just confusing. The grace
window is still applied to the shared_assets sweep, which discovers
candidates fresh from disk.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ses_assets

AddSharedAsset(copy_in=true) without any uses_assets reference produces
a pending copy that has nothing referencing it. Previously commit would
silently materialize it; now both the in-place and dest_root commits
fail with ERR_STATE so the author notices the missing reference instead
of shipping an orphan asset. Existing tests updated to add the
reference; new negative test covers both commit paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The dest_root commit path already rehashes the staged copy via
ComputeDirectoryAssetUri and rejects mismatches (commit_prune_validate
~line 420). Add an explicit test that tampers with a landed sha256-<hex>/
directory between in-place commit and dest_root commit, confirming the
mismatch is caught so the source-trust behavior cannot regress silently.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
No consumer (ORT internals or integration tests) ever called OrtJson_*,
so the opaque-handle DOM was only paying its cost in header bloat,
build time, and review friction. Delete the public header, the
implementation, and its dedicated unit test; clean up the doc strings
that referenced it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ptions

The captured OrtSessionOptions from CreateModelPackageOptionsFromSessionOptions
only drives variant selection and EP discovery. The default-path session
is built from a fresh OrtSessionOptions plus variant-metadata merge,
NOT from the captured options. Update the doc to match.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
'ns' clashed with a stdlib convention (std::ns aliases are common) and
the field meaning was unclear at the call site. Rename to namespace_key
across the public C header, the impl, and the FindExecutorInfo parameter.
No internal consumers (ORT, integration tests) referenced .ns directly,
so the rename is contained.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous doc said 'remains valid until the asset is removed or the
package is closed', but the pointer comes from either a vector-owned
string_cache or an unordered_map key — both of which can be invalidated
by the next mutation (PostMutate rebuilds shared_assets; the
pending_shared_asset_copies map can rehash). Narrow the contract to
'until next mutation' so callers copy when they need to keep the value.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The old README documented an API surface that no longer exists
(ModelPackage_CreateContext, ModelPackage_GetComponentCount, ...) and
a directory layout from an earlier design (metadata.json + variant.json
files per variant). Replace with a description of the current
Open/Author/Commit + read-tree surface, the lifetime contract, the
opaque-to-us boundaries (variant selection, executor_info payloads),
and an accurate on-disk layout.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The dest_root commit path unconditionally ran fs::create_directories on
<dest_root>/shared_assets/ before figuring out whether any assets were
actually going in it. Packages that use no shared assets at all (e.g. a
GenAI-style component that lays out its own files inside variant_directory)
ended up with a stray empty folder on disk.

Open / Load / Validate / Prune already tolerate the folder being absent,
and the in-place commit path is already gated by pending_shared_asset_copies
being non-empty. Gate the dest_root mkdir the same way.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Rewrite model_package/README.md as a full reference for the package
  format: on-disk layout, portable vs installed, manifest/component/
  variant schemas with field tables, shared-asset hashing (file names +
  contents), path resolution rules, C API tour, commit/prune/validate.
- Add onnxruntime/core/session/model_package/README.md documenting ORT's
  consumer side: executor_info["ort"] schema (model_file, external_data,
  session_options, provider_options) with inline+external forms, variant
  selection algorithm (EP intent capture + ValidateCompiledModelCompat
  scoring), CreateSession session-options precedence, and C/Python API
  examples.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
jambayk and others added 4 commits June 10, 2026 19:59
The Ort::ModelPackageContext / Ort::ModelPackageOptions C++ RAII
wrappers were removed when the model package surface moved onto the
experimental C API. Drop the two CxxWrappers_* tests that still
referenced them; their coverage is fully replicated by the existing
function-table tests in the same file.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
unistd.h and fcntl.h were included unconditionally in commit_prune_validate.cc
even though the POSIX symbols they define (open, fsync, close, O_RDONLY,
O_DIRECTORY, errno) are only used inside FsyncPath's non-Windows branch.
Wrap the includes in #ifndef _WIN32 so MSVC stops failing with C1083.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
RandomSuffix() was casting a uint64_t to unsigned long and printing
with %016lx. On Windows (LLP64) unsigned long is 32-bit, so the cast
silently truncated half the entropy. Switch to unsigned long long
and %016llx so the full 64-bit value is preserved on every platform.

Apply the same fix to the matching helpers in the standalone test
suites (not in the ORT Windows CI path today, but kept consistent
for the time the tests are enabled).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-redesign

# Conflicts:
#	include/onnxruntime/core/session/onnxruntime_experimental_c_api.inc
#	onnxruntime/test/autoep/test_model_package.cc
@jambayk jambayk changed the title Model package: standalone library and experimental ORT C API Model package: standalone library and ORT redesign Jun 11, 2026
@jambayk jambayk changed the title Model package: standalone library and ORT redesign Model package: add standalone authoring library and schema improvements Jun 11, 2026
@jambayk jambayk changed the title Model package: add standalone authoring library and schema improvements Model package: extract authoring into a standalone library Jun 11, 2026
@jambayk jambayk changed the title Model package: extract authoring into a standalone library Add standalone model package library and wire ORT package APIs through it Jun 11, 2026
@jambayk jambayk changed the title Add standalone model package library and wire ORT package APIs through it Expand the model_package library with authoring, commit, prune, and validate Jun 11, 2026
@jambayk jambayk changed the title Expand the model_package library with authoring, commit, prune, and validate Expand model package with authoring tools and schema versioning Jun 11, 2026
@jambayk jambayk requested review from chilo-ms and Copilot June 18, 2026 17:35

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 expands the standalone model_package/ library from read-only inspection into a full authoring + validation + commit workflow (with content-addressed shared assets and schema versioning), and updates ONNX Runtime’s model-package integration to consume the library via its public C API (including wiring external_data into session creation).

Changes:

  • Replaces the old internal-only model package parser with a new public model_package.h C API and in-library implementation split across focused translation units (parsing, path resolution, hashing, authoring, commit/prune/validate).
  • Updates ORT’s model package integration to open/inspect packages via the library C API and adds support for external_data by switching to buffer-load + setting the external-initializers folder hint.
  • Adds standalone library tests (inspection/authoring/asset hashing/commit) and new integration documentation.

Reviewed changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
onnxruntime/core/session/utils.cc Adjusts model-package session creation to support external initializers via buffer-load + config entry.
onnxruntime/core/session/model_package/README.md New documentation for ORT-side consumption, selection, and experimental API usage.
onnxruntime/core/session/model_package/model_package_context.h Extends ORT’s internal variant model info with resolved external initializer folder path.
onnxruntime/core/session/model_package/model_package_context.cc Switches ORT integration to the model_package public C API; parses executor_info["ort"]; resolves model/external_data refs.
onnxruntime/core/session/model_package_api.cc Tightens error/out-param behavior for experimental model package API getters.
model_package/tests/test_inspection.cc New standalone inspection API tests.
model_package/tests/test_commit.cc New commit/prune/validate tests (in-place + dest_root flows).
model_package/tests/test_authoring.cc New authoring/mutation API tests.
model_package/tests/test_asset_hashing.cc New SHA-256 + directory Merkle-hash tests for shared assets.
model_package/src/status_impl.h New internal status representation shared across the library.
model_package/src/sha256.h New minimal SHA-256 interface for content-addressed assets.
model_package/src/sha256.cc New SHA-256 implementation (used for directory hashing and asset validation).
model_package/src/path_resolver.h New path resolution + confinement helpers and sha256 URI helpers.
model_package/src/path_resolver.cc Implements portable/installed path rules and sha256 URI parsing.
model_package/src/model_package_impl.h Defines the in-memory package representation and cached Info view materialization.
model_package/src/model_package_impl.cc Implements lifecycle, info tree, lookups, string ref resolution, and hashing API.
model_package/src/manifest_parser.h Declares manifest/component/variant parsing + cache refresh helpers.
model_package/src/manifest_parser.cc Implements manifest/component/variant parsing into the in-memory model.
model_package/src/authoring.cc Implements mutation APIs (set/remove components/variants/executor_info, shared assets, metadata).
model_package/src/asset_hasher.h Declares directory Merkle-hash algorithm for canonical asset URIs.
model_package/src/asset_hasher.cc Implements directory hashing rules (sorted manifest text, reject symlinks, posix paths).
model_package/src/commit_prune_validate.cc Implements commit (“preserve”/“dense”), prune, and validate workflows.
model_package/src/parser.h Deletes old internal parser header (superseded by new library).
model_package/src/parser.cc Deletes old internal parser implementation (superseded by new library).
model_package/src/model_package_internal.h Deletes old internal type definitions (replaced by new in-memory model).
model_package/src/api.cc Deletes old standalone API implementation (replaced by new C API surface).
model_package/README.md Major documentation expansion for layout/schema/assets/path resolution/commit-prune-validate.
model_package/include/model_package.h New public API header defining lifecycle/inspection/authoring/commit/prune/validate.
model_package/include/model_package_api.h Refactors shared types (export macro, status handle, stable error enum).
model_package/CMakeLists.txt Updates sources/installs new headers and adds standalone tests wiring.
cmake/onnxruntime_session.cmake Updates ORT build integration to include only the model_package public headers and use the public C API.

Comment thread model_package/src/path_resolver.cc
Comment thread model_package/src/path_resolver.cc Outdated
Comment thread model_package/src/commit_prune_validate.cc
Comment thread model_package/tests/test_commit.cc
Comment thread onnxruntime/core/session/model_package/model_package_context.cc
effective.allow_external_paths = false;
effective.follow_symlinks = true;
effective.strict_unknown_fields = true;
if (opts) {

@chilo-ms chilo-ms Jun 19, 2026

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.

abi_version  is stored in every struct but never validated. If a caller compiled against a future v2 header passes abi_version = 2 (where field semantics or layout may have changed), this code silently proceeds with v1 interpretation, this might cause issue.

Should we gate on abi_version before using the struct?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The structs no longer carry an abi_version (or struct_size) field. The model package library is consumed as source — each consumer compiles it into its own binary — so there is no separately-built binary boundary to version, and nothing to gate at runtime. On-disk compatibility is governed solely by schema_version ("major.minor"): the library accepts any package whose major is within its supported range, and consumers branch on schema_version_major / schema_version_minor to know which optional fields a package may carry. See the README "Schema versioning and source distribution" section.

effective.allow_external_paths = false;
effective.follow_symlinks = true;
effective.strict_unknown_fields = true;
if (opts) {

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.

abi_version  is documented as 1  in every struct's doc comment, but there's no named constant for it. Callers have to hardcode the literal 1 , and the library has no single source of truth to validate against.

Consider adding a define:

#define MODEL_PACKAGE_ABI_VERSION 1

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

abi_version has been removed entirely rather than turned into a named constant. Since the library ships as source with no binary boundary, an ABI-version macro would be vestigial (the same reasoning that removed struct_size). The only compatibility contract that remains is schema_version (major.minor), which is what consumers key off of.

}

const ModelVariantInfo* ModelComponentInfo_FindVariant(const ModelComponentInfo* comp,
const char* name) {

@chilo-ms chilo-ms Jun 19, 2026

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.

Same concern here.

It should check abi_version of the ModelComponentInfo struct first, otherwise, consider the scenario where library/api is newer and consumer/caller is older, as per implementation of this function, it might return corrupted data.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This accessor no longer exists — collections are plain array members again (components / num_components, etc.), and there is no abi_version or struct_size. With source distribution there is a single struct definition per build, so the newer-library / older-caller layout skew that motivated a check cannot arise. A breaking change to the on-disk data contract is instead carried by a schema_version major bump, which the parser gates on.

jambayk and others added 7 commits June 23, 2026 19:07
ModelPackageContext keeps the model_package handle open for its lifetime and
exposes ResolveStringRef, which forwards to the model_package library's
resolver. It handles sha256: content-addressed shared-asset references
(honoring manifest overrides) and plain relative paths resolved against a base
directory, with the resolved path cached for C-API pointer lifetime.

The experimental C API gains OrtModelPackageApi_ModelPackage_ResolveStringRef so
consumers can resolve package path references without reopening the package.
autoep tests cover sha256 directory and tail resolution, relative-path
resolution, and rejection of an undeclared asset.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ResolvePath runs the portable-layout confinement check whether or not the
  leaf exists (when a package_root is set), and uses weakly_canonical for a
  missing leaf so symlinks in the existing prefix are resolved. This closes a
  gap where a not-yet-created path could escape package_root through a symlinked
  prefix. The check is skipped when package_root is empty (in-memory authoring
  before the package is anchored to a directory).
- CheckPortableConfinement only rejects an absolute candidate whose first
  relative component is '..', instead of any dot-prefixed name, so in-root
  hidden paths like '.hidden/component.json' are no longer wrongly rejected.
- Add MODEL_PACKAGE_ABI_VERSION as the single source of truth for the struct
  abi_version fields and use it at every assignment site.
- test_commit includes <sstream> directly for std::ostringstream.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…-redesign

# Conflicts:
#	onnxruntime/test/autoep/test_model_package.cc
…s via accessors

The on-disk schema_version is a "<major>.<minor>" string. The library accepts any
package whose major is in its supported range and any minor; evolution within a major
is additive, so a single parser reads every minor and tolerates unknown fields from a
newer minor. The schema version is validated up front, before component parsing.
ModelPackageInfo exposes schema_version_major and schema_version_minor.

The read API no longer exposes collections as raw arrays. Components, variants, shared
assets, and executor infos are reached through count + index accessors, so the library
owns the element stride and can append fields to the element structs without breaking a
compiled consumer. Each element with children is stored as a view whose first member is
the public struct, so an accessor recovers the children from the public pointer.

Struct compatibility rests on struct_size plus an append-only layout enforced by
static_asserts on field offsets; breaking changes are carried by the library SOVERSION.
The per-struct abi_version field is removed. ModelPackage_Open validates a minimum
options struct_size before reading caller fields.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The model package library is compiled into each consumer's own binary, so its
POD structs have no binary boundary. Drop the struct_size/SOVERSION/static_assert
ABI machinery and the count+index accessors, and read collections directly via
array members (components/num_components, variants/num_variants, executor_infos,
shared_assets). Compatibility is governed solely by schema_version (major.minor).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a "Versioning and compatibility" section to the model package README
covering source distribution (no published shared library, hence no ABI
machinery), the major.minor schema_version contract, what the parser enforces
(unsupported major rejected, newer minor tolerated), and how the supported
major range lets a breaking format change land without invalidating already
published packages or forcing consumers to upgrade in lockstep.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a README subsection clarifying that the structs carry a single definition per
build: an old-major package exists only as on-disk JSON, so reconciling it is a
parse-time job. Document the superset/newest struct shape, parse-time
normalization of older majors, nullable fields plus schema_version_major
branching for non-migratable changes, and per-major typed structs as the escape
hatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

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

Copilot reviewed 33 out of 33 changed files in this pull request and generated 5 comments.

Comment thread onnxruntime/core/session/model_package/model_package_context.cc
Comment thread model_package/src/path_resolver.cc
Comment thread model_package/include/model_package.h
Comment thread model_package/include/model_package.h Outdated
Comment thread model_package/tests/test_asset_hashing.cc Outdated
Reject drive-rooted paths (e.g. Windows "C:rel") alongside absolute paths in
portable layout, since has_root_name() paths are not is_absolute() but still
escape confinement. Error in GetSelectedVariantFilePath when the selected
variant's ort entry has no model_file, instead of returning an empty path.
Note allow_external_paths in the ResolveStringRef doc, align the Prune doc with
its actual behavior (it never removes content-addressed shared-asset dirs), and
clean up an exploratory comment in the hashing test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jambayk jambayk marked this pull request as ready for review June 24, 2026 00:29
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.

4 participants