Skip to content

Make MSIX a first class distribution model for models: block level dedupe means two apps shipping the same model cost one download and one copy on disk #911

Description

Summary

Every application that uses Foundry Local downloads and stores its own private copy of
every model it uses. For a 7B class model that is several gigabytes per application, and
nothing is shared even when the bytes are identical.

Windows already solves this problem for application payload. An MSIX package is described
by a signed AppxBlockMap.xml that hashes every file in 64 KB blocks. Single instance
storage keys on those hashes, so a block that is already on the machine is stored once no
matter how many packages contain it, and deployment does not fetch a block the machine
already has. The same mechanism covers both the download and the disk.

Model weights are close to the perfect input for it: large, immutable, and byte identical
wherever they appear.

What we are asking for: that Foundry Local support and document loading a model from a
read only directory inside the application's own MSIX package. Two applications shipping
the same model would then cost one copy on disk and one download, each application would
pin the exact model version it shipped and was tested against, and uninstalling an
application would reclaim its model as ordinary package payload.

The duplication is real, and it is byte identical

Measured across every Foundry Local cache on one machine:

Caches Files On disk Redundant Share
32 365 40.05 GB 17.55 GB 43.8%

This is not a name heuristic. Candidates were grouped by exact byte length and then every
candidate was hashed with SHA-256, which found 55 groups of identical content and 101
redundant copies. Examples:

655 MB  encoder.onnx.data      x3
439 MB  context_0_ctx_qnn.bin  x3     (and _1, _2, _3, also x3)
787 MB  context_0_ctx_qnn.bin  x2     (and _1, _2, _3, also x2)

Every one of those is the same model, downloaded and stored once per application.

Why MSIX is the right mechanism

Shipping the model as package payload changes four things at once, and no application code
is needed for any of them:

Today, per-app cache Model in the MSIX
Second app, same model, disk Second full copy Stored once, SIS
Second app, same model, download Full download again Blocks already present are not fetched
Integrity Unverified, see the note below Signed block hashes, verified by the platform
Uninstall Nothing reclaimed Reclaimed as package payload
Version the app depends on Whatever the shared state currently holds Pinned to the package, unaffected by other apps

Uninstall is listed for completeness and is covered properly in a companion issue, since it
applies to the default location regardless of how models are distributed. The rows that are
unique to packaging are the first two and the last: no other mechanism available to Foundry
Local deduplicates the download as well as the disk, and none of them lets an application
share storage without also sharing fate with whoever else is using it.

The integrity row is not hypothetical. We filed a separate issue after a model was
silently corrupted by a download that was interrupted when the machine slept: the file was
left at its full final length with the remainder reading as zeros, and nothing in the SDK
could tell. A content addressed transport cannot produce that state, because a partial
transfer does not match the block hashes and is not mistaken for a complete one.

Sharing without coupling, which a shared cache cannot offer

This is the property that makes packaging different in kind from any cache location, and
it is worth stating plainly because it is easy to miss.

Single instance storage is transparent and reference counted. Each package logically
contains its own complete copy of the model. The platform stores identical blocks once and
tracks how many packages reference them. The application does not opt in, does not
coordinate with anyone, and cannot observe the difference.

The consequences are all the ones an application actually wants:

  • The application pins the model version it shipped and was tested against. The model
    is package payload, versioned with the application, updated only when the application
    updates. Nothing can move it underneath a release that was validated against it.
  • Other applications cannot affect it. Another application installing, updating, or
    uninstalling the same model cannot remove, evict, downgrade, or lock this one. The
    platform reference counts the blocks, so the last reference is what frees them.
  • Uninstall is exact. Removing an application releases its reference and nothing more.
    There is no shared directory left behind, no ownership question about who is allowed to
    delete it, and no orphan.

Compare that with a shared cache keyed on model identity, which is the right answer during
development and the wrong one here. A shared mutable directory used by multiple shipped
applications introduces exactly the problems that packaging avoids: who is allowed to evict
an entry, what happens to application A when application B's cleanup runs, what happens
when two applications want different revisions of the same model, and what a user should do
when it is corrupt. Every one of those is a coupling between applications that were never
written with each other in mind.

MSIX gives the storage and bandwidth savings of sharing with the semantics of a private
copy. That combination is the whole argument, and it is not something Foundry Local could
reasonably build itself.

What the SDK needs to make this work

The good news is that the payload looks ready for it. We inspected a cached model
directory: it contains only immutable content, and every file was written during the
download with nothing modified since.

qwen2.5-7b-instruct-qnn-npu-2\v2\
  context_0..3_ctx_qnn.bin   787 MB each
  embeddings.onnx            325 MB
  lm_head.onnx               325 MB
  context_ctx.onnx, iterator_ctx.onnx, tokenizer.json, vocab.json,
  merges.txt, genai_config.json, tokenizer_config.json, special_tokens_map.json

The mutable pieces already live above it. foundry.modelinfo.json sits at the cache\models
root rather than inside the model folder, and logs live in a separate logs directory. So
the writable index and the immutable payload are already cleanly separated, which is the
hard part.

What we think is needed:

  1. Let a model be sourced from a read only directory. ModelCacheDir can already be
    pointed anywhere, and AppDataDir relocates everything, so the configuration surface
    largely exists. What is not established is whether a model actually loads when its
    directory cannot be written to. We have not been able to confirm this either way, and
    it is the single thing this whole request depends on. If it already works, this issue
    is mostly a documentation request.
  2. Keep the writable index separate from the payload. If anything is written into the
    model directory during load, it needs somewhere else to go, since package install
    directories are read only to the application.
  3. Document the layout. What an application must place in its package, and how to point
    the SDK at it, so that applications do not have to reverse engineer the cache layout.
  4. Let the resolver see a packaged model so IsCachedAsync and friends report it as
    present rather than trying to download it again.

Alternatives we considered and rejected

Framework packages and optional packages. Both are the textbook Windows answers for
sharing payload between applications, and both are the wrong fit here. Optional packages
must share a publisher with their main package to go through the Store, which rules out
independent vendors sharing a model. Framework packages are cross publisher, but they are
independently versioned and independently serviced, which reintroduces the problem the
application was trying to avoid: the model can move underneath an application that was
tested against a specific one. Neither is removed cleanly when the last consuming
application is uninstalled.

Shipping the model in the application's own package avoids all of that. It uninstalls with
the application, it is reference counted by the platform, and SIS still dedupes it across
unrelated publishers because dedupe operates on block hashes, not on package identity.

Applications cleaning up after themselves. Covered in the companion issue on uninstall.
Briefly: MSIX has no custom uninstall actions by design, and Windows Installer deliberately
leaves runtime created data alone, so this does not work in practice for either packaging
model.

On the objection that the app now owns model updates

Shipping the model in the package does mean the application is responsible for updating it,
and we would argue that is a benefit rather than a cost.

An application whose behaviour depends on a specific model should be able to pin that
model and update it deliberately, as part of its own dependency graph, with its own
testing and its own release. That is how every other dependency an application ships is
handled. It matters most in exactly the settings where on-device AI is most interesting,
such as clinical or regulated software, where "the model changed underneath us" is not an
acceptable answer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions