Files
Model-Optimizer/tests/gpu/torch/quantization/plugins/test_accelerate_gpu.py
T
realAsma 2d868d3f1f Performant layerwise calibration for large models (#1251)
## Summary

Adds **performant layerwise calibration** for quantizing large models
(e.g. DeepSeek-R1 671B) that don't fit entirely on GPU. ([Example
commands](#example-commands))

1. **Performant calibration for large models** — Each decoder layer is
moved from CPU/disk to GPU (accelerate) or unsharded (FSDP2) **only
once** and kept on GPU for the entire calibration step. Previously,
every calibration batch triggered weight transfer for every layer —
O(num_batches) weight movements per layer. Now it is O(1) per layer.
This also means you can **increase batch size** since only one layer's
weights occupy GPU at a time — e.g. DeepSeek-R1 on a single node
(8×80GB) with `batch_size=16` and `gpu_max_mem_percentage=0.5`.
2. **Checkpoint save/resume** — Saves progress after each layer, so jobs
that exceed cluster time limits (e.g. 4-hour Slurm windows for 100+
layer MoE models) can resume from the last completed layer.
3. **Rename** `sequential_calibrate` → `layerwise_calibrate` for
clarity.

### Design details

The existing layerwise state machine (skip/run/capture) already
processes one layer at a time, but skip-mode layers still kept their
parameters in the ModuleList — so frameworks transferred all weights
every forward pass. This PR adds:
- **`_SkipLayer`**: replaces fully-calibrated layers with a
parameter-free dummy in the ModuleList, so framework hooks have nothing
to transfer
- **`persistent_materialization`**: keeps the active layer on GPU for
the entire calibration step, avoiding repeated offload/reload cycles

Checkpoint save is per-layer; restore is bulk — quantizer state and
weights for layers 0..K-1 are restored once at the end of calibration,
keeping the hot path fast.

### Example commands

**Qwen3-8B** (NVFP4+GPTQ, single GPU):
```bash
python hf_ptq.py \
    --pyt_ckpt_path Qwen/Qwen3-8B \
    --recipe nvfp4_gptq_sequential.yaml \
    --calib_size 64 \
    --batch_size 16 \
    --dataset cnn_dailymail \
    --export_path outputs/qwen3_8b_nvfp4_gptq_seq \
    --gpu_max_mem_percentage 0.5 \
    --use_seq_device_map \
    --vllm_fakequant_export
```

**DeepSeek-R1** (NVFP4 experts-only + FP8 KV, 8×80GB):
```bash
python hf_ptq.py \
    --model unsloth/DeepSeek-R1-0528-BF16 \
    --recipe ../../modelopt_recipes/general/ptq/nvfp4_experts_only-fp8_kv.yaml \
    --dataset cnn_dailymail \
    --batch_size 16 \
    --calib_size 64 \
    --calib_seq 512 \
    --gpu_max_mem_percentage 0.5 \
    --use_seq_device_map \
    --trust_remote_code \
    --export_path output/DeepSeek-R1-BF16-nvfp4-experts-only-fp8-kv \
    --vllm_fakequant_export
```

### Example: NVFP4+GPTQ layerwise calibration on Qwen3-8B (36 layers,
single GPU — 20 GB peak)

**Initial run** (killed after layer 11):
```
Layerwise calibration: Found 36 transformer layers
Calibrating layer 1/36 | capture: [1]
Computing Hessians for 7 linear layers...
GPTQ time: 51.39s
Calibrating layer 2/36 | run: [1] | capture: [2]
Checkpoint: saved layer 0
GPTQ time: 50.06s
Calibrating layer 3/36 | skip: 1 | run: [2] | capture: [3]
Checkpoint: saved layer 1
...
Calibrating layer 12/36 | skip: 10 | run: [11] | capture: [12]
Checkpoint: saved layer 10
<killed>
```

**Resumed run** (picks up from layer 11, finishes all 36):
```
Layerwise calibration: Found 36 transformer layers
Checkpoint: resuming layerwise calibration from layer 11/36
Calibrating layer 12 (resumed)
GPTQ time: 51.45s
Calibrating layer 13/36 | skip: 11 | run: [12] | capture: [13]
Checkpoint: saved layer 11
...
Calibrating layer 36/36 | skip: 34 | run: [35] | capture: [36]
Checkpoint: saved layer 34
GPTQ time: 50.33s
Checkpoint: saved layer 35 (final)
Checkpoint: restored 11 previously calibrated layers
Layerwise calibration completed
Quantized model exported to: outputs/qwen3_8b_nvfp4_gptq_seq
GPU 0: Peak memory usage = 20.42 GB
```

## TODO
- [ ] Update CHANGELOG

## Test plan
- `tests/unit/torch/quantization/test_layerwise_calibrate.py` — unit
tests for skip/swap/restore
- `tests/unit/torch/quantization/test_sequential_checkpoint.py` —
checkpoint save/resume correctness
- `tests/gpu/torch/quantization/plugins/test_accelerate_gpu.py` —
CPU-offloaded layerwise + GPTQ + checkpoint resume
- `tests/gpu/torch/quantization/test_fsdp2.py` — FSDP2 layerwise
calibration

### Verified
- [x] Qwen3-8B: layerwise calibration + checkpoint save/restore +
fakequantized checkpoint export + vLLM serve
- [x] DeepSeek-R1: checkpoint resume tested
- [x] DeepSeek-R1: fakequantized checkpoint export verified

---------

Signed-off-by: realAsma <akuriparambi@nvidia.com>
2026-04-18 00:32:34 +00:00

644 lines
24 KiB
Python

# SPDX-FileCopyrightText: Copyright (c) 2023-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import copy
import json
import os
import shutil
import pytest
import torch
from _test_utils.torch.transformers_models import create_tiny_llama_dir
from accelerate import init_empty_weights, load_checkpoint_and_dispatch
from transformers import AutoConfig, AutoModelForCausalLM
import modelopt.torch.quantization as mtq
from modelopt.torch.quantization.utils import (
enable_weight_access_and_writeback,
is_quantized_linear,
)
from modelopt.torch.quantization.utils.layerwise_calib import _layer_dir
def test_cpu_offloaded_tinyllama(tmp_path):
quant_cfg = mtq.NVFP4_AWQ_LITE_CFG
tiny_llama_dir = create_tiny_llama_dir(tmp_path, num_hidden_layers=2)
config = AutoConfig.from_pretrained(tiny_llama_dir)
model_ref = AutoModelForCausalLM.from_pretrained(
tiny_llama_dir, torch_dtype=config.torch_dtype
).cuda()
inputs = torch.randint(0, model_ref.config.vocab_size, (1, 4)).cuda()
mtq.quantize(model_ref, quant_cfg, lambda model: model(inputs))
output_ref = model_ref(inputs)
with init_empty_weights():
model = AutoModelForCausalLM.from_config(config)
device_map = {
n: 0
for n, m in model.named_modules()
if "layers" not in n or n.split("layers.")[-1].isdigit()
}
device_map["model.layers.0"] = "cpu"
model = load_checkpoint_and_dispatch(model, tiny_llama_dir, device_map=device_map)
assert all(p.device == torch.device("meta") for p in model.model.layers[0].parameters())
mtq.quantize(model, quant_cfg, lambda model: model(inputs))
output_test = model(inputs)
for name, module in model.named_modules():
if is_quantized_linear(module):
with enable_weight_access_and_writeback(module, model):
assert torch.allclose(module.weight, model_ref.get_submodule(name).weight)
assert torch.allclose(output_ref.logits, output_test.logits)
def _make_cpu_offloaded_model(tmp_path, num_hidden_layers=3):
"""Create a tiny LLaMA model with layer 0 offloaded to CPU via accelerate."""
tiny_llama_dir = create_tiny_llama_dir(tmp_path, num_hidden_layers=num_hidden_layers)
config = AutoConfig.from_pretrained(tiny_llama_dir)
with init_empty_weights():
model = AutoModelForCausalLM.from_config(config)
device_map = {
n: 0
for n, m in model.named_modules()
if "layers" not in n or n.split("layers.")[-1].isdigit()
}
device_map["model.layers.0"] = "cpu"
model = load_checkpoint_and_dispatch(model, tiny_llama_dir, device_map=device_map)
inputs = torch.randint(0, config.vocab_size, (1, 4)).cuda()
return model, config, tiny_llama_dir, inputs
def _make_layerwise_cfg(base_cfg):
"""Add layerwise=True to a quant config's algorithm field."""
cfg = copy.deepcopy(base_cfg)
algo = cfg.get("algorithm", "max")
if isinstance(algo, str):
cfg["algorithm"] = {"method": algo, "layerwise": True}
else:
algo["layerwise"] = True
return cfg
def _make_layerwise_checkpoint_cfg(base_cfg, checkpoint_dir):
"""Add layerwise=True and layerwise_checkpoint_dir to a quant config's algorithm field."""
cfg = _make_layerwise_cfg(base_cfg)
cfg["algorithm"]["layerwise_checkpoint_dir"] = checkpoint_dir
return cfg
@pytest.mark.parametrize("use_checkpoint", [False, True], ids=["no_ckpt", "ckpt"])
def test_layerwise_calibrate_cpu_offloaded(tmp_path, use_checkpoint):
"""Layerwise calibration on CPU-offloaded model matches GPU-only reference."""
quant_cfg = mtq.NVFP4_AWQ_LITE_CFG
num_layers = 3
tiny_llama_dir = create_tiny_llama_dir(tmp_path, num_hidden_layers=num_layers)
config = AutoConfig.from_pretrained(tiny_llama_dir)
inputs = torch.randint(0, config.vocab_size, (1, 4)).cuda()
if use_checkpoint:
ckpt_dir = str(tmp_path / "seq_ckpt")
seq_cfg = _make_layerwise_checkpoint_cfg(quant_cfg, ckpt_dir)
else:
seq_cfg = _make_layerwise_cfg(quant_cfg)
# Reference: GPU-only model with layerwise calibration
ref_cfg = _make_layerwise_cfg(quant_cfg)
model_ref = AutoModelForCausalLM.from_pretrained(
tiny_llama_dir, torch_dtype=config.torch_dtype
).cuda()
mtq.quantize(model_ref, ref_cfg, lambda model: model(inputs))
output_ref = model_ref(inputs)
# Test: CPU-offloaded model
with init_empty_weights():
model = AutoModelForCausalLM.from_config(config)
device_map = {
n: 0
for n, m in model.named_modules()
if "layers" not in n or n.split("layers.")[-1].isdigit()
}
device_map["model.layers.0"] = "cpu"
model = load_checkpoint_and_dispatch(model, tiny_llama_dir, device_map=device_map)
mtq.quantize(model, seq_cfg, lambda model: model(inputs))
output_test = model(inputs)
for name, module in model.named_modules():
if is_quantized_linear(module):
with enable_weight_access_and_writeback(module, model):
assert torch.allclose(module.weight, model_ref.get_submodule(name).weight), (
f"Weight mismatch at {name}"
)
assert torch.allclose(output_ref.logits, output_test.logits)
if use_checkpoint:
manifest_path = os.path.join(ckpt_dir, "manifest.json")
assert os.path.isfile(manifest_path)
with open(manifest_path) as f:
manifest = json.load(f)
assert manifest["last_completed_layer"] == num_layers - 1
assert manifest["num_layers"] == num_layers
def test_sequential_checkpoint_resume_cpu_offloaded(tmp_path):
"""Resume from a partial checkpoint on a CPU-offloaded model matches a full run."""
quant_cfg = mtq.NVFP4_AWQ_LITE_CFG
num_layers = 3
tiny_llama_dir = create_tiny_llama_dir(tmp_path, num_hidden_layers=num_layers)
config = AutoConfig.from_pretrained(tiny_llama_dir)
inputs = torch.randint(0, config.vocab_size, (1, 4)).cuda()
ckpt_dir = str(tmp_path / "seq_ckpt")
seq_ckpt_cfg = _make_layerwise_checkpoint_cfg(quant_cfg, ckpt_dir)
# Full reference run with checkpointing
with init_empty_weights():
model_ref = AutoModelForCausalLM.from_config(config)
device_map = {
n: 0
for n, m in model_ref.named_modules()
if "layers" not in n or n.split("layers.")[-1].isdigit()
}
device_map["model.layers.0"] = "cpu"
model_ref = load_checkpoint_and_dispatch(model_ref, tiny_llama_dir, device_map=device_map)
mtq.quantize(model_ref, seq_ckpt_cfg, lambda model: model(inputs))
output_ref = model_ref(inputs)
# Simulate crash after layer 0 by truncating the manifest and removing later layers
last_completed_layer = 0
manifest_path = os.path.join(ckpt_dir, "manifest.json")
with open(manifest_path, "w") as f:
json.dump({"last_completed_layer": last_completed_layer, "num_layers": num_layers}, f)
for i in range(last_completed_layer + 1, num_layers):
d = _layer_dir(ckpt_dir, i)
if os.path.isdir(d):
shutil.rmtree(d)
# Resume from a fresh CPU-offloaded model
with init_empty_weights():
model_resumed = AutoModelForCausalLM.from_config(config)
device_map = {
n: 0
for n, m in model_resumed.named_modules()
if "layers" not in n or n.split("layers.")[-1].isdigit()
}
device_map["model.layers.0"] = "cpu"
model_resumed = load_checkpoint_and_dispatch(
model_resumed, tiny_llama_dir, device_map=device_map
)
mtq.quantize(model_resumed, seq_ckpt_cfg, lambda model: model(inputs))
output_resumed = model_resumed(inputs)
assert torch.allclose(output_ref.logits, output_resumed.logits), (
"Resumed checkpoint should produce identical output to full run"
)
def test_sequential_checkpoint_resume_multi_offload(tmp_path):
"""Resume with multiple layers offloaded exercises per-layer device resolution."""
num_layers = 3
tiny_llama_dir = create_tiny_llama_dir(tmp_path, num_hidden_layers=num_layers)
config = AutoConfig.from_pretrained(tiny_llama_dir)
inputs = torch.randint(0, config.vocab_size, (1, 4)).cuda()
ckpt_dir = str(tmp_path / "seq_ckpt")
seq_ckpt_cfg = _make_layerwise_checkpoint_cfg(mtq.INT4_AWQ_CFG, ckpt_dir)
def _make_multi_offload_model():
with init_empty_weights():
m = AutoModelForCausalLM.from_config(config)
dmap = {
n: 0
for n, mod in m.named_modules()
if "layers" not in n or n.split("layers.")[-1].isdigit()
}
dmap["model.layers.0"] = "cpu"
dmap["model.layers.1"] = "cpu"
return load_checkpoint_and_dispatch(m, tiny_llama_dir, device_map=dmap)
# Full reference run
model_ref = _make_multi_offload_model()
mtq.quantize(model_ref, seq_ckpt_cfg, lambda model: model(inputs))
output_ref = model_ref(inputs)
# Simulate crash after layer 0
last_completed_layer = 0
manifest_path = os.path.join(ckpt_dir, "manifest.json")
with open(manifest_path, "w") as f:
json.dump({"last_completed_layer": last_completed_layer, "num_layers": num_layers}, f)
for i in range(last_completed_layer + 1, num_layers):
d = _layer_dir(ckpt_dir, i)
if os.path.isdir(d):
shutil.rmtree(d)
# Resume from fresh model with same offload layout
model_resumed = _make_multi_offload_model()
mtq.quantize(model_resumed, seq_ckpt_cfg, lambda model: model(inputs))
output_resumed = model_resumed(inputs)
assert torch.allclose(output_ref.logits, output_resumed.logits), (
"Resumed checkpoint with multi-offload should match full run"
)
def _make_gptq_sequential_cfg(base_cfg):
"""Create a sequential GPTQ config from a base quantization config."""
cfg = copy.deepcopy(base_cfg)
cfg["algorithm"] = {"method": "gptq", "layerwise": True}
return cfg
def _make_gptq_sequential_checkpoint_cfg(base_cfg, checkpoint_dir):
"""Create a sequential GPTQ config with checkpoint dir."""
cfg = _make_gptq_sequential_cfg(base_cfg)
cfg["algorithm"]["layerwise_checkpoint_dir"] = checkpoint_dir
return cfg
@pytest.mark.parametrize("use_checkpoint", [False, True], ids=["no_ckpt", "ckpt"])
def test_sequential_gptq_cpu_offloaded(tmp_path, use_checkpoint):
"""Sequential GPTQ (weight-modifying) on CPU-offloaded model matches GPU-only reference."""
num_layers = 3
tiny_llama_dir = create_tiny_llama_dir(tmp_path, num_hidden_layers=num_layers)
config = AutoConfig.from_pretrained(tiny_llama_dir)
inputs = torch.randint(0, config.vocab_size, (1, 4)).cuda()
if use_checkpoint:
ckpt_dir = str(tmp_path / "gptq_ckpt")
seq_cfg = _make_gptq_sequential_checkpoint_cfg(mtq.NVFP4_AWQ_LITE_CFG, ckpt_dir)
else:
seq_cfg = _make_gptq_sequential_cfg(mtq.NVFP4_AWQ_LITE_CFG)
# Reference: GPU-only model
ref_cfg = _make_gptq_sequential_cfg(mtq.NVFP4_AWQ_LITE_CFG)
model_ref = AutoModelForCausalLM.from_pretrained(
tiny_llama_dir, torch_dtype=config.torch_dtype
).cuda()
mtq.quantize(model_ref, ref_cfg, lambda model: model(inputs))
output_ref = model_ref(inputs)
# Test: CPU-offloaded model
model, _, _, _ = _make_cpu_offloaded_model(tmp_path / "offloaded", num_hidden_layers=num_layers)
mtq.quantize(model, seq_cfg, lambda model: model(inputs))
output_test = model(inputs)
for name, module in model.named_modules():
if is_quantized_linear(module):
with enable_weight_access_and_writeback(module, model):
assert torch.allclose(module.weight, model_ref.get_submodule(name).weight), (
f"Weight mismatch at {name}"
)
assert torch.allclose(output_ref.logits, output_test.logits)
def test_sequential_gptq_checkpoint_resume_cpu_offloaded(tmp_path):
"""GPTQ checkpoint resume with CPU offloading restores modified weights correctly."""
num_layers = 3
tiny_llama_dir = create_tiny_llama_dir(tmp_path, num_hidden_layers=num_layers)
config = AutoConfig.from_pretrained(tiny_llama_dir)
inputs = torch.randint(0, config.vocab_size, (1, 4)).cuda()
ckpt_dir = str(tmp_path / "gptq_ckpt")
seq_ckpt_cfg = _make_gptq_sequential_checkpoint_cfg(mtq.NVFP4_AWQ_LITE_CFG, ckpt_dir)
# Full reference run with checkpointing
with init_empty_weights():
model_ref = AutoModelForCausalLM.from_config(config)
device_map = {
n: 0
for n, m in model_ref.named_modules()
if "layers" not in n or n.split("layers.")[-1].isdigit()
}
device_map["model.layers.0"] = "cpu"
model_ref = load_checkpoint_and_dispatch(model_ref, tiny_llama_dir, device_map=device_map)
mtq.quantize(model_ref, seq_ckpt_cfg, lambda model: model(inputs))
output_ref = model_ref(inputs)
# Simulate crash after layer 0
last_completed_layer = 0
manifest_path = os.path.join(ckpt_dir, "manifest.json")
with open(manifest_path, "w") as f:
json.dump({"last_completed_layer": last_completed_layer, "num_layers": num_layers}, f)
for i in range(last_completed_layer + 1, num_layers):
d = _layer_dir(ckpt_dir, i)
if os.path.isdir(d):
shutil.rmtree(d)
# Resume from fresh CPU-offloaded model
with init_empty_weights():
model_resumed = AutoModelForCausalLM.from_config(config)
device_map = {
n: 0
for n, m in model_resumed.named_modules()
if "layers" not in n or n.split("layers.")[-1].isdigit()
}
device_map["model.layers.0"] = "cpu"
model_resumed = load_checkpoint_and_dispatch(
model_resumed, tiny_llama_dir, device_map=device_map
)
mtq.quantize(model_resumed, seq_ckpt_cfg, lambda model: model(inputs))
output_resumed = model_resumed(inputs)
assert torch.allclose(output_ref.logits, output_resumed.logits), (
"GPTQ resumed checkpoint should produce identical output to full run"
)
class _TupleReturningBlock(torch.nn.Module):
"""Decoder layer that returns a tuple, mimicking HuggingFace decoder layers."""
def __init__(self, dim=16):
super().__init__()
self.linear = torch.nn.Linear(dim, dim, bias=False)
def forward(self, x, **kwargs):
return (self.linear(x), None)
class _TupleUnpackingModel(torch.nn.Module):
"""Parent model that unpacks layer outputs as tuples."""
def __init__(self, n_layers=4, dim=16):
super().__init__()
self.layers = torch.nn.ModuleList([_TupleReturningBlock(dim) for _ in range(n_layers)])
def forward(self, x):
for layer in self.layers:
x, _ = layer(x)
return x
def test_skip_dummy_has_no_hf_hook(monkeypatch):
"""Dummies must not carry _hf_hook from the original layer."""
from accelerate.hooks import AlignDevicesHook, add_hook_to_module
from modelopt.torch.quantization.utils.layerwise_calib import (
LayerActivationCollector,
_SkipLayer,
)
monkeypatch.setattr(
LayerActivationCollector,
"_decoder_layer_support",
[(lambda m: hasattr(m, "layers"), lambda m: m.layers)],
)
model = _TupleUnpackingModel(n_layers=4, dim=16)
data = [torch.randn(2, 16)]
for layer in model.layers:
hook = AlignDevicesHook(execution_device=torch.device("cpu"))
add_hook_to_module(layer, hook)
def forward_loop(m):
for d in data:
m(d)
collector = LayerActivationCollector(model)
collector._patch_all_layers()
try:
for layer in list(model.layers):
collector.get_input_activations(layer, forward_loop)
for i in range(2):
dummy = model.layers[i]
assert isinstance(dummy, _SkipLayer)
assert not hasattr(dummy, "_hf_hook"), f"Dummy at {i} should not have _hf_hook"
finally:
collector._unpatch_all_layers()
def test_persistent_materialization_cpu_offloaded(tmp_path):
"""persistent_materialization keeps CPU-offloaded weights on GPU and writes back modifications."""
import torch.nn as nn
from accelerate.hooks import AlignDevicesHook
from modelopt.torch.quantization.utils import persistent_materialization
model, config, _, inputs = _make_cpu_offloaded_model(tmp_path)
offloaded_layer = model.model.layers[0]
# Verify offloaded (meta device)
assert all(p.device.type == "meta" for p in offloaded_layer.parameters())
# Save reference weight
linear = None
with enable_weight_access_and_writeback(offloaded_layer, model):
linear = next(m for m in offloaded_layer.modules() if isinstance(m, nn.Linear))
ref_weight = linear.weight.clone()
with persistent_materialization(offloaded_layer):
# Params materialized on GPU
assert all(
p.device.type == "cuda" for p in offloaded_layer.parameters() if p.device.type != "meta"
)
# Run multiple forward passes (hooks don't re-offload)
for _ in range(3):
model(inputs)
# Modify a weight
linear.weight.data.add_(1.0)
# Verify hooks have offload=False during context
for mod in offloaded_layer.modules():
if hasattr(mod, "_hf_hook"):
hook = mod._hf_hook
if isinstance(hook, AlignDevicesHook):
assert not hook.offload
# After context: back to meta device (offloaded)
assert all(p.device.type == "meta" for p in offloaded_layer.parameters())
# Verify weight modification persisted through writeback
with enable_weight_access_and_writeback(offloaded_layer, model):
assert torch.allclose(linear.weight, ref_weight + 1.0)
def _make_disk_offload_device_map(model):
"""Build a device_map with layer 0 on disk, everything else on GPU 0.
Ancestor modules (``""`` and ``"model"``) are excluded so that
``dispatch_model`` does not attach a ``place_submodules=True`` hook that
would try to move disk-offloaded meta tensors to GPU (which fails because
no ``value`` is available — unlike CPU offload where weights are on CPU and
can be moved directly).
"""
device_map = {
n: 0
for n, m in model.named_modules()
if n not in ("", "model") and ("layers" not in n or n.split("layers.")[-1].isdigit())
}
device_map["model.layers.0"] = "disk"
return device_map
def _make_disk_offloaded_model(tmp_path, num_hidden_layers=3):
"""Create a tiny LLaMA model with layer 0 offloaded to disk via accelerate."""
tiny_llama_dir = create_tiny_llama_dir(tmp_path, num_hidden_layers=num_hidden_layers)
config = AutoConfig.from_pretrained(tiny_llama_dir)
with init_empty_weights():
model = AutoModelForCausalLM.from_config(config)
device_map = _make_disk_offload_device_map(model)
offload_dir = str(tmp_path / "offload")
model = load_checkpoint_and_dispatch(
model, tiny_llama_dir, device_map=device_map, offload_folder=offload_dir
)
inputs = torch.randint(0, config.vocab_size, (1, 4)).cuda()
return model, config, tiny_llama_dir, inputs
def test_disk_offloaded_tinyllama(tmp_path):
quant_cfg = mtq.NVFP4_AWQ_LITE_CFG
tiny_llama_dir = create_tiny_llama_dir(tmp_path, num_hidden_layers=2)
config = AutoConfig.from_pretrained(tiny_llama_dir)
model_ref = AutoModelForCausalLM.from_pretrained(
tiny_llama_dir, torch_dtype=config.torch_dtype
).cuda()
inputs = torch.randint(0, model_ref.config.vocab_size, (1, 4)).cuda()
mtq.quantize(model_ref, quant_cfg, lambda model: model(inputs))
output_ref = model_ref(inputs)
with init_empty_weights():
model = AutoModelForCausalLM.from_config(config)
device_map = _make_disk_offload_device_map(model)
offload_dir = str(tmp_path / "offload")
model = load_checkpoint_and_dispatch(
model, tiny_llama_dir, device_map=device_map, offload_folder=offload_dir
)
assert all(p.device == torch.device("meta") for p in model.model.layers[0].parameters())
mtq.quantize(model, quant_cfg, lambda model: model(inputs))
output_test = model(inputs)
for name, module in model.named_modules():
if is_quantized_linear(module):
with enable_weight_access_and_writeback(module, model):
assert torch.allclose(module.weight, model_ref.get_submodule(name).weight)
assert torch.allclose(output_ref.logits, output_test.logits)
def test_persistent_materialization_disk_offloaded(tmp_path):
"""persistent_materialization keeps disk-offloaded weights on GPU and writes back modifications."""
import torch.nn as nn
from accelerate.hooks import AlignDevicesHook
from modelopt.torch.quantization.utils import persistent_materialization
model, config, _, inputs = _make_disk_offloaded_model(tmp_path)
offloaded_layer = model.model.layers[0]
# Verify offloaded (meta device)
assert all(p.device.type == "meta" for p in offloaded_layer.parameters())
# Save reference weight
linear = None
with enable_weight_access_and_writeback(offloaded_layer, model):
linear = next(m for m in offloaded_layer.modules() if isinstance(m, nn.Linear))
ref_weight = linear.weight.clone()
with persistent_materialization(offloaded_layer):
# Params materialized on GPU
assert all(
p.device.type == "cuda" for p in offloaded_layer.parameters() if p.device.type != "meta"
)
# Run multiple forward passes (hooks don't re-offload)
for _ in range(3):
model(inputs)
# Modify a weight
linear.weight.data.add_(1.0)
# Verify hooks have offload=False during context
for mod in offloaded_layer.modules():
if hasattr(mod, "_hf_hook"):
hook = mod._hf_hook
if isinstance(hook, AlignDevicesHook):
assert not hook.offload
# After context: back to meta device (offloaded)
assert all(p.device.type == "meta" for p in offloaded_layer.parameters())
# Verify weight modification persisted through writeback
with enable_weight_access_and_writeback(offloaded_layer, model):
assert torch.allclose(linear.weight, ref_weight + 1.0)
@pytest.mark.parametrize("use_checkpoint", [False, True], ids=["no_ckpt", "ckpt"])
def test_layerwise_calibrate_disk_offloaded(tmp_path, use_checkpoint):
"""Layerwise calibration on disk-offloaded model matches GPU-only reference."""
quant_cfg = mtq.NVFP4_AWQ_LITE_CFG
num_layers = 3
tiny_llama_dir = create_tiny_llama_dir(tmp_path, num_hidden_layers=num_layers)
config = AutoConfig.from_pretrained(tiny_llama_dir)
inputs = torch.randint(0, config.vocab_size, (1, 4)).cuda()
if use_checkpoint:
ckpt_dir = str(tmp_path / "seq_ckpt")
seq_cfg = _make_layerwise_checkpoint_cfg(quant_cfg, ckpt_dir)
else:
seq_cfg = _make_layerwise_cfg(quant_cfg)
# Reference: GPU-only model with layerwise calibration
ref_cfg = _make_layerwise_cfg(quant_cfg)
model_ref = AutoModelForCausalLM.from_pretrained(
tiny_llama_dir, torch_dtype=config.torch_dtype
).cuda()
mtq.quantize(model_ref, ref_cfg, lambda model: model(inputs))
output_ref = model_ref(inputs)
# Test: disk-offloaded model
with init_empty_weights():
model = AutoModelForCausalLM.from_config(config)
device_map = _make_disk_offload_device_map(model)
offload_dir = str(tmp_path / "offload")
model = load_checkpoint_and_dispatch(
model, tiny_llama_dir, device_map=device_map, offload_folder=offload_dir
)
mtq.quantize(model, seq_cfg, lambda model: model(inputs))
output_test = model(inputs)
for name, module in model.named_modules():
if is_quantized_linear(module):
with enable_weight_access_and_writeback(module, model):
assert torch.allclose(module.weight, model_ref.get_submodule(name).weight), (
f"Weight mismatch at {name}"
)
assert torch.allclose(output_ref.logits, output_test.logits)