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 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
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
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
wsm/errors.py ADDED
@@ -0,0 +1,7 @@
1
+ """Errors raised by model editing and program execution."""
2
+
3
+
4
+ class WeightlessError(ValueError):
5
+ def __init__(self, code, message):
6
+ super().__init__(message)
7
+ self.code = code