Fix duplicated subgraph initializers when saving a model with external data - #32474
Conversation
…ternal data Graph::AddExternalInitializersToGraphProtoImpl recurses into subgraphs and re-adds every initializer, but it never clears the entries that Node::ToProto -> Graph::ToGraphProto already placed in the subgraph proto. Each subgraph initializer therefore appears twice in the saved model. The main graph is unaffected because ToGraphProtoWithExternalInitializers starts from an empty GraphProto, so the top level initializers are added exactly once. The stale duplicate keeps the original external data location, so loading the saved model fails: Data of TensorProto ( tensor name: Wsub ) should be stored in <input>.onnx.data, but it is not regular file. onnx.checker also rejects the model, because a graph must not declare the same initializer twice. Saving itself succeeds, so the failure only surfaces when the optimized model is loaded, which makes it easy to miss. This affects any model whose subgraph owns an initializer, regardless of whether its data is external or inline and regardless of size - a 256 byte tensor in an If branch is enough to reproduce. The sibling path ToGraphProtoWithCustomInitializerHandlingImpl already performs this clear for the same reason; mirror it here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nal data Two cases, one for a subgraph initializer that stays embedded in the .onnx file and one for a subgraph initializer that is written to the external data file. Both build an If node whose branches own their initializers, save the model with Model::SaveWithExternalInitializers, and then check that - every subgraph initializer is declared exactly once in the saved proto, and - the saved model can be loaded back. Without the fix in the previous commit both tests fail: each subgraph initializer appears twice and loading the saved model reports the stale external data location as missing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
🟡 Changes recommended
The new sparse-initializer clearing path lacks regression coverage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Fixes duplicate subgraph initializers when saving ONNX models with external data.
Changes:
- Clears dense and sparse subgraph initializers before regeneration.
- Adds embedded and externalized initializer regression tests.
File summaries
| File | Description |
|---|---|
onnxruntime/core/graph/graph.cc |
Prevents duplicate subgraph initializers. |
onnxruntime/test/framework/save_model_with_external_initializers.cc |
Adds save-and-reload regression coverage. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| // The recursive Impl call below re-adds all initializers, so we must clear to avoid duplicates. | ||
| subgraph_proto->clear_initializer(); | ||
| #if !defined(DISABLE_SPARSE_TENSORS) | ||
| subgraph_proto->clear_sparse_initializer(); |
|
Independent confirmation - I diagnosed this today from a different direction and arrived at the identical patch (same two A second way in, with a different error. Rather than Searching that string currently returns nothing, which is most of why this took a while to pin down at our end. It predates the onnx 1.21 default. The description attributes exposure to
Dropping either session-config entry makes the same graphs round-trip cleanly, which is the workaround we shipped in the interim. Verification. Built from source at Happy to see this land as-is. |
…h paths (#159) The upstream bug turned out to already have a fix in flight (microsoft/onnxruntime#32474, opened ~90 minutes before I went to file, with a character-identical graph.cc change reached from the other trigger path). Notes that, and that nothing of ours was filed beyond a corroborating comment. Also unpicks two references to a scratch tree that is being deleted now that the fix is confirmed upstream — the reproducer's shape is described in full in Run 6, so nothing is lost with the files. Claude-Session: https://claude.ai/code/session_01DZUD6pAV2iVKg4G45xzzDq Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Description
Graph::AddExternalInitializersToGraphProtoImpl()recurses into subgraphs and re-adds every initializer, but never clears the entries thatNode::ToProto()already placed in the subgraph proto. Each subgraph initializer therefore appears twice in the saved model, and the stale entry keeps the source model's data location:The saved model violates the ONNX spec and fails to load:
Fix by clearing the subgraph proto's initializers before the recursion, the same way the sibling path
ToGraphProtoWithCustomInitializerHandlingImpl()already does. The recursion re-adds every initializer (dense, string, and sparse), so the clear is lossless.Added two tests (subgraph initializer embedded vs. externalized); both fail without the fix.
Motivation and Context
Saving an optimized model with external initializers silently produces an invalid model whenever a subgraph owns an initializer, regardless of data location or size — a 256-byte tensor in an
Ifbranch reproduces it. This is easy to hit: since onnx 1.21,save_as_external_data=Trueexternalizes subgraph initializers by default. Saving succeeds and the failure only surfaces when the saved model is loaded.