mirror of
https://github.com/elder-plinius/OBLITERATUS.git
synced 2026-08-17 16:37:30 +02:00
334 lines
12 KiB
Python
334 lines
12 KiB
Python
"""Deterministic and transactional contracts for local model checkpoints."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import stat
|
|
import tempfile
|
|
import uuid
|
|
from collections.abc import Mapping
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
from typing import Any, Callable, Iterator, Protocol
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
if os.name == "nt": # pragma: no cover - exercised by the Windows CI lane
|
|
_DIRECTORY_SYNC_FLAGS: int | None = None
|
|
else:
|
|
_DIRECTORY_SYNC_FLAGS = os.O_RDONLY | os.O_DIRECTORY
|
|
|
|
|
|
class SizedTensor(Protocol):
|
|
"""Structural subset used to estimate a serialized state dictionary."""
|
|
|
|
def numel(self) -> int: ...
|
|
|
|
def element_size(self) -> int: ...
|
|
|
|
|
|
def state_dict_size_bytes(state_dict: Mapping[str, SizedTensor]) -> int:
|
|
"""Return the exact unsharded tensor payload size for a state dictionary."""
|
|
return sum(value.numel() * value.element_size() for value in state_dict.values())
|
|
|
|
|
|
def required_checkpoint_bytes(payload_bytes: int) -> int:
|
|
"""Return the checkpoint capacity requirement with ten percent headroom."""
|
|
if payload_bytes < 0:
|
|
raise ValueError("checkpoint payload size cannot be negative")
|
|
return (payload_bytes * 11 + 9) // 10
|
|
|
|
|
|
def ensure_checkpoint_capacity(
|
|
free_bytes: int,
|
|
payload_bytes: int,
|
|
) -> None:
|
|
"""Reject a checkpoint write that cannot satisfy the headroom policy."""
|
|
if free_bytes < required_checkpoint_bytes(payload_bytes):
|
|
raise OSError(
|
|
f"Insufficient disk space: {free_bytes / 1e9:.1f} GB free, "
|
|
f"need ~{payload_bytes / 1e9:.1f} GB. "
|
|
f"Try a different --output-dir on a larger filesystem.",
|
|
)
|
|
|
|
|
|
def serialize_checkpoint_metadata(metadata: Mapping[str, Any]) -> str:
|
|
"""Serialize checkpoint metadata before any model artifact is written."""
|
|
return json.dumps(metadata, indent=2, sort_keys=True, allow_nan=False)
|
|
|
|
|
|
def _read_json_object(path: Path, artifact_name: str) -> Mapping[str, Any]:
|
|
"""Load a required, non-empty JSON object from a checkpoint."""
|
|
if path.is_symlink() or not path.is_file() or path.stat().st_size == 0:
|
|
raise ValueError(f"Checkpoint {artifact_name} is missing or empty: {path}")
|
|
try:
|
|
value = json.loads(path.read_bytes().decode())
|
|
except (OSError, UnicodeError, json.JSONDecodeError) as error:
|
|
raise ValueError(f"Checkpoint {artifact_name} is corrupt: {path}") from error
|
|
if not isinstance(value, Mapping):
|
|
raise ValueError(f"Checkpoint {artifact_name} must contain a JSON object: {path}")
|
|
return value
|
|
|
|
|
|
def _require_nonempty_regular_file(path: Path, artifact_name: str) -> None:
|
|
"""Reject absent, empty, or link-backed checkpoint payloads."""
|
|
if (
|
|
path.is_symlink()
|
|
or not path.is_file()
|
|
or not stat.S_ISREG(path.stat().st_mode)
|
|
or path.stat().st_size == 0
|
|
):
|
|
raise ValueError(f"Checkpoint {artifact_name} is missing or empty: {path}")
|
|
|
|
|
|
def validate_local_checkpoint(
|
|
checkpoint_dir: Path,
|
|
expected_metadata_json: str,
|
|
) -> None:
|
|
"""Validate the minimum artifacts needed for a local Transformers reload."""
|
|
checkpoint_dir = Path(checkpoint_dir)
|
|
if checkpoint_dir.is_symlink() or not checkpoint_dir.is_dir():
|
|
raise ValueError(f"Checkpoint staging path is not a directory: {checkpoint_dir}")
|
|
|
|
expected_metadata = json.loads(expected_metadata_json)
|
|
actual_metadata = _read_json_object(
|
|
checkpoint_dir / "abliteration_metadata.json",
|
|
"metadata",
|
|
)
|
|
if actual_metadata != expected_metadata:
|
|
raise ValueError("Checkpoint metadata does not match the prepared transaction")
|
|
|
|
_read_json_object(checkpoint_dir / "config.json", "model config")
|
|
_read_json_object(checkpoint_dir / "tokenizer_config.json", "tokenizer config")
|
|
|
|
direct_weights = [
|
|
checkpoint_dir / "model.safetensors",
|
|
checkpoint_dir / "pytorch_model.bin",
|
|
]
|
|
for weights_path in direct_weights:
|
|
if _path_exists(weights_path):
|
|
_require_nonempty_regular_file(weights_path, "weights")
|
|
return
|
|
|
|
index_paths = [
|
|
checkpoint_dir / "model.safetensors.index.json",
|
|
checkpoint_dir / "pytorch_model.bin.index.json",
|
|
]
|
|
for index_path in index_paths:
|
|
if not _path_exists(index_path):
|
|
continue
|
|
index = _read_json_object(index_path, "weight index")
|
|
weight_map = index.get("weight_map")
|
|
if not isinstance(weight_map, Mapping) or not weight_map:
|
|
raise ValueError(f"Checkpoint weight index has no weight map: {index_path}")
|
|
shard_values = list(weight_map.values())
|
|
if not all(isinstance(name, str) and Path(name).name == name for name in shard_values):
|
|
raise ValueError(f"Checkpoint weight index contains an unsafe shard path: {index_path}")
|
|
shard_names = set(shard_values)
|
|
for shard_name in shard_names:
|
|
_require_nonempty_regular_file(
|
|
checkpoint_dir / shard_name,
|
|
"weight shard",
|
|
)
|
|
return
|
|
|
|
raise ValueError(f"Checkpoint has no model weights: {checkpoint_dir}")
|
|
|
|
|
|
def _path_exists(path: Path) -> bool:
|
|
"""Return whether a path or dangling symlink occupies ``path``."""
|
|
return path.exists() or path.is_symlink()
|
|
|
|
|
|
def _remove_checkpoint_path(path: Path) -> None:
|
|
"""Remove a staging/backup path without following directory symlinks."""
|
|
if path.is_symlink() or path.is_file():
|
|
path.unlink(missing_ok=True)
|
|
elif path.exists():
|
|
try:
|
|
shutil.rmtree(path)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
|
|
def _sync_file(path: Path) -> None:
|
|
"""Flush one regular checkpoint artifact to stable storage."""
|
|
mode = path.lstat().st_mode
|
|
if not stat.S_ISREG(mode):
|
|
raise OSError(f"Checkpoint artifact is not a regular file: {path}")
|
|
descriptor = os.open(path, os.O_RDONLY)
|
|
try:
|
|
os.fsync(descriptor)
|
|
finally:
|
|
os.close(descriptor)
|
|
|
|
|
|
def _sync_directory(path: Path) -> None:
|
|
"""Persist directory entries where the platform exposes directory fsync."""
|
|
if _DIRECTORY_SYNC_FLAGS is None:
|
|
return
|
|
descriptor = os.open(path, _DIRECTORY_SYNC_FLAGS)
|
|
try:
|
|
os.fsync(descriptor)
|
|
finally:
|
|
os.close(descriptor)
|
|
|
|
|
|
def _sync_checkpoint_tree(checkpoint_dir: Path) -> None:
|
|
"""Flush checkpoint files and directories from leaves to root."""
|
|
for root, directory_names, file_names in os.walk(
|
|
checkpoint_dir,
|
|
topdown=False,
|
|
followlinks=False,
|
|
):
|
|
root_path = Path(root)
|
|
for file_name in sorted(file_names):
|
|
_sync_file(root_path / file_name)
|
|
for directory_name in sorted(directory_names):
|
|
directory = root_path / directory_name
|
|
if directory.is_symlink():
|
|
raise OSError(
|
|
f"Checkpoint directory contains a symlink: {directory}",
|
|
)
|
|
_sync_directory(directory)
|
|
_sync_directory(root_path)
|
|
|
|
|
|
@contextmanager
|
|
def _checkpoint_lock(destination: Path) -> Iterator[None]:
|
|
"""Serialize commits to one destination across threads and processes."""
|
|
lock_path = destination.with_name(f".{destination.name}.lock")
|
|
with lock_path.open("a+b") as lock_file:
|
|
if os.name == "nt": # pragma: no cover - exercised by the Windows CI lane
|
|
import msvcrt
|
|
|
|
lock_file.seek(0, os.SEEK_END)
|
|
if lock_file.tell() == 0:
|
|
lock_file.write(b"\0")
|
|
lock_file.flush()
|
|
lock_file.seek(0)
|
|
msvcrt.locking(lock_file.fileno(), msvcrt.LK_LOCK, 1)
|
|
try:
|
|
yield
|
|
finally:
|
|
lock_file.seek(0)
|
|
msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
|
|
else:
|
|
import fcntl
|
|
|
|
fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
|
|
try:
|
|
yield
|
|
finally:
|
|
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
|
|
|
|
|
|
def _rollback_checkpoint_commit(
|
|
destination: Path,
|
|
staging: Path,
|
|
backup: Path | None,
|
|
*,
|
|
had_destination: bool,
|
|
) -> None:
|
|
"""Restore the pre-commit state after a failed or cancelled commit."""
|
|
if backup is not None and _path_exists(backup):
|
|
if _path_exists(destination):
|
|
if _path_exists(staging):
|
|
raise RuntimeError(
|
|
"Checkpoint rollback found both destination and staging; "
|
|
f"recover the previous checkpoint from {backup}",
|
|
)
|
|
os.replace(destination, staging)
|
|
os.replace(backup, destination)
|
|
elif not had_destination and _path_exists(destination) and not _path_exists(staging):
|
|
os.replace(destination, staging)
|
|
_sync_directory(destination.parent)
|
|
|
|
|
|
def _cleanup_failed_staging(staging: Path) -> None:
|
|
"""Remove failed staging without masking the transaction's primary error."""
|
|
try:
|
|
_remove_checkpoint_path(staging)
|
|
except OSError as cleanup_error:
|
|
logger.warning(
|
|
"Checkpoint write failed, and staging directory %s could not be "
|
|
"removed: %s",
|
|
staging,
|
|
cleanup_error,
|
|
)
|
|
|
|
|
|
@contextmanager
|
|
def atomic_checkpoint_directory(
|
|
destination: Path,
|
|
*,
|
|
validate: Callable[[Path], None] | None = None,
|
|
) -> Iterator[Path]:
|
|
"""Yield a sibling staging directory and atomically promote it on success.
|
|
|
|
An existing checkpoint is moved to a uniquely named backup immediately
|
|
before promotion. Concurrent commits to the same destination are serialized.
|
|
Validation and durable file flushes happen before the commit lock is taken.
|
|
If commit or parent-directory sync fails, the previous destination is
|
|
restored. Cancellation and other ``BaseException`` failures follow the same
|
|
rollback path as ordinary exceptions.
|
|
"""
|
|
destination = Path(destination)
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
staging = Path(
|
|
tempfile.mkdtemp(
|
|
prefix=f".{destination.name or 'checkpoint'}.staging-",
|
|
dir=destination.parent,
|
|
),
|
|
)
|
|
backup: Path | None = None
|
|
try:
|
|
yield staging
|
|
if validate is not None:
|
|
validate(staging)
|
|
_sync_checkpoint_tree(staging)
|
|
with _checkpoint_lock(destination):
|
|
had_destination = _path_exists(destination)
|
|
try:
|
|
if had_destination:
|
|
backup = destination.with_name(
|
|
f".{destination.name}.backup-{uuid.uuid4().hex}",
|
|
)
|
|
os.replace(destination, backup)
|
|
_sync_directory(destination.parent)
|
|
os.replace(staging, destination)
|
|
_sync_directory(destination.parent)
|
|
except BaseException:
|
|
try:
|
|
_rollback_checkpoint_commit(
|
|
destination,
|
|
staging,
|
|
backup,
|
|
had_destination=had_destination,
|
|
)
|
|
except BaseException as restore_error:
|
|
recovery_path = backup if backup is not None else staging
|
|
raise RuntimeError(
|
|
"Checkpoint promotion and rollback both failed; "
|
|
f"recover the previous checkpoint from {recovery_path}",
|
|
) from restore_error
|
|
raise
|
|
if backup is not None:
|
|
try:
|
|
_remove_checkpoint_path(backup)
|
|
_sync_directory(destination.parent)
|
|
except OSError as cleanup_error:
|
|
logger.warning(
|
|
"Checkpoint promoted, but previous-checkpoint backup %s "
|
|
"could not be removed durably: %s",
|
|
backup,
|
|
cleanup_error,
|
|
)
|
|
except BaseException:
|
|
_cleanup_failed_staging(staging)
|
|
raise
|