Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dac780c
Add AR_DISK_STREAM_MODEL and AR_RESUME_DIR env vars
aquilarubra Jul 19, 2026
083bb44
Add disk-streaming primitive for per-block checkpoint materialization
aquilarubra Jul 19, 2026
eaaf80c
context/model: build meta-device skeleton under AR_DISK_STREAM_MODEL
aquilarubra Jul 19, 2026
c90acd4
offload: materialize/reload blocks from meta correctly
aquilarubra Jul 19, 2026
a72847b
offload: use a deterministic offload dir when AR_RESUME_DIR is set
aquilarubra Jul 19, 2026
011874a
compressors/base: propagate model_dir to offloader; defer resume clear
aquilarubra Jul 19, 2026
97696fe
compressors/data_driven: honor disk streaming outside resumability
aquilarubra Jul 19, 2026
a863e84
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 19, 2026
933cb6b
Strip leaked internal-repo comments; fix meta-materialize dtype bug
aquilarubra Jul 22, 2026
09450c8
Add unit tests for disk_stream_util.py primitives
aquilarubra Jul 22, 2026
de38945
Re-trigger CI (unrelated HF Hub 503 flake on test_gptoss)
aquilarubra Jul 22, 2026
b60e864
Re-trigger CI (retest after flaky VRAM/RAM perf checks and llmc_forma…
Jul 27, 2026
22e0c2d
Re-trigger CI (unrelated HF Hub 503 flake on test_gptoss)
aquilarubra Jul 22, 2026
06f1a0b
Re-trigger CI (retest after flaky VRAM/RAM perf checks and llmc_forma…
Jul 27, 2026
949ff2c
Add AR_DISK_STREAM_MODEL and AR_RESUME_DIR env vars
aquilarubra Jul 19, 2026
0050b4d
Re-trigger CI (unrelated HF Hub 503 flake on test_gptoss)
aquilarubra Jul 22, 2026
11d1d45
compressors/data_driven: resumable standard tuning loop (AR_RESUME_DIR)
aquilarubra Jul 19, 2026
d9cc0fc
Add ResumeState: crash/resume checkpointing for the tuning loop
aquilarubra Jul 19, 2026
f595ce1
shard_writer: discover pre-existing shards to resume across processes
aquilarubra Jul 19, 2026
3140a5f
export: skip pack_layer() for a resumed, still-meta layer
aquilarubra Jul 19, 2026
e0ca142
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 19, 2026
d7f0e36
Strip leaked internal-repo comment markers
aquilarubra Jul 22, 2026
995afea
Add unit and integration tests for AR_RESUME_DIR
aquilarubra Jul 22, 2026
ead1eda
Re-trigger CI (retest after flaky VRAM/RAM perf checks and llmc_forma…
Jul 27, 2026
2092f07
Fix envs.py syntax corrupted during rebase conflict resolution
aquilarubra Jul 29, 2026
7434f22
auto_scheme: disk-streaming support prep (gen_auto_scheme low_cpu_mem…
aquilarubra Jul 29, 2026
b9eb98f
auto_scheme: stream per-layer sensitivity scoring block-by-block
aquilarubra Jul 29, 2026
8f14123
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 118 additions & 12 deletions auto_round/auto_scheme/delta_loss.py
Comment thread
xin3he marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -563,14 +563,20 @@ def __init__(self, message):
last_grad_input = None


def prepare_model_low_gpu(model, block_inputs: dict = None, pbar=None, major_device="cpu"):
def prepare_model_low_gpu(model, block_inputs: dict = None, pbar=None, major_device="cpu", disk_index=None):
"""Wrap every block's forward so that, for one calibration batch, it (1) moves itself to
``major_device`` on demand, (2) records its own inputs into ``block_inputs`` (on CPU) so
they can be replayed later, and (3) moves itself back to CPU once done.

Called once per calibration batch before ``model_forward_low_gpu`` runs the actual
forward+backward -- the recorded ``block_inputs`` are what let the backward pass be
replayed manually, one block at a time, without keeping every block resident on GPU.

When ``disk_index`` is set (streaming mode -- the model is a meta-device skeleton,
see ``gen_layer_config``/``disk_stream_util.py``), each block's real weights are
materialized from the checkpoint right before its own forward and released back to
meta right after, instead of assuming the block already has real CPU-resident weights
to shuffle to GPU and back.
"""
block_inputs.clear()
for n, m in model.named_modules():
Expand All @@ -590,6 +596,10 @@ def new_forward(*args, **kwargs):
"""Move the block to device, run its original forward, cache its (CPU) inputs
for later replay, then move the block back to CPU.
"""
if disk_index is not None:
from auto_round.utils.disk_stream_util import materialize_module

materialize_module(module, module_name, disk_index, device=major_device)
move_module_to_tuning_device(module, major_device=major_device)
# for n,m in module.named_modules():
# if hasattr(m, "post_init_qdqw"):
Expand All @@ -608,7 +618,12 @@ def new_forward(*args, **kwargs):
}
block_inputs[module_name] = input_info

module.to("cpu")
if disk_index is not None:
from auto_round.utils.disk_stream_util import free_module

free_module(module)
else:
module.to("cpu")
memory_monitor.update(device_list=major_device)
# clear_memory(device_list=major_device) #slow
# memory_monitor.log_summary()
Expand Down Expand Up @@ -685,14 +700,19 @@ def model_forward(model, data, **forward_kwargs):
return model(**prepared, **forward_kwargs), prepared


def model_forward_low_gpu(model, dataloader, major_device="cuda", pbar=None, scheme_tag=None):
def model_forward_low_gpu(model, dataloader, major_device="cuda", pbar=None, scheme_tag=None, disk_index=None):
"""Run one full scoring pass (all calibration batches) in low-GPU-memory mode.

For each batch: capture per-block inputs via ``prepare_model_low_gpu``, run a forward
pass whose backward is deliberately interrupted at the last block (``backward_pre_hook``
raising ``MyCustomError``), then manually replay the backward pass block-by-block
(moving each block to ``major_device`` only for its own recompute + backward, then back
to CPU) so only one block's weights need to be resident on GPU at a time.

When ``disk_index`` is set (streaming mode -- the model is a meta-device skeleton),
each block's real weights are materialized from the checkpoint right before use and
released back to meta right after, both here (the manual reverse-order backward
replay) and in ``prepare_model_low_gpu`` (the initial forward capture pass).
"""
block_inputs = {}
total_batches = len(dataloader) if hasattr(dataloader, "__len__") else None
Expand All @@ -710,7 +730,7 @@ def backward_pre_hook(module, grad_input):
raise MyCustomError("Interrupt backward pass")

for batch_idx, data in enumerate(dataloader, start=1):
prepare_model_low_gpu(model, block_inputs, major_device=major_device, pbar=pbar)
prepare_model_low_gpu(model, block_inputs, major_device=major_device, pbar=pbar, disk_index=disk_index)

# lm_head sits outside every decoder block, so it never gets `grad_mode=True`
# in the manual block-by-block backward below. Scope the fix narrowly to
Expand Down Expand Up @@ -777,6 +797,10 @@ def backward_pre_hook(module, grad_input):
for n, m in block_module.named_modules():
if hasattr(m, "grad_mode"):
m.grad_mode = True
if disk_index is not None:
from auto_round.utils.disk_stream_util import materialize_module

materialize_module(block_module, block_name, disk_index, device=major_device)
move_module_to_tuning_device(block_module, major_device=major_device)

# Set the block to eval mode while enabling gradient computation
Expand Down Expand Up @@ -816,7 +840,12 @@ def backward_pre_hook(module, grad_input):
break

del block_output, main_output, block_input_args, block_input_kwargs
block_module.to("cpu")
if disk_index is not None:
from auto_round.utils.disk_stream_util import free_module

free_module(block_module)
else:
block_module.to("cpu")

# clear_memory(device_list=major_device) # this one is very slow and seems does not affect max ram usage
memory_monitor.update()
Expand Down Expand Up @@ -856,6 +885,7 @@ def get_score_for_scheme(
force_mllm: bool = False,
model_name: Optional[str] = None,
scheme_tag: Optional[str] = None,
disk_index=None,
):
"""Wrap every quantizable layer in ``quant_layer_names`` with a scoring wrapper, run
forward(+backward, unless RTN-only) calibration over ``nsamples`` examples from
Expand Down Expand Up @@ -1078,11 +1108,20 @@ def _build_mllm_calib_dataloader():
"AutoScheme(force_mllm): cannot build mllm dataloader. "
"Provide a `processor` and a multimodal `dataset`."
)
model_forward_low_gpu(model, mllm_loader, major_device=major_device, pbar=pbar, scheme_tag=scheme_tag)
model_forward_low_gpu(
model, mllm_loader, major_device=major_device, pbar=pbar, scheme_tag=scheme_tag, disk_index=disk_index
)
else:
try:
dataloader = _build_calib_dataloader()
model_forward_low_gpu(model, dataloader, major_device=major_device, pbar=pbar, scheme_tag=scheme_tag)
model_forward_low_gpu(
model,
dataloader,
major_device=major_device,
pbar=pbar,
scheme_tag=scheme_tag,
disk_index=disk_index,
)
except Exception as exc: # noqa: BLE001
if not is_vlm:
raise
Expand All @@ -1094,7 +1133,14 @@ def _build_mllm_calib_dataloader():
batch_size = 1
if mllm_loader is None:
raise
model_forward_low_gpu(model, mllm_loader, major_device=major_device, pbar=pbar, scheme_tag=scheme_tag)
model_forward_low_gpu(
model,
mllm_loader,
major_device=major_device,
pbar=pbar,
scheme_tag=scheme_tag,
disk_index=disk_index,
)
else:
for n, m in model.named_modules():
if hasattr(m, "grad_mode"):
Expand Down Expand Up @@ -1861,6 +1907,7 @@ def _gen_layer_config(
device_list=None,
processor=None,
is_vlm: bool = False,
disk_index=None,
):
"""Score every candidate scheme in ``auto_scheme.options`` against ``quant_layer_names``
and return per-layer per-scheme losses used by the caller to pick a final bit-width
Expand All @@ -1878,8 +1925,15 @@ def _gen_layer_config(
# Create offload context for CPU RAM optimization
# Note: low_cpu_mem_usage only works when low_gpu_mem_usage is also enabled,
# because it requires layer-by-layer processing
#
# When disk_index is set, gen_layer_config already built the model as a
# meta-device skeleton and materialize_module/free_module (called directly
# around each block's use, see get_score_for_scheme/model_forward_low_gpu/
# prepare_model_low_gpu above) are the actual streaming mechanism --
# OffloadManager's hook-based approach doesn't apply to a model that never
# had real CPU-resident weights to begin with.
offload_context = None
if auto_scheme.low_cpu_mem_usage and auto_scheme.low_gpu_mem_usage:
if disk_index is None and auto_scheme.low_cpu_mem_usage and auto_scheme.low_gpu_mem_usage:
_model_dir = model_name
if _model_dir is None and hasattr(model, "config"):
_model_dir = getattr(model.config, "_name_or_path", None)
Expand Down Expand Up @@ -2225,6 +2279,13 @@ def _save_per_op_scores(index, scheme, cache_key, cache_path, per_op_scores):
and num_gpus >= 1
and len(uncached_indices) >= 2
and not need_imatrix
# Each parallel worker fully loads its own copy of the model
# (_load_scheme_worker_model) in a separate process -- incompatible
# with a meta-device streaming skeleton, whose entire point is to
# avoid ever materializing a full copy. Force serial scoring
# (which honors disk_index via materialize_module/free_module)
# instead when streaming is active.
and disk_index is None
)
if not parallel_enabled and len(uncached_indices) >= 2:
logger.info(
Expand Down Expand Up @@ -2360,7 +2421,13 @@ def _serialize_scheme(scheme):
pbar.reset(total=pbar_cnt)

if not parallel_done:
if uncached_indices:
if uncached_indices and disk_index is None:
# Skipped in streaming mode: materialize_model_ only acts on
# ReplacementModuleBase (fused-MoE) instances -- a no-op for
# dense models like ours -- but it also warns once per
# still-meta parameter/buffer, which would flood the log with
# one warning per decoder-block tensor (intentionally still
# meta, to be streamed on demand later).
from auto_round.modeling.fused_moe.replace_modules import materialize_model_

materialize_model_(model)
Expand Down Expand Up @@ -2422,6 +2489,7 @@ def _serialize_scheme(scheme):
force_mllm=force_mllm,
model_name=model_name,
scheme_tag=scheme_tag,
disk_index=disk_index,
)
memory_monitor.update()
memory_monitor.log_summary()
Expand Down Expand Up @@ -2689,12 +2757,48 @@ def gen_layer_config(
"""
model_name = None
is_vlm = False
disk_index = None
if isinstance(model, str):
model_name = model
model, tokenizer, processor, _, _, is_vlm, _ = load_model(model_name, device="cpu", use_auto_mapping=False)
is_vlm = is_mllm_model(model_name)
if not is_vlm and auto_scheme.low_cpu_mem_usage and low_gpu_mem_usage:
# Disk-streamed load (meta-device skeleton + on-demand per-block
# materialize/free, see disk_stream_util.py) instead of
# load_model()'s full-checkpoint CPU RAM load -- infeasible for a
# checkpoint bigger than available RAM. Falls back to load_model()
# for anything build_meta_model doesn't cover.
try:
from auto_round.utils.disk_stream_util import build_meta_model, materialize_non_block_params

model, tokenizer, disk_index = build_meta_model(model_name)
block_prefixes = flatten_list(get_block_names(model, quant_vision=is_vlm))
materialize_non_block_params(model, block_prefixes, disk_index, device="cpu")
except Exception as exc: # noqa: BLE001
logger.warning(
f"AutoScheme streaming load failed ({exc}); falling back to "
f"load_model() (needs the whole checkpoint resident in RAM)."
)
disk_index = None
model, tokenizer, processor, _, _, is_vlm, _ = load_model(
model_name, device="cpu", use_auto_mapping=False
)
else:
model, tokenizer, processor, _, _, is_vlm, _ = load_model(model_name, device="cpu", use_auto_mapping=False)
else:
# Object passed in: still try to detect VLM so we can pick the right dataloader later.
_, _, _, _, _, is_vlm, _ = load_model(model)
try:
_, _, _, _, _, is_vlm, _ = load_model(model)
except Exception: # noqa: BLE001
is_vlm = False
# By the time AutoRound's compressor calls into AutoScheme, ModelContext
# has already turned a string model into a real object -- meaning the
# `isinstance(model, str)` branch above never actually runs in the
# standard `AutoRound(model=path, ...)` API flow. When ModelContext
# itself built the object as a meta skeleton (AR_DISK_STREAM_MODEL=1),
# it stashes the SafetensorsIndex on the model so we can pick it up
# here instead of re-detecting streaming mode from scratch (or,
# worse, silently treating a meta model as if it were fully real).
disk_index = getattr(model, "_disk_stream_index", None)

# ---- Vision-tower scoring requires a full backward ---- #
# ``model_forward_low_gpu`` only walks the language tower (it uses
Expand Down Expand Up @@ -2789,6 +2893,7 @@ def _enable_gc(mod):
min_avg_bit_scheme=min_avg_bit_scheme,
processor=processor,
is_vlm=is_vlm,
disk_index=disk_index,
)
except torch.OutOfMemoryError:
logger.warning(
Expand Down Expand Up @@ -2820,6 +2925,7 @@ def _enable_gc(mod):
min_avg_bit_scheme=min_avg_bit_scheme,
processor=processor,
is_vlm=is_vlm,
disk_index=disk_index,
)

return res
11 changes: 8 additions & 3 deletions auto_round/auto_scheme/gen_auto_scheme.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,14 @@ def __init__(
processor=None,
):
self.auto_scheme = auto_scheme
if self.auto_scheme.low_cpu_mem_usage:
logger.info("force not using `low_cpu_mem_usage` in AutoScheme")
self.auto_scheme.low_cpu_mem_usage = False
# Upstream unconditionally forced low_cpu_mem_usage=False here
# (commit 0c9c5b1d, "reduce memory consumption... for gguf") because of an
# acknowledged bug for mixed INT4/INT8 schemes under the old OffloadManager-
# based streaming path (see an earlier commit's "low_cpu_mem_usage: bool =
# False # TODO bug for INT4 INT8 mixed bug"). We don't use that old path at
# all -- see delta_loss.py's materialize_module/free_module-based streaming,
# gated on this same flag -- so the bug that motivated disabling it doesn't
# apply here.
self.model = model
self.tokenizer = tokenizer
self.processor = processor
Expand Down
19 changes: 19 additions & 0 deletions auto_round/compressors/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,12 @@ def __init__(
# second AutoRound(...) call reuses the previous instance and silently keeps
# stale values (e.g. low_cpu_mem_usage=True from a prior run).
CompressContext.reset_context()
# When the model was built as a meta skeleton (AR_DISK_STREAM_MODEL=1),
# give the offloader the original checkpoint path so it can materialize
# each block on first touch directly from disk instead of assuming
# blocks already hold real weights (see OffloadManager._reload).
if self.model_context.disk_stream_model_dir is not None:
self._offloader.model_dir = self.model_context.disk_stream_model_dir
# Alternatively, you can use CompressContext.create_context
self.compress_context = CompressContext(
low_cpu_mem_usage,
Expand All @@ -403,6 +409,11 @@ def __init__(
static_attention_dtype=self.static_attention_dtype,
)
self.shard_writer = None
# Resumability state deferred from Orchestrator._quantize_data_driven() until
# quantize_and_save()'s save_quantized() call actually succeeds; see the
# comment in quantize() near "is_immediate_saving" for why clearing is
# deferred.
self._resume_states = None

# Flag for post_init idempotency. Set to False here so post_init() can be called
# either via quantize_and_save() (preferred, outside inference_mode) or directly
Expand Down Expand Up @@ -1625,6 +1636,14 @@ def quantize_and_save(
model, folders = self.save_quantized(output_dir, inplace=inplace, return_folders=True, **kwargs)
memory_monitor.log_summary()

# Only now -- after the full export (packing pass, config/tokenizer
# writes) has actually succeeded -- is it safe to drop the resume
# manifest. See the deferral comment in Orchestrator._quantize_data_driven().
if self._resume_states:
for rs in self._resume_states:
rs.clear()
self._resume_states = None

return model, folders


Expand Down
Loading