.. _l-design-loading-saving-scenarios: Complex loading and saving scenarios ==================================== The basic load/save patterns of *onnx-light* (one file, two files, parallel I/O, alignment) are covered side by side in :ref:`l-howto-load-save-onnx-files`. This page focuses on two more advanced scenarios that go beyond the basic patterns and motivated the addition of two dedicated helpers: * :func:`onnx_light.onnx.align_external_data_streaming` * :func:`onnx_light.onnx.save_model_with_shared_external_data` Both helpers share a common goal: **manipulate large two-file (or multi-file) ONNX models without paying the cost of materialising the full set of weights in RAM** — a constraint that the standard :func:`onnx_light.onnx.load` / :func:`onnx_light.onnx.save` pair cannot satisfy for very large models, and that the protobuf-free design of *onnx-light* makes natural to implement. Scenario 1 — Re-align external weights without loading them ----------------------------------------------------------- **Problem.** A model has already been saved as a ``(.onnx, .data[, .data.1, ...])`` pair, possibly several gigabytes of weights spread over multiple files. A downstream consumer (mmap-based loader, accelerator runtime) now requires each tensor offset in the weights file to be aligned to a power-of-two boundary, typically ``4096`` bytes. The host running the conversion does not have enough memory to load the full set of weights just to re-serialize them. **Naive approach.** ``load(load_external_data=True)`` followed by :meth:`SerializeToFile` with :attr:`SerializeOptions.alignment` set to the desired boundary. This works but requires the full weights payload to fit in RAM during the round-trip. **Streaming approach.** :func:`onnx_light.onnx.align_external_data_streaming` parses **only the proto metadata** of the source ``.onnx`` (using ``skip_raw_data=True``) and then streams each tensor's bytes from the source weights file(s) to a single consolidated destination weights file in chunks of ``chunk_size`` bytes, zero-padding to the requested alignment between tensors. Peak heap usage is bounded by *(metadata size + chunk_size)* — independent of the total weights size. Key properties: * Source files are read-only and unmodified. * The destination always uses a **single** consolidated weights file, even when the source spread tensors across multiple ``external_data.location`` files. * On Linux the in-kernel ``splice(2)`` syscall is used for zero-copy file-to-file transfer when the kernel supports it for the underlying file types; otherwise a regular ``read`` + ``write`` loop bounded by ``chunk_size`` is used. * The output is byte-equivalent to the in-memory variant for the tensor payloads at the same alignment — only the peak memory footprint differs. Recipe and step-by-step explanation: :ref:`l-howto-align-external-data-streaming`. Runnable benchmark vs the in-memory variant: :ref:`l-example-plot-align-external-data-streaming`. Scenario 2 — Save a variant model sharing weights with another on-disk model ---------------------------------------------------------------------------- **Problem.** A first model has been saved to disk with external data, producing ``first.onnx`` + ``first.onnx.data``. A variant of that model is then assembled in memory by mixing some of the first model's initializers (loaded **without** external data, so they still carry their original ``external_data`` metadata) with brand-new initializers that carry inline ``raw_data`` (for example a fresh head, a quantized branch, or extra adapters). The variant must be saved as ``second.onnx`` next to ``first.onnx`` **without duplicating the bytes of the reused initializers**. **Naive approach.** Calling ``onnxl.save(second, "second.onnx", location="second.onnx.data")`` materialises every initializer — including the reused ones — into ``second.onnx.data``. Storage cost is doubled and write time is multiplied by the total weight size, even though most of those bytes already exist on disk. **Shared-data approach.** :func:`onnx_light.onnx.save_model_with_shared_external_data` walks the model's initializers and: * For each initializer already marked ``EXTERNAL``, copies its ``external_data`` entry verbatim into ``second.onnx``. No byte is read from the source weights file; the ``location``, ``offset``, and ``length`` are written out unchanged. The caller is responsible for the recorded ``location`` remaining resolvable relative to ``second.onnx``'s parent directory. * For each initializer carrying inline ``raw_data``, writes the bytes into a single secondary file at ``second.onnx.data`` at aligned offsets, clears the inline bytes from the in-memory proto, and updates the proto's ``external_data`` entry to point at that secondary file. The secondary file is not created at all when every initializer is reused. Key properties: * The first model's files are read-only and unmodified. * ``second.onnx.data`` contains *only* the new weights, never the reused ones — minimising both disk usage and write time. * The function honours :attr:`SerializeOptions.alignment` for the new initializers, so the secondary file is mmap-friendly when needed. * The input model is mutated in place: new initializers no longer carry inline ``raw_data`` after the call; they reference the secondary file instead. Recipe and step-by-step explanation: :ref:`l-howto-save-model-with-shared-external-data`. Why these scenarios are first-class in *onnx-light* --------------------------------------------------- Both scenarios rely on two capabilities that are awkward to express with the standard protobuf-based ``onnx`` package and that *onnx-light* exposes by design: * **Skip raw data on parse.** ``ParseOptions::skip_raw_data`` (and the Python equivalent) lets a caller read only the proto metadata — even for multi-GB models — so the metadata-driven re-alignment of scenario 1 is possible without ever touching the weights bytes. * **Honour pre-existing ``external_data`` entries on serialize.** ``SerializeOptions::use_external_data_location`` controls whether the serializer respects per-tensor ``external_data.location`` entries that are already set on initializers. Scenario 2 builds on that to keep reused initializers pointing at the first model's weights file while the freshly added initializers are routed to a new file. The two helpers documented here package those primitives into ready-made recipes for the two most common "complex" scenarios encountered when shipping large ONNX models.