mirror of
https://github.com/elder-plinius/OBLITERATUS.git
synced 2026-08-30 14:40:38 +02:00
392 lines
13 KiB
Python
392 lines
13 KiB
Python
"""CPU-only contracts for transactional Accelerate offload surgery."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import MutableMapping
|
|
|
|
import pytest
|
|
import torch
|
|
import torch.nn as nn
|
|
from accelerate.big_modeling import disk_offload
|
|
from accelerate.hooks import (
|
|
AlignDevicesHook,
|
|
ModelHook,
|
|
SequentialHook,
|
|
add_hook_to_module,
|
|
)
|
|
from accelerate.utils.modeling import get_state_dict_offloaded_model
|
|
|
|
import obliteratus.models.offload_surgery as offload_surgery
|
|
from obliteratus.abliterate import AbliterationPipeline
|
|
from obliteratus.models.offload_surgery import (
|
|
OffloadSurgeryError,
|
|
UnsupportedOffloadLayoutError,
|
|
UnsupportedOffloadedQuantizationError,
|
|
logical_module_device,
|
|
resolve_logical_parameter,
|
|
validate_offloaded_parameters,
|
|
)
|
|
|
|
|
|
def _direct_offload(module: nn.Module) -> AlignDevicesHook:
|
|
hook = AlignDevicesHook(execution_device="cpu", offload=True)
|
|
add_hook_to_module(module, hook)
|
|
return hook
|
|
|
|
|
|
def test_direct_align_hook_updates_backing_weight_and_preserves_meta_state():
|
|
module = nn.Linear(3, 2, bias=False)
|
|
original = module.weight.detach().clone()
|
|
hook = _direct_offload(module)
|
|
parameter_identity = id(module.weight)
|
|
|
|
transaction = resolve_logical_parameter(module)
|
|
transaction.commit(transaction.tensor + 2)
|
|
|
|
assert module.weight.device.type == "meta"
|
|
assert id(module.weight) == parameter_identity
|
|
torch.testing.assert_close(hook.weights_map["weight"], original + 2)
|
|
|
|
|
|
def test_sequential_hook_finds_nested_align_devices_hook():
|
|
module = nn.Linear(3, 2, bias=False)
|
|
original = module.weight.detach().clone()
|
|
align = AlignDevicesHook(execution_device="cpu", offload=True)
|
|
add_hook_to_module(module, SequentialHook(ModelHook(), align))
|
|
|
|
transaction = resolve_logical_parameter(module)
|
|
transaction.commit(transaction.tensor - 1)
|
|
|
|
assert module.weight.device.type == "meta"
|
|
torch.testing.assert_close(align.weights_map["weight"], original - 1)
|
|
|
|
|
|
def test_parent_prefixed_weight_map_resolves_nested_parameter():
|
|
block = nn.Module()
|
|
block.self_attn = nn.Module()
|
|
block.self_attn.o_proj = nn.Linear(3, 2, bias=False)
|
|
original = block.self_attn.o_proj.weight.detach().clone()
|
|
hook = AlignDevicesHook(
|
|
execution_device="cpu",
|
|
offload=True,
|
|
place_submodules=True,
|
|
)
|
|
add_hook_to_module(block, hook)
|
|
|
|
transaction = resolve_logical_parameter(
|
|
block.self_attn.o_proj,
|
|
search_roots=(block,),
|
|
)
|
|
assert transaction.backing_key == "self_attn.o_proj.weight"
|
|
transaction.commit(transaction.tensor * 0.5)
|
|
|
|
assert block.self_attn.o_proj.weight.device.type == "meta"
|
|
torch.testing.assert_close(
|
|
hook.weights_map["self_attn.o_proj.weight"],
|
|
original * 0.5,
|
|
)
|
|
|
|
|
|
def test_disk_backed_prefixed_loader_uses_updated_authoritative_value(tmp_path):
|
|
model = nn.Sequential(nn.Linear(3, 2, bias=False))
|
|
original = model[0].weight.detach().clone()
|
|
disk_offload(model, tmp_path, execution_device=torch.device("cpu"))
|
|
|
|
transaction = resolve_logical_parameter(model[0], search_roots=(model,))
|
|
transaction.commit(transaction.tensor + 3)
|
|
|
|
assert model[0].weight.device.type == "meta"
|
|
hook = model[0]._hf_hook
|
|
torch.testing.assert_close(hook.weights_map["weight"], original + 3)
|
|
gathered = get_state_dict_offloaded_model(model)
|
|
assert all(tensor.device.type != "meta" for tensor in gathered.values())
|
|
torch.testing.assert_close(gathered["0.weight"], original + 3)
|
|
|
|
|
|
def test_save_reload_observes_surgery_instead_of_stale_backing_weight():
|
|
model = nn.Sequential(nn.Linear(3, 2, bias=False))
|
|
original = model[0].weight.detach().clone()
|
|
_direct_offload(model[0])
|
|
|
|
transaction = resolve_logical_parameter(model[0], search_roots=(model,))
|
|
transaction.commit(transaction.tensor - 4)
|
|
state_dict = get_state_dict_offloaded_model(model)
|
|
|
|
reloaded = nn.Sequential(nn.Linear(3, 2, bias=False))
|
|
reloaded.load_state_dict(state_dict)
|
|
torch.testing.assert_close(reloaded[0].weight, original - 4)
|
|
assert reloaded[0].weight.device.type == "cpu"
|
|
|
|
|
|
def test_tied_backing_values_retain_identity_and_consistent_values():
|
|
block = nn.Module()
|
|
block.first = nn.Linear(3, 3, bias=False)
|
|
block.second = nn.Linear(3, 3, bias=False)
|
|
shared = block.first.weight.detach().clone()
|
|
shared_meta = nn.Parameter(torch.empty_like(shared, device="meta"))
|
|
block.first.weight = shared_meta
|
|
block.second.weight = shared_meta
|
|
hook = AlignDevicesHook(
|
|
execution_device="cpu",
|
|
offload=True,
|
|
weights_map={"first.weight": shared, "second.weight": shared},
|
|
place_submodules=True,
|
|
)
|
|
block._hf_hook = hook
|
|
live_identity = id(block.first.weight)
|
|
|
|
transaction = resolve_logical_parameter(block.first, search_roots=(block,))
|
|
transaction.commit(transaction.tensor + 1)
|
|
|
|
first = hook.weights_map["first.weight"]
|
|
second = hook.weights_map["second.weight"]
|
|
assert first is second
|
|
torch.testing.assert_close(first, shared + 1)
|
|
assert block.first.weight is block.second.weight
|
|
assert id(block.first.weight) == live_identity
|
|
|
|
|
|
class _FailOnceAliasMap(MutableMapping[str, torch.Tensor]):
|
|
def __init__(self, shared: torch.Tensor):
|
|
self.values = {"first.weight": shared, "second.weight": shared}
|
|
self.failed = False
|
|
|
|
def __getitem__(self, key: str) -> torch.Tensor:
|
|
return self.values[key]
|
|
|
|
def __setitem__(self, key: str, value: torch.Tensor) -> None:
|
|
if key == "second.weight" and not self.failed:
|
|
self.failed = True
|
|
raise OSError("simulated backing-store failure")
|
|
self.values[key] = value
|
|
|
|
def __delitem__(self, key: str) -> None:
|
|
del self.values[key]
|
|
|
|
def __iter__(self):
|
|
return iter(self.values)
|
|
|
|
def __len__(self) -> int:
|
|
return len(self.values)
|
|
|
|
|
|
def test_commit_failure_rolls_back_all_backing_aliases_and_leaves_meta_state():
|
|
block = nn.Module()
|
|
block.first = nn.Linear(3, 3, bias=False)
|
|
original = block.first.weight.detach().clone()
|
|
block.first.weight = nn.Parameter(torch.empty_like(original, device="meta"))
|
|
backing = _FailOnceAliasMap(original)
|
|
block._hf_hook = AlignDevicesHook(
|
|
execution_device="cpu",
|
|
offload=True,
|
|
weights_map=backing,
|
|
place_submodules=True,
|
|
)
|
|
|
|
transaction = resolve_logical_parameter(block.first, search_roots=(block,))
|
|
with pytest.raises(OffloadSurgeryError, match="rolled back"):
|
|
transaction.commit(transaction.tensor + 5)
|
|
|
|
torch.testing.assert_close(backing["first.weight"], original)
|
|
torch.testing.assert_close(backing["second.weight"], original)
|
|
assert block.first.weight.device.type == "meta"
|
|
|
|
|
|
def test_quantized_offloaded_weight_fails_before_any_mutation():
|
|
module = nn.Linear(3, 2, bias=False, device="meta")
|
|
backing = {"weight": torch.ones(2, 3, dtype=torch.uint8)}
|
|
module._hf_hook = AlignDevicesHook(
|
|
execution_device="cpu",
|
|
offload=True,
|
|
weights_map=backing,
|
|
)
|
|
|
|
with pytest.raises(UnsupportedOffloadedQuantizationError):
|
|
resolve_logical_parameter(module)
|
|
|
|
assert module.weight.device.type == "meta"
|
|
assert backing["weight"].dtype == torch.uint8
|
|
assert torch.equal(backing["weight"], torch.ones(2, 3, dtype=torch.uint8))
|
|
|
|
|
|
def test_read_only_unknown_mapping_fails_during_resolution():
|
|
module = nn.Linear(3, 2, bias=False, device="meta")
|
|
|
|
class MappingOnly:
|
|
def __getitem__(self, key):
|
|
return torch.ones(2, 3)
|
|
|
|
def __iter__(self):
|
|
return iter(("weight",))
|
|
|
|
def __len__(self):
|
|
return 1
|
|
|
|
# Registering through Mapping is deliberately avoided: this object models
|
|
# a future wrapper with read access but no supported write contract.
|
|
hook = AlignDevicesHook(execution_device="cpu", offload=True)
|
|
hook.weights_map = MappingOnly()
|
|
module._hf_hook = hook
|
|
|
|
with pytest.raises(UnsupportedOffloadLayoutError, match="read-only"):
|
|
resolve_logical_parameter(module)
|
|
|
|
|
|
def test_validation_resolves_all_meta_parameters_before_surgery():
|
|
block = nn.Module()
|
|
block.proj = nn.Linear(3, 2, bias=False)
|
|
hook = AlignDevicesHook(
|
|
execution_device="cpu",
|
|
offload=True,
|
|
place_submodules=True,
|
|
)
|
|
add_hook_to_module(block, hook)
|
|
|
|
validate_offloaded_parameters(block)
|
|
|
|
hook.weights_map = {}
|
|
with pytest.raises(UnsupportedOffloadLayoutError, match="attempted keys"):
|
|
validate_offloaded_parameters(block)
|
|
|
|
|
|
def test_advanced_projection_updates_offloaded_logical_weight():
|
|
module = nn.Module()
|
|
module.proj = nn.Linear(3, 2, bias=False)
|
|
original = module.proj.weight.detach().clone()
|
|
hook = _direct_offload(module.proj)
|
|
direction = torch.tensor([[1.0], [0.0], [0.0]])
|
|
|
|
count = AbliterationPipeline._project_out_advanced(
|
|
module,
|
|
direction,
|
|
["proj"],
|
|
)
|
|
|
|
assert count == 1
|
|
assert module.proj.weight.device.type == "meta"
|
|
expected = original.clone()
|
|
expected[:, 0] = 0
|
|
torch.testing.assert_close(hook.weights_map["weight"], expected)
|
|
|
|
|
|
def test_advanced_projection_resolves_all_candidates_before_mutation():
|
|
block = nn.Module()
|
|
block.first = nn.Linear(3, 2, bias=False)
|
|
first_original = block.first.weight.detach().clone()
|
|
first_hook = _direct_offload(block.first)
|
|
block.second = nn.Linear(3, 2, bias=False, device="meta")
|
|
block.second._hf_hook = AlignDevicesHook(
|
|
execution_device="cpu",
|
|
offload=True,
|
|
weights_map={"weight": torch.ones(2, 3, dtype=torch.uint8)},
|
|
)
|
|
|
|
with pytest.raises(UnsupportedOffloadedQuantizationError):
|
|
AbliterationPipeline._project_out_advanced(
|
|
block,
|
|
torch.tensor([[1.0], [0.0], [0.0]]),
|
|
["first", "second"],
|
|
)
|
|
|
|
torch.testing.assert_close(first_hook.weights_map["weight"], first_original)
|
|
assert block.first.weight.device.type == "meta"
|
|
|
|
|
|
def test_logical_module_device_uses_authoritative_backing_device():
|
|
module = nn.Linear(3, 2, bias=False)
|
|
_direct_offload(module)
|
|
|
|
assert logical_module_device(module) == torch.device("cpu")
|
|
|
|
|
|
def test_unknown_accelerate_major_version_fails_closed(monkeypatch):
|
|
module = nn.Linear(3, 2, bias=False)
|
|
original = module.weight.detach().clone()
|
|
hook = _direct_offload(module)
|
|
monkeypatch.setattr(offload_surgery, "version", lambda _name: "2.0.0")
|
|
|
|
with pytest.raises(UnsupportedOffloadLayoutError, match="outside the tested"):
|
|
resolve_logical_parameter(module)
|
|
|
|
assert module.weight.device.type == "meta"
|
|
torch.testing.assert_close(hook.weights_map["weight"], original)
|
|
|
|
|
|
def test_bias_projection_updates_parent_prefixed_backing_value():
|
|
block = nn.Module()
|
|
block.proj = nn.Linear(3, 3, bias=True)
|
|
original = block.proj.bias.detach().clone()
|
|
hook = AlignDevicesHook(
|
|
execution_device="cpu",
|
|
offload=True,
|
|
place_submodules=True,
|
|
)
|
|
add_hook_to_module(block, hook)
|
|
|
|
count = AbliterationPipeline._project_bias(
|
|
block,
|
|
torch.tensor([[1.0], [0.0], [0.0]]),
|
|
["proj"],
|
|
offload_roots=(block,),
|
|
)
|
|
|
|
assert count == 1
|
|
assert block.proj.bias.device.type == "meta"
|
|
expected = original.clone()
|
|
expected[0] = 0
|
|
torch.testing.assert_close(hook.weights_map["proj.bias"], expected)
|
|
|
|
|
|
class _FailSecondParameterMap(MutableMapping[str, torch.Tensor]):
|
|
def __init__(self, first: torch.Tensor, second: torch.Tensor):
|
|
self.values = {"first.weight": first, "second.weight": second}
|
|
self.failed = False
|
|
|
|
def __getitem__(self, key: str) -> torch.Tensor:
|
|
return self.values[key]
|
|
|
|
def __setitem__(self, key: str, value: torch.Tensor) -> None:
|
|
if key == "second.weight" and not self.failed:
|
|
self.failed = True
|
|
raise OSError("simulated second-parameter failure")
|
|
self.values[key] = value
|
|
|
|
def __delitem__(self, key: str) -> None:
|
|
del self.values[key]
|
|
|
|
def __iter__(self):
|
|
return iter(self.values)
|
|
|
|
def __len__(self) -> int:
|
|
return len(self.values)
|
|
|
|
|
|
def test_multi_parameter_projection_rolls_back_prior_commits():
|
|
block = nn.Module()
|
|
block.first = nn.Linear(3, 2, bias=False)
|
|
block.second = nn.Linear(3, 2, bias=False)
|
|
first = block.first.weight.detach().clone()
|
|
second = block.second.weight.detach().clone()
|
|
block.first.weight = nn.Parameter(torch.empty_like(first, device="meta"))
|
|
block.second.weight = nn.Parameter(torch.empty_like(second, device="meta"))
|
|
backing = _FailSecondParameterMap(first, second)
|
|
block._hf_hook = AlignDevicesHook(
|
|
execution_device="cpu",
|
|
offload=True,
|
|
weights_map=backing,
|
|
place_submodules=True,
|
|
)
|
|
|
|
with pytest.raises(OffloadSurgeryError, match="rolled back"):
|
|
AbliterationPipeline._project_out_advanced(
|
|
block,
|
|
torch.tensor([[1.0], [0.0], [0.0]]),
|
|
["first", "second"],
|
|
offload_roots=(block,),
|
|
)
|
|
|
|
torch.testing.assert_close(backing["first.weight"], first)
|
|
torch.testing.assert_close(backing["second.weight"], second)
|
|
assert block.first.weight.device.type == "meta"
|
|
assert block.second.weight.device.type == "meta"
|