Skip to content

[modular] improve auto offload - #14304

Draft
yiyixuxu wants to merge 14 commits into
mainfrom
auto-offload-v2
Draft

[modular] improve auto offload#14304
yiyixuxu wants to merge 14 commits into
mainfrom
auto-offload-v2

Conversation

@yiyixuxu

Copy link
Copy Markdown
Collaborator

No description provided.

…tive add/remove

- each offloading decision checks the memory actually available on the device
  (`mem_get_info` free plus the allocator's reusable cache) and keeps a tunable
  `memory_reserve` of it free; `memory_reserve_margin` is renamed to `memory_reserve`
- if a forward still runs out of device memory, offload the smallest model on the
  device and retry, escalating one model at a time until it fits (`retry_on_oom=True`
  by default); when nothing else is resident, point at group offloading
- adding or removing a component attaches/detaches a single hook instead of
  re-running `enable_auto_cpu_offload` and offloading the resident working set
- add a `simulate_accelerator_memory` test util that makes a large accelerator behave
  like a smaller card, so offloading can be exercised with real models

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added size/L PR with diff > 200 LOC documentation Improvements or additions to documentation tests modular-pipelines labels Jul 27, 2026
@HuggingFaceDocBuilderDev

Copy link
Copy Markdown

The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update.

yiyixuxu and others added 2 commits July 28, 2026 00:17
…ments

`memory_budget` (and with it the budget/dynamic split) is not part of this PR, so
there is only one strategy behavior left to describe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`enable_auto_cpu_offload` makes its decisions silently, so there is no way to
tell a well-tuned `memory_reserve` from one that thrashes, and tests have to
monkeypatch internals to see the sequence.

Add an always-on `OffloadRecord` on the manager: every onload, offload and OOM
retry is appended as an `OffloadEvent` carrying the model, its size, the reason
(`needed_by:<model>`, `oom_retry:<model>`, `component_added`,
`offloading_disabled`), and what the strategy saw when it decided. Printing
`manager.offload_record` gives a table plus a summary (bytes moved, OOM
retries, peak co-residency); the event log is bounded and notes what it dropped.

Add opt-in `measure_activations=True`, which brackets each forward with
`reset_peak_memory_stats()` and reports `activation_peak` and
`suggested_memory_reserve` — the number `memory_reserve` is supposed to cover,
measured on the user's own hardware at their own settings, instead of a
calibration run that assumes spare memory.

`AutoOffloadStrategy.last_decision` is cleared at the top of `__call__` so an
early-returning decision cannot inherit the previous call's readings.

The two test spies that monkeypatched `UserCustomOffloadHook.offload` and
`AutoOffloadStrategy.__call__` now read the record instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@yiyixuxu
yiyixuxu marked this pull request as draft July 28, 2026 22:18
yiyi@huggingface.co and others added 6 commits July 29, 2026 00:16
Each printed row is now one decision: the model that loaded, what was
evicted to make room for it (folded from its needed_by offload events),
and the free memory the decision saw. Offloads an onload did not cause
(OOM retry, disabling offloading) keep their own rows, and column widths
follow the content. The event log and summary() are unchanged.

The doc example is replaced with real output from running Z-Image-Turbo
under auto offload on a (simulated) 20GB card - the previous hand-written
table showed sizes that contradicted the component listing above it and
an Available value on an offload row, which never has one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…measurement; cover encode/decode in the OOM retry

- format_size, a shared format_table renderer, and summarize_dict_by_value_and_parts
  (simplified; its no-common-prefix branch wrote a stale loop variable) move to
  components_manager_utils.py. Both table reprs render through format_table, the
  components table Size column uses format_size, and get_model_info drops a deepcopy
  of the attention processors. Doc tables regenerated from a real run.
- measure_activations is removed: the reserve can be deduced from torch's own peak
  counter plus the weights the record already shows, so the flag, the per-load
  peak-stat resets, and OffloadRecord.activations go away; the doc shows the
  deduction on a real Z-Image run (peak 14.15 GB - 11.6 GB resident weights ->
  ~2.5 GB decode headroom, covered by the default 3GB reserve).
- The OOM retry now wraps encode/decode too: autoencoders enter through
  apply_forward_hook, which fires pre_forward but routes around forward, so a VAE
  decode OOM was previously never retried.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ry itself

- OffloadEvent drops data nothing read: seconds, the constant reason="forward"
  on onloads, the OOM message, and memory_reserve (static config). The Reason
  column now only speaks on moves an onload did not cause (oom_retry:<model>,
  offloading_disabled); the doc points at set_verbosity_info() for watching
  moves live.
- AutoOffloadStrategy.last_decision is gone: pre_forward reads the available
  memory itself just before the eviction decision, via a shared
  available_device_memory() helper (driver-free + reusable allocator cache)
  used by both the hook and the strategy. Every onload row now records a
  reading - including the first, which previously showed "-" - and custom
  strategies get the Available column for free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An event is now only ever a move - action "onload" or "offload" - with
reason explaining why. The oom pseudo-action is gone (an OOM appears as the
eviction it caused, reason oom_retry:<model>, which summary() now counts),
and OffloadEvent drops the redundant offloaded/resident_before tuples: each
eviction is its own event whose needed_by:<onloader> reason names its cause,
and the printed table correlates them back into one row per decision. The
dropped counter and truncation notice are removed (bounded deque truncates
silently at 10k events).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The record is now just the bounded event deque plus the printed table -
summary(), the repr's footer line, and __len__ are gone. Consumers read
events directly; the tests replay peak co-residency from the moves with a
small helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- apply_forward_hook stamps its wrapper with _is_forward_entry_point, so
  wrap_forward statically discovers which methods besides forward are device
  entry points (autoencoders' encode/decode) and wraps them all at attach for
  the OOM retry - no hardcoded name list, first calls included. The retry
  loop moves to a _with_oom_retry method.
- Eviction reason renamed needed_by:<model> -> release_memory_for:<model>.
- MAX_RECORDED_OFFLOAD_EVENTS becomes OffloadRecord.MAX_EVENTS.
- pre_forward reads available memory unguarded (enable_auto_cpu_offload
  already rejects devices without mem_get_info); the cpu hook tests keep
  their fake mem_get_info alive with an autouse fixture instead.
- resident_other_hooks() inlined into its only caller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the utils label Jul 29, 2026
yiyixuxu and others added 2 commits July 29, 2026 10:20
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Checked every method against its real consumers (diffusers itself and the
Mellon ModularDiffusers nodes):

- search_components and its pattern-matching machinery (wildcards, !, |)
  are removed: nothing used them - Mellon fetches by component_id, the one
  in-repo caller was get_one's search path, which now goes through
  _lookup_ids (exact name/collection/load_id).
- get_components_by_names is removed (no callers anywhere); the zh doc's
  workflow example now builds the dict with a get_one comprehension.
- Single-caller helpers inlined: _attach_offload_hook into add(),
  _detach_offload_hook into remove(), get_ids into (the late)
  get_components_by_names.

Kept with receipts: remove_from_collection (Mellon x3), _lookup_ids
(Mellon x3 + 3 internal callers), get_components_by_ids (Mellon x8),
get_one/get_model_info/enable+disable_auto_cpu_offload (Mellon + tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
yiyixuxu and others added 3 commits July 29, 2026 21:02
…arts

- OffloadEvent recording moves to the site of each decision; offload() is
  a pure move, and every event (onload and offload) carries the free-memory
  reading taken just before its move. The repr's Available column shows the
  decision's first reading, so the table keeps its meaning.
- add()'s offload(reason="component_added") was dead code: accelerate's
  add_hook_to_module already moves the model to CPU at attach, so the call
  never moved or recorded anything. Deleted.
- One device rule: normalize_execution_device() (accelerators indexed,
  cpu not - the form tensors report) is shared by enable_auto_cpu_offload
  and CustomOffloadHook, and every resident check is now a plain ==.
  The hook defaults via get_device(); the PartialState import is gone.
- Removed unused set_strategy; _offload_retry_on_oom is None outside the
  enabled window; remove() finds the hook by component id; pre_forward got
  a docstring; the strategy docstring no longer describes hook behavior;
  stale pre-PR comment deleted; "evict" replaced by "offload" throughout.
- Tests: the four record tests that relied on cpu != cpu:0 to observe
  onloads moved to the accelerator section under simulated pressure, and
  disable's recorded final move is now asserted; the strategy-record test
  also checks the offload event's own reading.

Verified with a real Z-Image-Turbo run on a simulated 20GB card: same
3-row table, peak 14.15 GB as documented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Four focused test groups: registry (no hardware), strategy unit tests
  (cuda descriptor + scripted readings), simulate_accelerator_memory
  validation, and TestAutoOffload (real device moves), plus the pipeline
  offload mixin. House-style require_accelerate/require_accelerator
  decorators per test.
- The OOM-retry tests now recover from real torch.OutOfMemoryErrors on a
  hard-capped simulated card (weights fit, the forward's output does not;
  one eviction is exactly what makes the retry fit) instead of scripted
  fake OOMs - the fake-OOM wrapper and its choreography tests are gone.
- One patch helper (_patch_memory_stats) replaces the three reading
  fakes; _simulate_card_with_headroom sizes a simulated card relative to
  whatever the device currently holds.
- Patterns adopted from the group-offloading tests: output equivalence
  (an OOM-survived run reproduces a plain run's output bit-for-bit),
  torch.no_grad around measured phases, hooks-installed guards, backend_*
  memory helpers, and a three-tier peak-memory test (baseline > partial
  offloading on a 160MB card > fully serialized on an 80MB card).
- New coverage: enable rejects backends without mem_get_info; adding or
  removing models mid-run keeps residents in place and links hooks
  correctly; per-record asserts walk the offload record event by event.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation modular-pipelines size/L PR with diff > 200 LOC tests utils

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants