Simplex offloading (2x speed) - #1644
Open
dxqb wants to merge 15 commits into
Open
Conversation
…()/evict() API
Replaces the per-model `{part}_to(device)` methods across all model classes,
plus scattered call sites in dataLoader/modelSetup/modelSampler/GenericTrainer,
with generic BaseModel methods driven by the existing ModelType.model_parts()
registry: materialize(*parts), evict(*parts), and materialize_only(*parts)
(evict everything else, then materialize the given parts - the swap-in/swap-out
pattern used throughout the Samplers and text-caching setup). eval() and
adapters() are likewise made concrete on BaseModel instead of hand-written per
model. Models whose component names diverge (Wuerstchen) or that have
components outside model_parts() (SD's depth_estimator, Anima's
text_conditioner) override the relevant methods directly.
Also fixes multi-TE samplers (Flux/SD3/SDXL/HiDream/HunyuanVideo) that
previously evicted all but the first text encoder and ran encode_text with
the rest still on temp_device.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extracts the transformer / text-encoder / vae loading logic duplicated across the per-model loaders into shared helpers on HFModelLoaderMixin: - _load_transformer: the from_single_file(..., quantization_config= GGUFQuantizationConfig(...)) / else-load-from-repo pattern for loading an optionally-GGUF-quantized transformer checkpoint, previously duplicated across 9 loaders. - _load_text_encoder: thin wrapper over _load_transformers_sub_module giving every loader one call site for the load-on-demand streaming branch to hook. - _load_vae: collapses the duplicated "separate vae repo overrides the base vae subfolder" branch across 15 diffusers-path loaders. Flux's separate-transformer/pipeline path and HunyuanVideo's ckpt path use structurally different else-branches and were left untouched, as were the safetensors/pipeline-sourced vae branches. Also fixes Ideogram's VAE Override being silently ignored (model_names. vae_model was never read by the loader) and a HiDream text_encoder_4 override regression introduced while switching to _load_text_encoder (the override repo holds text_encoder_4 at its root with no subfolder). Pure refactor otherwise, no behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On CUDA, the layer-offload cache now uses one cache tensor instead of the multi-chunk split: a large cuda allocation is page-mapped, so one buffer packs with no inter-chunk tail waste, and the arena is filled per-layer from the CPU so no full resident source coexists with it. The host/pinned cache keeps the lazy multi-chunk split (its peak-doubling justification is host-only). Each layer-offload cache tensor, and each BaseModel component move, gets its own dedicated torch.cuda.MemPool, so the churny small tensors in the default pool can't wedge into a freed cache/component segment and strand it across an evict/reload cycle -- the cross-cycle fragmentation OOM on a tight budget. Pools are released once their tensors are freed. Shared MemPool helpers (create_mem_pool, mem_pool_context, supports_mem_pool) live in torch_util so both BaseModel._move_part and the offload conductor use the same wrapper. The alignment budget for the offload cache is sized from the actual offload-tensor count (TENSOR_ALIGNMENT_BYTES per tensor) instead of a fixed 4KB, since the unguarded ring wrap would otherwise silently overwrite live weights once a cache tensor holds enough tensors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Squashed history of the mempool branch on top of PR Nerogar#1620 (Arena MemPool + single-buffer offload cache / materialize-evict API): eviction handling for multi-TE samplers, materialize_only_text_encoders() helper, generic BaseModel.eval()/adapters(), per-stem LoRA pooling, and review cleanups.
…elper into BaseModelSetup
A part's 'train' flag defaults True even for parts the architecture can't train (e.g. the frozen text encoder on the newer transformer models), so taking it at face value misclassifies those parts as trained. Record the architecture-trainable parts per model type and expose part_trained_in_place() -- train and architecture-trainable and FINE_TUNE -- for the memory-management modes that must not silently discard in-place weight updates. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…load) Wires LayerOffloadConductor to materialize/evict layers directly from the checkpoint (set_disk_materialize, per-layer key_prefix) so disk-offload load-on-demand and the GPU/CPU layer split can be used together (issue Nerogar#69). A streamed sub-module is loaded as a meta skeleton; its real weights are streamed straight from the checkpoint to the compute device and quantized there on first use, so the full unquantized module never lands in system RAM. With cache_in_ram off the weights are discarded back to meta after each use and re-streamed on the next; with it on they stay resident in pinned RAM. Includes the unpublished part of the materialize()/evict() base work this depends on (the conductor.to split and the move to evicting only at save/teardown boundaries).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Discard-offload for the frozen-base (LoRA / embedding / frozen fine-tune) streaming path: when weights never change and `cache_in_ram` is on, per-layer offload stops copying host-ward. `FullModelLayerAllocator` (temp/CPU-side sibling of the ring `StaticLayerAllocator`) keeps every layer's packed weights in one permanent pinned CPU buffer for the model's lifetime. Offload is a pointer swap back into that buffer — `place()` copies nothing — and onload copies the slot into the GPU ring as before. The buffer is filled once at materialize by an explicit GPU→CPU copy, since quantized weights only exist on the GPU. Initially-loaded layers are dual-resident (GPU ring + dormant CPU slot); evict unpins but keeps the buffer and repoints the loaded layers to their slots with no clone. The mode gate is `streaming ∧ cache_in_ram ∧ simplex`, where `simplex` (computed in `enable_checkpointing`) is false only for a full fine-tune of a trained part. The hot-path offload (`__schedule_layer_to`) stays branch-free: the no-op `place()` and no-op `deallocate_layer` absorb the difference. Only `__materialize` / `__evict_to_temp` / `__evict_to_meta` carry a simplex branch. `offload_quantized`'s allocator argument is also generalized into a `place()` functor that owns the copy decision, so the ring, clone, and full-model paths share one call site (the `StaticLayerTensorAllocator.place` / `clone_tensor_allocator` / `pool_clone` refactor).
stream_module_from_checkpoint only joined its reader threads after the drain loop completed cleanly. A mid-stream failure (place() OOM, a reader error) unwound past the join, leaving up to STREAM_READER_THREADS daemon threads still executing inside safe_open().get_tensor(). When the run then tore down, force-killing a reader mid-mmap-read segfaulted on Windows (0xC0000005 inside torch_cpu, called from _safetensors_rust). Wrap the drain in try/finally: a stop Event lets readers break their stripe early, and the finally drains remaining items so a reader blocked on a full queue can post its done sentinel and exit before the unconditional join. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolves the collisions with the materialize()/evict() API (Nerogar#1617), the centralized setup_optimizations (Nerogar#1623) and the weight-compression work (Nerogar#1630) that landed upstream while this branch was open. Conflicting files are resolved to the versions this feature has been developed and tested against.
…llections stream_module_to() now returns whether any weight actually moved, so a materialize/evict that changed nothing no longer triggers a collection, and the per-epoch torch_gc() in GenericTrainer is gone for the same reason. Streamed components report their nvCOMP compression saving like resident ones do (report_compression() on the first materialize). CompressedWeightMixin tracks the measured blob length in compressed_bytes() so the offload arenas can be sized from it, and mark_needs_recompression() lets a re-materialize rebuild the blob instead of leaving an uncompressed weight flagged as compressed. Once the stream readers are joined, _drop_page_cache() releases the checkpoint shards' page cache (posix_fadvise(DONTNEED), Linux only) so it cannot grow large enough to push the host-side offload buffers into swap. Also carried in from the branch this one is stacked on: QuantizationConfig gains fallback_dtype, the dtype used for plain Linear layers the quantization layer filter excluded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Simplex mode used to be inferred from the other settings: any component that was not trained in place, streamed from disk and kept its weights cached in RAM got the full-model buffer whether or not the RAM was there to spare. Since the buffer holds the whole component rather than only its offloaded layers, that is a sizeable allocation to enter without asking, so it becomes a per-part switch (simplex_offloading, off by default). cache_in_ram is no longer part of the condition. With it off, the buffer is freed along with the weights at the evict to meta and refilled from a fresh stream on the next materialize, which works -- it just fills once per materialize instead of once for the model's life. The two combinations that cannot work are now rejected at setup with a message naming the fix: a fully fine-tuned component (the buffer is filled from the checkpoint, so in-place updates would be discarded) and stream_from_disk off (the buffer is filled from the streamed weights). A component with layer offloading switched off reads the toggle as inactive rather than an error -- there is no conductor to hand the buffer to, which makes it a stale setting rather than a wrong one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Effectively doubles the offloading speed for LoRA training. If offloading is the bottleneck it can make a big difference in overall training speed, for example Qwen on my 16 GB card with 0.5 offloading:
Baseline: 1.7 s/it
This branch: 1.2 s/it
The current offloading method transfers the model to GPU and back, but single-duplex: transfers to GPU are not parallel to transfers from GPU.
A full-duplex mode would be the obvious optimization and that works, but:
This PR implements something else instead:
It only transfers from CPU and GPU, and discards after use. This is effectively 2x the speed of current offloading, with some limitations:
streamingbranch - in the main branch you needed the RAM to store the full model during loading and sampling anyway)Simplex offloading is automatically enabled if "Cache in RAM" is enabled, offloading is enabled, and the streaming loader is enabled.
It's a small PR but builds on top of #1642 hence the large diff.
This PRs commit: 96ffea2
Test plan
pre-commit run --all-filespassesAI assistance