Skip to content

Streaming model loader#1642

Open
dxqb wants to merge 10 commits into
Nerogar:masterfrom
dxqb:streaming-squashed
Open

Streaming model loader#1642
dxqb wants to merge 10 commits into
Nerogar:masterfrom
dxqb:streaming-squashed

Conversation

@dxqb

@dxqb dxqb commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

New model loader capable of streaming from disk to GPU at several GiB/s.

Krea 2, disable both cache on RAM at 16 GB vram and 0.3 offloading:

Run Peak RAM Peak VRAM Reserved VRAM
Baseline 25.44 GiB 12.90 GiB 13.63 GiB
Streaming 9.72 GiB 11.36 GiB 12.32 GiB

includes several other PRs, including #1623, #1620, #1617

Test plan

  • pre-commit run --all-files passes
  • Launched the affected UI or script and exercised the change
  • Tested with at least one real preset / config when relevant (note which: Krea2)

AI assistance

  • AI-assisted — I have read every line in this diff and can defend each change

dxqb and others added 9 commits July 23, 2026 22:20
…()/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.
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>
@dxqb dxqb mentioned this pull request Jul 24, 2026
3 tasks
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant