wsm 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wsm/__init__.py +14 -0
- wsm/__main__.py +3 -0
- wsm/_publication.py +19 -0
- wsm/cli.py +78 -0
- wsm/coding_tui.py +79 -0
- wsm/editing.py +394 -0
- wsm/errors.py +7 -0
- wsm/modal_trial.py +162 -0
- wsm/program.py +213 -0
- wsm/trial.py +248 -0
- wsm/trial_distillation.py +197 -0
- wsm/trial_editor.py +89 -0
- wsm/trial_grading.py +84 -0
- wsm/trial_poc.py +422 -0
- wsm/trial_training.py +261 -0
- wsm/trial_transport.py +188 -0
- wsm/trial_worker.py +81 -0
- wsm/tui.py +110 -0
- wsm-0.1.0.dist-info/METADATA +50 -0
- wsm-0.1.0.dist-info/RECORD +23 -0
- wsm-0.1.0.dist-info/WHEEL +5 -0
- wsm-0.1.0.dist-info/entry_points.txt +2 -0
- wsm-0.1.0.dist-info/top_level.txt +1 -0
wsm/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Python tools for editing, evaluating, and saving model weights."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
__all__ = ["WeightModel", "execute_program"]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def __getattr__(name):
|
|
8
|
+
if name == "WeightModel":
|
|
9
|
+
from .editing import WeightModel
|
|
10
|
+
return WeightModel
|
|
11
|
+
if name == "execute_program":
|
|
12
|
+
from .program import execute_program
|
|
13
|
+
return execute_program
|
|
14
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
wsm/__main__.py
ADDED
wsm/_publication.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Atomic checkpoint publication without replacing an existing destination."""
|
|
2
|
+
import ctypes
|
|
3
|
+
import errno
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from .errors import WeightlessError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _publish(source, destination):
|
|
10
|
+
rename = getattr(ctypes.CDLL(None, use_errno=True), "renameat2", None)
|
|
11
|
+
if rename is None:
|
|
12
|
+
raise WeightlessError("unsupported_platform", "Atomic exclusive export requires renameat2")
|
|
13
|
+
rename.argtypes = [ctypes.c_int, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint]
|
|
14
|
+
rename.restype = ctypes.c_int
|
|
15
|
+
if rename(-100, os.fsencode(source), -100, os.fsencode(destination), 1) != 0:
|
|
16
|
+
code = ctypes.get_errno()
|
|
17
|
+
if code in (errno.EEXIST, errno.ENOTEMPTY):
|
|
18
|
+
raise WeightlessError("export_exists", "Destination already exists")
|
|
19
|
+
raise OSError(code, os.strerror(code), str(destination))
|
wsm/cli.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Commands for trusted Python weight editing."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
from .errors import WeightlessError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parser():
|
|
12
|
+
root = argparse.ArgumentParser(prog="wsm", description="Edit model weights with trusted Python. No command opens the TUI.")
|
|
13
|
+
root.add_argument('--version', action='version', version=f'wsm {__version__}')
|
|
14
|
+
commands = root.add_subparsers(dest='command')
|
|
15
|
+
execute = commands.add_parser('execute', help='Execute a trusted Python file defining edit(model)')
|
|
16
|
+
execute.add_argument('program', type=Path)
|
|
17
|
+
execute.add_argument('--model', type=Path, required=True)
|
|
18
|
+
execute.add_argument('--output', type=Path, required=True)
|
|
19
|
+
execute.add_argument('--cases', type=Path, required=True)
|
|
20
|
+
execute.add_argument('--device', default='cpu')
|
|
21
|
+
commands.add_parser('tui', help='Open the Python editing terminal')
|
|
22
|
+
trial = commands.add_parser("trial", help="Run or watch an experimental Modal coding trial")
|
|
23
|
+
trial_commands = trial.add_subparsers(dest="trial_command", required=True)
|
|
24
|
+
preflight = trial_commands.add_parser("preflight", help="Validate explicit task, policy, and price configuration offline")
|
|
25
|
+
preflight.add_argument("--config", type=Path, required=True)
|
|
26
|
+
trial_run = trial_commands.add_parser("run", help="Run one fixed-model Modal trial and hosted Tinker update")
|
|
27
|
+
trial_run.add_argument("--config", type=Path, required=True)
|
|
28
|
+
trial_run.add_argument("--output", type=Path, required=True)
|
|
29
|
+
trial_view = trial_commands.add_parser("view", help="Read the public trial journal without control access")
|
|
30
|
+
trial_view.add_argument("directory", type=Path)
|
|
31
|
+
trial_view.add_argument("--follow", action="store_true")
|
|
32
|
+
weights = trial_commands.add_parser("weights", help="Read retained tensor shapes or numerical values")
|
|
33
|
+
weights.add_argument("checkpoint", type=Path, help="Retained safetensors file")
|
|
34
|
+
weights.add_argument("--tensor")
|
|
35
|
+
weights.add_argument("--indices", type=int, nargs="+")
|
|
36
|
+
return root
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main(argv=None):
|
|
40
|
+
args = parser().parse_args(argv)
|
|
41
|
+
try:
|
|
42
|
+
if args.command == "trial":
|
|
43
|
+
from .trial_poc import inspect_weights, load_config, run_poc, watch
|
|
44
|
+
if args.trial_command == "view":
|
|
45
|
+
watch(args.directory, follow=args.follow)
|
|
46
|
+
elif args.trial_command == "weights":
|
|
47
|
+
print(json.dumps(inspect_weights(args.checkpoint, tensor=args.tensor, indices=args.indices),
|
|
48
|
+
indent=2, allow_nan=False))
|
|
49
|
+
else:
|
|
50
|
+
config = load_config(args.config)
|
|
51
|
+
result = ({"valid": True, "cloud_calls": 0, "budget_usd": config["budget"]["max_usd"]}
|
|
52
|
+
if args.trial_command == "preflight" else run_poc(config, args.output))
|
|
53
|
+
print(json.dumps(result, indent=2, allow_nan=False))
|
|
54
|
+
return 0
|
|
55
|
+
if args.command in (None, 'tui'):
|
|
56
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
57
|
+
raise WeightlessError('terminal_required', 'The TUI requires a terminal; use --help for scriptable commands')
|
|
58
|
+
from .tui import launch
|
|
59
|
+
return launch()
|
|
60
|
+
from .program import execute_program
|
|
61
|
+
cases = json.loads(args.cases.read_text())
|
|
62
|
+
result = execute_program(args.program, model_path=args.model, output=args.output,
|
|
63
|
+
device=args.device, cases=cases)
|
|
64
|
+
print(json.dumps(result, allow_nan=False, indent=2))
|
|
65
|
+
return 0 if result.get('state') == 'published' else 1
|
|
66
|
+
except KeyboardInterrupt:
|
|
67
|
+
message = (f"Interrupted. Inspect {args.output / 'run.json'} and its checkpoint before retrying."
|
|
68
|
+
if args.command == 'execute' else 'Interrupted.')
|
|
69
|
+
print(json.dumps({'error': {'code': 'interrupted', 'message': message}}), file=sys.stderr)
|
|
70
|
+
return 130
|
|
71
|
+
except (Exception, SystemExit) as error:
|
|
72
|
+
print(json.dumps({'error': {'code': getattr(error, 'code', 'execution_error'),
|
|
73
|
+
'type': type(error).__name__, 'message': str(error)}}), file=sys.stderr)
|
|
74
|
+
return 1
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == '__main__':
|
|
78
|
+
raise SystemExit(main())
|
wsm/coding_tui.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Terminal controls for the same Python execution command used by coding agents."""
|
|
2
|
+
import curses
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import signal
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def stop(process, first_signal):
|
|
13
|
+
try:
|
|
14
|
+
os.killpg(process.pid, first_signal)
|
|
15
|
+
except ProcessLookupError:
|
|
16
|
+
pass
|
|
17
|
+
try:
|
|
18
|
+
process.wait(timeout=10)
|
|
19
|
+
except subprocess.TimeoutExpired:
|
|
20
|
+
pass
|
|
21
|
+
finally:
|
|
22
|
+
try:
|
|
23
|
+
os.killpg(process.pid, signal.SIGKILL)
|
|
24
|
+
except ProcessLookupError:
|
|
25
|
+
pass
|
|
26
|
+
process.wait(timeout=10)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def execute(screen):
|
|
30
|
+
from .tui import prompt, put, show_result
|
|
31
|
+
|
|
32
|
+
program = Path(prompt(screen, "Python editing file defining edit(model)")).expanduser().resolve()
|
|
33
|
+
show_result(screen, "Review Python source before execution", program.read_text().splitlines())
|
|
34
|
+
model = Path(prompt(screen, "Local model snapshot directory")).expanduser().resolve()
|
|
35
|
+
cases = Path(prompt(screen, "Public evaluation cases JSON")).expanduser().resolve()
|
|
36
|
+
output = Path(prompt(screen, "New output directory")).expanduser().resolve()
|
|
37
|
+
device = prompt(screen, "Torch device", default="cpu")
|
|
38
|
+
command = [sys.executable, "-m", "wsm", "execute", str(program), "--model", str(model),
|
|
39
|
+
"--cases", str(cases), "--output", str(output), "--device", device]
|
|
40
|
+
process = None
|
|
41
|
+
with tempfile.TemporaryFile(mode="w+b") as log:
|
|
42
|
+
try:
|
|
43
|
+
process = subprocess.Popen(command, stdin=subprocess.DEVNULL, stdout=log,
|
|
44
|
+
stderr=subprocess.STDOUT, start_new_session=True)
|
|
45
|
+
screen.timeout(100)
|
|
46
|
+
while process.poll() is None:
|
|
47
|
+
screen.erase()
|
|
48
|
+
put(screen, 1, "WSM / execute Python", attr=curses.A_BOLD)
|
|
49
|
+
put(screen, 3, str(program))
|
|
50
|
+
state = "starting"
|
|
51
|
+
try:
|
|
52
|
+
state = json.loads((output / "run.json").read_text()).get("state", state)
|
|
53
|
+
except (OSError, ValueError):
|
|
54
|
+
pass
|
|
55
|
+
put(screen, 5, "State " + state)
|
|
56
|
+
put(screen, 7, "Output " + str(output))
|
|
57
|
+
put(screen, 9, "Ctrl-C interrupts execution. Publication is recorded in run.json.")
|
|
58
|
+
screen.refresh()
|
|
59
|
+
try:
|
|
60
|
+
screen.get_wch()
|
|
61
|
+
except curses.error:
|
|
62
|
+
pass
|
|
63
|
+
except KeyboardInterrupt:
|
|
64
|
+
if process is not None:
|
|
65
|
+
stop(process, signal.SIGINT)
|
|
66
|
+
finally:
|
|
67
|
+
screen.timeout(-1)
|
|
68
|
+
if process is not None:
|
|
69
|
+
stop(process, signal.SIGTERM)
|
|
70
|
+
log.seek(0, 2)
|
|
71
|
+
log.seek(max(0, log.tell() - 16000))
|
|
72
|
+
text = log.read(16000).decode('utf-8', errors='replace')
|
|
73
|
+
try:
|
|
74
|
+
result = json.loads((output / "run.json").read_text())
|
|
75
|
+
except (OSError, ValueError):
|
|
76
|
+
result = {"state": "unknown", "output": str(output),
|
|
77
|
+
"detail": "Inspect output before retrying. No completion receipt was readable."}
|
|
78
|
+
show_result(screen, "Python execution result", {"run": result, "process_output": text})
|
|
79
|
+
return result
|
wsm/editing.py
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"""Experimental live-tensor editing.
|
|
2
|
+
|
|
3
|
+
`WeightModel` exposes the parameters of a local causal language model as ordinary
|
|
4
|
+
PyTorch tensors. Checkpoint publication uses the same lifecycle as program
|
|
5
|
+
execution: ready, running, staging, verified, then published. `failure` and
|
|
6
|
+
`interrupted` are reachable only from running, staging, or verified. `published`
|
|
7
|
+
is terminal.
|
|
8
|
+
|
|
9
|
+
An operating-system crash can leave a prepublication staging directory and a
|
|
10
|
+
journal whose state is not published. Those artifacts are not a completed run.
|
|
11
|
+
A later error must not relabel a checkpoint whose directory was already
|
|
12
|
+
published.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import contextlib
|
|
16
|
+
import json
|
|
17
|
+
import math
|
|
18
|
+
import os
|
|
19
|
+
import shutil
|
|
20
|
+
import tempfile
|
|
21
|
+
from collections.abc import Mapping
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from .errors import WeightlessError
|
|
25
|
+
|
|
26
|
+
_OFFLINE = ("HF_HUB_OFFLINE", "TRANSFORMERS_OFFLINE", "HF_DATASETS_OFFLINE", "HF_HUB_DISABLE_TELEMETRY")
|
|
27
|
+
_UNSAFE_SUFFIXES = {".py", ".pyc", ".so", ".bin", ".pt", ".pth", ".pkl", ".pickle"}
|
|
28
|
+
_FORWARD = {"ready": "running", "running": "staging", "staging": "verified", "verified": "published"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class _Lifecycle:
|
|
32
|
+
"""One publication statechart for a run journal and for a checkpoint directory."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, on_phase=None):
|
|
35
|
+
self.state = "ready"
|
|
36
|
+
self.verified = False
|
|
37
|
+
self.published = False
|
|
38
|
+
self._on_phase = on_phase
|
|
39
|
+
self._notify()
|
|
40
|
+
|
|
41
|
+
def _notify(self):
|
|
42
|
+
if self._on_phase is not None:
|
|
43
|
+
self._on_phase(self.state)
|
|
44
|
+
|
|
45
|
+
def advance(self, state):
|
|
46
|
+
if _FORWARD.get(self.state) != state:
|
|
47
|
+
raise RuntimeError(f"Cannot move from {self.state} to {state}")
|
|
48
|
+
if state == "verified":
|
|
49
|
+
self.verified = True
|
|
50
|
+
if state == "published":
|
|
51
|
+
if not self.verified:
|
|
52
|
+
raise RuntimeError("Publication requires verification")
|
|
53
|
+
self.published = True
|
|
54
|
+
self.state = state
|
|
55
|
+
self._notify()
|
|
56
|
+
|
|
57
|
+
def stop(self, state):
|
|
58
|
+
if self.published or self.state == "published":
|
|
59
|
+
return
|
|
60
|
+
if self.state not in ("running", "staging", "verified") or state not in ("failure", "interrupted"):
|
|
61
|
+
return
|
|
62
|
+
self.state = state
|
|
63
|
+
self._notify()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _Weights(Mapping):
|
|
67
|
+
"""Live parameter views, including tied names, detached from autograd."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, model):
|
|
70
|
+
self._model = model
|
|
71
|
+
|
|
72
|
+
def _parameters(self):
|
|
73
|
+
return dict(self._model.named_parameters(remove_duplicate=False))
|
|
74
|
+
|
|
75
|
+
def __getitem__(self, name):
|
|
76
|
+
try:
|
|
77
|
+
tensor = self._parameters()[name]
|
|
78
|
+
except KeyError:
|
|
79
|
+
raise KeyError(name) from None
|
|
80
|
+
return tensor.detach()
|
|
81
|
+
|
|
82
|
+
def __iter__(self):
|
|
83
|
+
return iter(self._parameters())
|
|
84
|
+
|
|
85
|
+
def __len__(self):
|
|
86
|
+
return len(self._parameters())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _libraries():
|
|
90
|
+
try:
|
|
91
|
+
import torch
|
|
92
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
93
|
+
except ImportError as error:
|
|
94
|
+
raise WeightlessError("unsupported_target", "Model dependencies are unavailable") from error
|
|
95
|
+
return torch, AutoModelForCausalLM, AutoTokenizer
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _cases(cases):
|
|
99
|
+
if cases is None:
|
|
100
|
+
return []
|
|
101
|
+
if type(cases) is not list:
|
|
102
|
+
raise WeightlessError("invalid_cases", "Cases must be a list of prompt and target strings")
|
|
103
|
+
normalized = []
|
|
104
|
+
for case in cases:
|
|
105
|
+
if type(case) is not dict or set(case) != {"prompt", "target"}:
|
|
106
|
+
raise WeightlessError("invalid_cases", "Each case needs prompt and target strings")
|
|
107
|
+
prompt, target = case["prompt"], case["target"]
|
|
108
|
+
if type(prompt) is not str or type(target) is not str:
|
|
109
|
+
raise WeightlessError("invalid_cases", "Each case needs prompt and target strings")
|
|
110
|
+
normalized.append({"prompt": prompt, "target": target})
|
|
111
|
+
return normalized
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@contextlib.contextmanager
|
|
115
|
+
def _offline():
|
|
116
|
+
saved = {key: os.environ.get(key) for key in _OFFLINE}
|
|
117
|
+
for key in _OFFLINE:
|
|
118
|
+
os.environ[key] = "1"
|
|
119
|
+
try:
|
|
120
|
+
yield
|
|
121
|
+
finally:
|
|
122
|
+
for key, value in saved.items():
|
|
123
|
+
if value is None:
|
|
124
|
+
os.environ.pop(key, None)
|
|
125
|
+
else:
|
|
126
|
+
os.environ[key] = value
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _require_finite(model):
|
|
130
|
+
torch, _, _ = _libraries()
|
|
131
|
+
named = list(model.named_parameters(remove_duplicate=False))
|
|
132
|
+
named.extend(model.named_buffers(remove_duplicate=False))
|
|
133
|
+
for name, tensor in named:
|
|
134
|
+
if not tensor.dtype.is_floating_point or tensor.numel() == 0:
|
|
135
|
+
continue
|
|
136
|
+
if not bool(torch.isfinite(tensor).all().item()):
|
|
137
|
+
raise WeightlessError("nonfinite", f"Nonfinite values in {name}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _snapshot(model):
|
|
141
|
+
parameters = {}
|
|
142
|
+
groups = {}
|
|
143
|
+
for name, tensor in model.named_parameters(remove_duplicate=False):
|
|
144
|
+
if tensor.dtype != _float32():
|
|
145
|
+
raise WeightlessError("unsupported_target", f"{name} must be float32 before saving")
|
|
146
|
+
parameters[name] = tensor.detach().to(device="cpu").contiguous().clone()
|
|
147
|
+
key = (tensor.untyped_storage().data_ptr(), int(tensor.storage_offset()))
|
|
148
|
+
groups.setdefault(key, []).append(name)
|
|
149
|
+
ties = [names for names in groups.values() if len(names) > 1]
|
|
150
|
+
parameter_names = set(parameters)
|
|
151
|
+
buffers = {}
|
|
152
|
+
for name, tensor in model.state_dict().items():
|
|
153
|
+
if name in parameter_names or not _is_tensor(tensor):
|
|
154
|
+
continue
|
|
155
|
+
buffers[name] = tensor.detach().to(device="cpu").contiguous().clone()
|
|
156
|
+
return parameters, buffers, ties
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _float32():
|
|
160
|
+
torch, _, _ = _libraries()
|
|
161
|
+
return torch.float32
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _is_tensor(value):
|
|
165
|
+
torch, _, _ = _libraries()
|
|
166
|
+
return torch.is_tensor(value)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _compare(loaded, parameters, buffers, ties):
|
|
170
|
+
torch, _, _ = _libraries()
|
|
171
|
+
found = dict(loaded.named_parameters(remove_duplicate=False))
|
|
172
|
+
if set(found) != set(parameters):
|
|
173
|
+
raise WeightlessError("verification_failed", "Reloaded parameters do not match the saved model")
|
|
174
|
+
for name, expected in parameters.items():
|
|
175
|
+
actual = found[name].detach().to(device="cpu").contiguous()
|
|
176
|
+
if not torch.equal(expected, actual):
|
|
177
|
+
raise WeightlessError("verification_failed", f"Reloaded parameter {name} differs")
|
|
178
|
+
pointers = {}
|
|
179
|
+
for name, tensor in found.items():
|
|
180
|
+
pointers[name] = (tensor.data_ptr(), int(tensor.storage_offset()))
|
|
181
|
+
for names in ties:
|
|
182
|
+
if len({pointers[name] for name in names}) != 1:
|
|
183
|
+
raise WeightlessError("verification_failed", "Tied parameters were not preserved")
|
|
184
|
+
reloaded_buffers = {}
|
|
185
|
+
parameter_names = set(found)
|
|
186
|
+
for name, tensor in loaded.state_dict().items():
|
|
187
|
+
if name in parameter_names or not torch.is_tensor(tensor):
|
|
188
|
+
continue
|
|
189
|
+
reloaded_buffers[name] = tensor.detach().to(device="cpu").contiguous()
|
|
190
|
+
if set(reloaded_buffers) != set(buffers):
|
|
191
|
+
raise WeightlessError("verification_failed", "Reloaded buffers do not match the saved model")
|
|
192
|
+
for name, expected in buffers.items():
|
|
193
|
+
if not torch.equal(expected, reloaded_buffers[name]):
|
|
194
|
+
raise WeightlessError("verification_failed", f"Reloaded buffer {name} differs")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _audit(root):
|
|
198
|
+
safetensors = False
|
|
199
|
+
for path in root.rglob("*"):
|
|
200
|
+
if path.is_symlink():
|
|
201
|
+
raise WeightlessError("verification_failed", "Checkpoint staging contained a symlink")
|
|
202
|
+
if not path.is_file():
|
|
203
|
+
continue
|
|
204
|
+
if path.suffix in _UNSAFE_SUFFIXES:
|
|
205
|
+
raise WeightlessError("verification_failed", "Checkpoint staging contained an unsupported file")
|
|
206
|
+
if path.suffix == ".safetensors":
|
|
207
|
+
safetensors = True
|
|
208
|
+
if not safetensors:
|
|
209
|
+
raise WeightlessError("verification_failed", "Checkpoint staging has no safetensors weights")
|
|
210
|
+
names = {path.name for path in root.iterdir() if path.is_file()}
|
|
211
|
+
if "config.json" not in names:
|
|
212
|
+
raise WeightlessError("verification_failed", "Checkpoint staging has no model config")
|
|
213
|
+
if "tokenizer_config.json" not in names and "tokenizer.json" not in names:
|
|
214
|
+
raise WeightlessError("verification_failed", "Checkpoint staging has no tokenizer files")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _fsync_tree(root):
|
|
218
|
+
for directory, dirnames, filenames in os.walk(root):
|
|
219
|
+
dirnames.sort()
|
|
220
|
+
for name in filenames:
|
|
221
|
+
descriptor = os.open(os.path.join(directory, name), os.O_RDONLY)
|
|
222
|
+
try:
|
|
223
|
+
os.fsync(descriptor)
|
|
224
|
+
finally:
|
|
225
|
+
os.close(descriptor)
|
|
226
|
+
descriptor = os.open(directory, os.O_RDONLY)
|
|
227
|
+
try:
|
|
228
|
+
os.fsync(descriptor)
|
|
229
|
+
finally:
|
|
230
|
+
os.close(descriptor)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _discard(path):
|
|
234
|
+
if path is not None and path.exists():
|
|
235
|
+
shutil.rmtree(path)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _ids(tokenizer, text, label):
|
|
239
|
+
try:
|
|
240
|
+
values = list(tokenizer.encode(text, add_special_tokens=False))
|
|
241
|
+
except Exception as error:
|
|
242
|
+
raise WeightlessError("invalid_cases", f"{label} could not be tokenized") from error
|
|
243
|
+
if not values or any(type(item) is not int for item in values):
|
|
244
|
+
shown = text if len(text) <= 80 else text[:80]
|
|
245
|
+
raise WeightlessError("invalid_cases", f"{label} produced no tokens: {shown}")
|
|
246
|
+
return values
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class WeightModel:
|
|
250
|
+
"""Experimental handle for direct tensor edits on a local causal language model.
|
|
251
|
+
|
|
252
|
+
Evaluation reports metrics without applying an acceptance policy.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
def __init__(self, model, tokenizer):
|
|
256
|
+
self.model = model
|
|
257
|
+
self.tokenizer = tokenizer
|
|
258
|
+
self.model.eval()
|
|
259
|
+
self.model.requires_grad_(False)
|
|
260
|
+
try:
|
|
261
|
+
self.device = next(self.model.parameters()).device
|
|
262
|
+
except StopIteration as error:
|
|
263
|
+
raise WeightlessError("invalid_model", "Model has no parameters") from error
|
|
264
|
+
self.weights = _Weights(self.model)
|
|
265
|
+
|
|
266
|
+
@classmethod
|
|
267
|
+
def load(cls, local_path, device="cpu"):
|
|
268
|
+
"""Load an offline Hugging Face causal LM in float32 and eval mode.
|
|
269
|
+
|
|
270
|
+
Repository Python is not executed. Files are read only from `local_path`.
|
|
271
|
+
"""
|
|
272
|
+
torch, auto_model, auto_tokenizer = _libraries()
|
|
273
|
+
if type(device) is not str or device == "":
|
|
274
|
+
raise WeightlessError("unsupported_target", "Device must be a Torch device string")
|
|
275
|
+
try:
|
|
276
|
+
requested = torch.device(device)
|
|
277
|
+
except (TypeError, RuntimeError, ValueError) as error:
|
|
278
|
+
raise WeightlessError("unsupported_target", "Unsupported execution device") from error
|
|
279
|
+
if requested.type == "cuda" and not torch.cuda.is_available():
|
|
280
|
+
raise WeightlessError("unsupported_target", "CUDA execution is unavailable")
|
|
281
|
+
path = Path(local_path)
|
|
282
|
+
if not path.is_dir():
|
|
283
|
+
raise WeightlessError("invalid_model", "Local model snapshot could not be loaded")
|
|
284
|
+
try:
|
|
285
|
+
with _offline():
|
|
286
|
+
tokenizer = auto_tokenizer.from_pretrained(
|
|
287
|
+
path, local_files_only=True, trust_remote_code=False)
|
|
288
|
+
model = auto_model.from_pretrained(
|
|
289
|
+
path, local_files_only=True, trust_remote_code=False,
|
|
290
|
+
dtype=torch.float32, attn_implementation="eager")
|
|
291
|
+
model.to(device=requested, dtype=torch.float32)
|
|
292
|
+
except WeightlessError:
|
|
293
|
+
raise
|
|
294
|
+
except (OSError, ValueError, RuntimeError) as error:
|
|
295
|
+
text = str(error).lower()
|
|
296
|
+
if "trust_remote_code" in text or "custom code" in text:
|
|
297
|
+
raise WeightlessError("unsupported_target", "Remote model code is unsupported") from error
|
|
298
|
+
raise WeightlessError("invalid_model", "Local model snapshot could not be loaded") from error
|
|
299
|
+
loaded = cls(model, tokenizer)
|
|
300
|
+
actual = loaded.device
|
|
301
|
+
if actual.type != requested.type or (requested.index is not None and actual.index != requested.index):
|
|
302
|
+
raise WeightlessError("unsupported_target", "Model parameters are not on the requested device")
|
|
303
|
+
if any(parameter.dtype != torch.float32 for parameter in loaded.model.parameters()):
|
|
304
|
+
raise WeightlessError("unsupported_target", "Model parameters are not float32")
|
|
305
|
+
return loaded
|
|
306
|
+
|
|
307
|
+
def evaluate(self, cases):
|
|
308
|
+
"""Return JSON-ready teacher-forced loss for public prompt and target strings.
|
|
309
|
+
|
|
310
|
+
Each case loss is the mean negative log likelihood of the target string's
|
|
311
|
+
tokens, conditioned on the prompt tokens. Prompt and target are encoded
|
|
312
|
+
with special tokens disabled, and no chat template is applied. The result
|
|
313
|
+
loss is the unweighted mean of the case losses. An empty case list returns
|
|
314
|
+
a null loss.
|
|
315
|
+
"""
|
|
316
|
+
torch, _, _ = _libraries()
|
|
317
|
+
rows = []
|
|
318
|
+
normalized = _cases(cases)
|
|
319
|
+
with torch.inference_mode():
|
|
320
|
+
for case in normalized:
|
|
321
|
+
prompt_ids = _ids(self.tokenizer, case["prompt"], "Prompt")
|
|
322
|
+
target_ids = _ids(self.tokenizer, case["target"], "Target")
|
|
323
|
+
input_ids = torch.tensor([prompt_ids + target_ids], dtype=torch.long, device=self.device)
|
|
324
|
+
logits = self.model(input_ids=input_ids).logits
|
|
325
|
+
start = len(prompt_ids) - 1
|
|
326
|
+
chosen = logits[0, start:start + len(target_ids), :]
|
|
327
|
+
labels = torch.tensor(target_ids, dtype=torch.long, device=self.device)
|
|
328
|
+
loss = float(torch.nn.functional.cross_entropy(chosen, labels).item())
|
|
329
|
+
if not math.isfinite(loss):
|
|
330
|
+
raise WeightlessError("nonfinite", "Nonfinite target loss")
|
|
331
|
+
rows.append({"target_nll": loss})
|
|
332
|
+
if not rows:
|
|
333
|
+
result = {"loss": None, "cases": []}
|
|
334
|
+
else:
|
|
335
|
+
result = {"loss": sum(row["target_nll"] for row in rows) / len(rows), "cases": rows}
|
|
336
|
+
try:
|
|
337
|
+
json.dumps(result, allow_nan=False)
|
|
338
|
+
except ValueError as error:
|
|
339
|
+
raise WeightlessError("nonfinite", "Nonfinite target loss") from error
|
|
340
|
+
return result
|
|
341
|
+
|
|
342
|
+
def save(self, destination, *, on_phase=None):
|
|
343
|
+
"""Publish a Hugging Face safetensors checkpoint without overwriting `destination`.
|
|
344
|
+
|
|
345
|
+
Finite float32 parameters are written to a private staging directory, reloaded
|
|
346
|
+
independently, and compared exactly, including tied storage. The staged directory
|
|
347
|
+
is then renamed into place. Failure or interruption before that rename removes
|
|
348
|
+
the staging directory and leaves an existing destination unchanged.
|
|
349
|
+
`on_phase` receives ready, running, staging, verified, published, failure,
|
|
350
|
+
or interrupted.
|
|
351
|
+
"""
|
|
352
|
+
destination = Path(destination).absolute()
|
|
353
|
+
if destination.exists() or destination.is_symlink():
|
|
354
|
+
raise WeightlessError("export_exists", "Destination already exists")
|
|
355
|
+
parent = destination.parent
|
|
356
|
+
if not parent.is_dir():
|
|
357
|
+
raise WeightlessError("invalid_model", "Checkpoint destination parent must be a directory")
|
|
358
|
+
if callable(getattr(self.model, "is_remote_code", None)) and self.model.is_remote_code():
|
|
359
|
+
raise WeightlessError("unsupported_target", "Remote model code is unsupported")
|
|
360
|
+
life = _Lifecycle(on_phase)
|
|
361
|
+
staged = None
|
|
362
|
+
renamed = False
|
|
363
|
+
try:
|
|
364
|
+
life.advance("running")
|
|
365
|
+
_require_finite(self.model)
|
|
366
|
+
parameters, buffers, ties = _snapshot(self.model)
|
|
367
|
+
life.advance("staging")
|
|
368
|
+
staged = Path(tempfile.mkdtemp(prefix=".wsm-stage-", dir=parent))
|
|
369
|
+
self.model.save_pretrained(staged)
|
|
370
|
+
self.tokenizer.save_pretrained(staged)
|
|
371
|
+
_audit(staged)
|
|
372
|
+
reloaded = WeightModel.load(staged, device=str(self.device))
|
|
373
|
+
try:
|
|
374
|
+
_compare(reloaded.model, parameters, buffers, ties)
|
|
375
|
+
finally:
|
|
376
|
+
del reloaded
|
|
377
|
+
_fsync_tree(staged)
|
|
378
|
+
life.advance("verified")
|
|
379
|
+
from ._publication import _publish
|
|
380
|
+
_publish(staged, destination)
|
|
381
|
+
renamed = True
|
|
382
|
+
life.advance("published")
|
|
383
|
+
return {"state": "published", "destination": str(destination)}
|
|
384
|
+
except BaseException as error:
|
|
385
|
+
if staged is not None and not staged.exists() and destination.is_dir():
|
|
386
|
+
renamed = True
|
|
387
|
+
if not renamed and not life.published:
|
|
388
|
+
_discard(staged)
|
|
389
|
+
if life.state in ("running", "staging", "verified"):
|
|
390
|
+
try:
|
|
391
|
+
life.stop("interrupted" if isinstance(error, KeyboardInterrupt) else "failure")
|
|
392
|
+
except Exception:
|
|
393
|
+
pass
|
|
394
|
+
raise
|