Repository navigation
feat(dynamo): map_weights - compile from memory-mapped checkpoint weights - #4854
cehongwang wants to merge 9 commits into
Conversation
…o-pass mapping, weakref registry, inward page release, strict = all zero-copy
…releasable, page-size-independent test, clearer header errors
…d example Adds a 'Compiling from Memory-Mapped Weights' section to the resource management guide (usage, CPU compilation, checkpoints, strict, file lifetime, platform) and examples/dynamo/compile_from_mapped_weights.py: write the weights once, build the model on meta, map, export and compile on the CPU with release_weights_after_build, check against the original. The example reports the mapped file's resident pages: 0 MiB after compiling with release_weights_after_build, the whole file without it.
map_weights, save_weights_for_mapping, release_mapped_storage and MappedWeights now live in torch_tensorrt/dynamo/tools/weight_mapping.py and are exported from torch_tensorrt.dynamo.tools instead of torch_tensorrt.dynamo. Imports, tests, the API reference, the guide and the example follow.
map_weights now maps an nn.Module only: an ExportedProgram is covered by mapping the module before torch.export, which never reads the weights. Dropped key_map, casting on a dtype mismatch (a mismatch now raises), still_meta (missing names are in missing) and the MappedWeights handle (close, contains, context manager); the registration groups are a plain loop over parameters and buffers. Kept: zero-copy views on meta or real modules with ties preserved, strict, header validation, all-or-nothing changes, release_mapped_storage with inward page rounding, save_weights_for_mapping.
narendasan
left a comment
There was a problem hiding this comment.
This is quite cool but I think we need to work on the APIs a bit
| with torch.device("meta"): | ||
| model = MyModel().eval() | ||
| mapping = map_weights(model, "weights.safetensors", strict=True) |
There was a problem hiding this comment.
It would be cool if we could simplify this boilerplat
|
|
||
| example = torch.randn(1, 3, 224, 224) # CPU: the mapped weights are CPU tensors | ||
| exported = torch.export.export(model, (example,)) | ||
| trt_model = torch_tensorrt.dynamo.compile( |
There was a problem hiding this comment.
Maybe like some of the other PRs where we might want to make a weight optimized API, we can fold a lot of these things in to a default recipe
RFC (draft): an API for low-memory compilationStatus (2026-10-15): proposal, not implemented. Builds on Two changes:
The existing primitives ( Part 1:
|
…dule with a prefix save_weights_for_mapping(model, path, source=...) writes a model built on meta straight from a checkpoint: one tensor at a time, each meta tensor read from source(name, tensor), whole or in chunks along dimension 0, and cast to the model's dtype. checkpoint_source(path, key=...) reads a safetensors file in 64 MiB chunks. Materialized tensors are copied to the host one at a time too, and the file is written under a temporary name and renamed when complete. map_weights(..., prefix=...) finds each name as prefix + name in the file, as load_state_dict does, so a submodule maps straight from a checkpoint of the whole model. pi0.5 (14.5 GB float32 checkpoint to a 6.74 GiB bfloat16 file): 0.91 GiB of process memory, against 9.3 GiB for a CPU load followed by save_weights_for_mapping; the same 820 tensors.
Description
Adds a weight-mapping tool,
torch_tensorrt.dynamo.tools(py/torch_tensorrt/dynamo/tools/weight_mapping.py), so a model can be compiled from a safetensors checkpoint's weights in place, without loading them into memory first.The problem. To compile, every weight is normally loaded into host memory before the first engine is built. CPU compilation already avoids a GPU copy, since TensorRT takes weights as host pointers, but all the weights still sit in host memory for the whole compile.
The fix.
map_weights(module, path)maps the file withmmapand points each parameter and buffer at a zero-copy view of its bytes. Nothing is read until something touches a weight:torch.exportreads only shapes, so after mapping and exporting, none of the file is resident.release_weights_after_build=True(feat(dynamo): release model weights once their last engine is built #4853; this PR is stacked on it), those pages are dropped once the weight's last engine is built. The tensor stays valid and is re-read from the file if used again.API:
map_weights(module, path, *, strict=False, prefix="") -> MappedWeightsprefix, each name is looked up asprefix + name, as inload_state_dict, so a submodule maps straight from a checkpoint of the whole model. A module built onmetanever allocates its weights, and tied weights stay tied. A real tensor keeps its object and gets.data = view. Each tensor must have the model's dtype and shape, or it raises. Names missing from the file go inMappedWeights.missing;strict=Trueraises for any missingstate_dictname, but not for non-persistent buffers. On any error, the module is unchanged.release_mapped_storage(storage) -> boolmadvise(MADV_DONTNEED). Only whole pages inside the storage are dropped, so a neighbor's writes survive.save_weights_for_mapping(module, path, *, source=None)source, it converts a checkpoint without loading it: each tensor of a model built onmetacomes fromsource(name, tensor), whole or in chunks along dimension 0, and is cast to the model's dtype as it is written.checkpoint_source(path, key=lambda name: name, *, chunk_bytes=64 << 20)sourcethat reads a safetensors checkpoint underkey(name)in chunks of at mostchunk_bytes, so no whole checkpoint tensor is in memory.Change to #4853's code: 5 lines in
_weight_release.py. Ifrelease_mapped_storagerecognizes the storage, its pages are dropped instead of callingresize_(0), which fails on a view of a file.Docs:
map_weights,save_weights_for_mapping,checkpoint_source,release_mapped_storageandMappedWeights;examples/dynamo/compile_from_mapped_weights.py.Measured
pi0.5 (lerobot, bf16): 6.74 GiB of weights mapped, nothing missing.
Before compilation: host RSS 8.7 → 2.9 GiB.
The whole rolled-up ExecuTorch export (load, compile, save, with feat(dynamo): release model weights once their last engine is built #4853, feat(dynamo): engine_output_dir - keep each built engine on disk, not in memory #4863, perf(export): share the compiled module's tensors instead of deep-copying them #4864 and fix(executorch): free each saved program after save(output_format="executorch") #4865): 6.60 GiB.
Without releasing the mapped pages (no feat(dynamo): release model weights once their last engine is built #4853), the save peaks at 11.21 GiB.
Writing the mapping file from the 14.5 GB float32 checkpoint (
source=checkpoint_source(...)on ameta-built policy, cast to bf16): 0.91 GiB of process memory and 25 s, against 9.3 GiB for loading the policy on the CPU and writing it. The output is the same 820 tensors, and is byte-identical across runs. This makes the one-time conversion possible on the 8 GB Jetson itself.On a Jetson AGX Thor with the whole stack (
pytorch/TensorRT:cehongw/pi05-low-memory-repro): the export peaks at 4,983 MiB in 233 s, against 11,752 MiB and 394 s for the chain of one process per part.The example: 192 MiB mapped. After compiling, 0 MiB of the file is resident (192 MiB, the whole file, without
release_weights_after_build), and the output matches the original model within 6.4e-06.Testing
tests/py/dynamo/models/test_weight_mapping.py(14 tests):metamodule, in fp32 and bf16: every tensor points into the mapping, values match, tied weights stay tied;Parameterobjects.strict=Truewithout changing the module;safetensorspackage'ssave_file(state_dict)maps withstrict=Truewithout its non-persistent buffer;prefix.meta, written from a float32 checkpoint stored under other names, in 64-byte chunks: no chunk is a whole tensor, and the file maps back to the checkpoint's values with ties kept;metatensor withoutsourceraises.release_storagedrops a mapped weight's pages, and still resizes an ordinary storage.meta-built model, mapped, exported and compiled on the CPU withrelease_weights_after_build=True, matches eager; every released weight was a mapped one, and the weights read correctly afterwards.Mutation checks:
map_weightscopies instead of mapping, 7 tests fail.#4853's tests pass on this branch (27 together), and mypy is clean.
Type of change
Checklist: