mirror of
https://github.com/unslothai/unsloth.git
synced 2026-10-21 14:00:32 +00:00
* Load one tensor at a time when quantizing a 16-bit checkpoint on the fly transformers 5.0 to 5.4 materialises every checkpoint tensor at full precision on the card from four worker threads while the main thread quantizes them one by one, so nothing bounds how many bf16 tensors sit on the GPU at once. transformers 5.5 switches to synchronous loading in exactly this case. Unsloth Studio runs Qwen3.5 training on its transformers 5.3.0 sidecar. A bf16 Qwen3.5-27B at 4bit ran out of memory loading there on a 32 GB R9700, while a plain script on 5.5.0 peaked at 16.8 GiB, and offload_layers=48 loaded fine (the zoo's load-to-host context already sets the same variable). This is the code-read cause; the per-phase measurement on the card is still to run. sync_load_when_quantizing sets HF_DEACTIVATE_ASYNC_LOAD=1 around the causal LM loads in FastBaseModel and FastLlamaModel when a quantization_config is given and the checkpoint is not pre-quantized. A caller's own setting wins. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Sync-load the sequence classification path too; 5.4 already loads on-the-fly quants synchronously * Sync-load the torchao save reloads and the offline fp8 quantize too * Tighten sync-load docstrings --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Han <23090290+danielhanchen@users.noreply.github.com>
137 lines
4.5 KiB
Python
137 lines
4.5 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
|
|
|
|
"""sync_load_when_quantizing, extracted with ast so nothing imports CUDA. No GPU needed."""
|
|
|
|
import ast
|
|
import contextlib
|
|
import os
|
|
import types
|
|
|
|
import pytest
|
|
|
|
HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
MODELS = os.path.join(HERE, "unsloth", "models")
|
|
ENV = "HF_DEACTIVATE_ASYNC_LOAD"
|
|
|
|
|
|
def _source(name):
|
|
with open(os.path.join(MODELS, name), encoding = "utf-8") as f:
|
|
return f.read()
|
|
|
|
|
|
def _load_helper():
|
|
tree = ast.parse(_source("loader_utils.py"))
|
|
keep = [
|
|
node
|
|
for node in tree.body
|
|
if (isinstance(node, ast.FunctionDef) and node.name == "sync_load_when_quantizing")
|
|
or (
|
|
isinstance(node, ast.Assign)
|
|
and any(getattr(t, "id", None) == "_ASYNC_LOAD_ENV" for t in node.targets)
|
|
)
|
|
]
|
|
assert len(keep) == 2
|
|
namespace = {"contextlib": contextlib, "os": os}
|
|
exec(compile(ast.Module(body = keep, type_ignores = []), "loader_utils", "exec"), namespace)
|
|
return namespace["sync_load_when_quantizing"]
|
|
|
|
|
|
sync_load_when_quantizing = _load_helper()
|
|
BNB = object()
|
|
UNQUANTIZED = types.SimpleNamespace()
|
|
PREQUANTIZED = types.SimpleNamespace(quantization_config = {"quant_method": "bitsandbytes"})
|
|
|
|
|
|
@pytest.fixture(autouse = True)
|
|
def _clean_env(monkeypatch):
|
|
monkeypatch.delenv(ENV, raising = False)
|
|
|
|
|
|
def test_on_the_fly_quantization_loads_synchronously_then_restores():
|
|
with sync_load_when_quantizing(BNB, UNQUANTIZED):
|
|
assert os.environ.get(ENV) == "1"
|
|
assert ENV not in os.environ
|
|
|
|
|
|
def test_restores_when_the_load_raises():
|
|
with pytest.raises(RuntimeError):
|
|
with sync_load_when_quantizing(BNB, UNQUANTIZED):
|
|
raise RuntimeError("out of memory")
|
|
assert ENV not in os.environ
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"quantization_config, config",
|
|
[(BNB, PREQUANTIZED), (None, UNQUANTIZED), (None, PREQUANTIZED)],
|
|
ids = ["prequantized", "16bit", "16bit-prequantized-config"],
|
|
)
|
|
def test_leaves_async_loading_alone_when_nothing_is_quantized_on_the_fly(
|
|
quantization_config, config
|
|
):
|
|
with sync_load_when_quantizing(quantization_config, config):
|
|
assert ENV not in os.environ
|
|
assert ENV not in os.environ
|
|
|
|
|
|
def test_a_caller_setting_wins(monkeypatch):
|
|
monkeypatch.setenv(ENV, "0")
|
|
with sync_load_when_quantizing(BNB, UNQUANTIZED):
|
|
assert os.environ[ENV] == "0"
|
|
assert os.environ[ENV] == "0"
|
|
|
|
|
|
def _calls_inside_sync_load(src, callee):
|
|
"""Each `<callee>.from_pretrained(...)` call, paired with whether a `with` around it uses the helper."""
|
|
tree = ast.parse(src)
|
|
parents = {}
|
|
for node in ast.walk(tree):
|
|
for child in ast.iter_child_nodes(node):
|
|
parents[child] = node
|
|
found = []
|
|
for node in ast.walk(tree):
|
|
if not (
|
|
isinstance(node, ast.Call)
|
|
and isinstance(node.func, ast.Attribute)
|
|
and node.func.attr == "from_pretrained"
|
|
and getattr(node.func.value, "id", None) == callee
|
|
):
|
|
continue
|
|
wrapped, cur = False, node
|
|
while cur in parents:
|
|
cur = parents[cur]
|
|
if isinstance(cur, ast.With) and any(
|
|
isinstance(item.context_expr, ast.Call)
|
|
and getattr(item.context_expr.func, "id", None) == "sync_load_when_quantizing"
|
|
for item in cur.items
|
|
):
|
|
wrapped = True
|
|
break
|
|
found.append((node.lineno, wrapped))
|
|
return found
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"module, callee",
|
|
[
|
|
("vision.py", "auto_model"),
|
|
("llama.py", "AutoModelForCausalLM"),
|
|
("llama.py", "AutoModelForSequenceClassification"),
|
|
("loader_utils.py", "auto_model"),
|
|
("../save.py", "auto_model"),
|
|
],
|
|
)
|
|
def test_every_on_the_fly_quantizing_load_runs_inside_the_helper(module, callee):
|
|
calls = _calls_inside_sync_load(_source(module), callee)
|
|
assert calls, f"no {callee}.from_pretrained in {module}"
|
|
assert all(wrapped for _, wrapped in calls), calls
|
|
|
|
|
|
def test_transformers_reads_the_variable_where_it_picks_the_loader():
|
|
# Pins the contract the helper leans on: the variable is what turns the thread pool off.
|
|
core = pytest.importorskip("transformers.core_model_loading")
|
|
import inspect
|
|
|
|
src = inspect.getsource(core.convert_and_load_state_dict_in_model)
|
|
assert 'is_env_variable_true("HF_DEACTIVATE_ASYNC_LOAD")' in src
|
|
assert "thread_pool = None" in src
|