weftcodec 1.2.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.
- weftcodec/__init__.py +62 -0
- weftcodec/__main__.py +6 -0
- weftcodec/ai_modes.py +3360 -0
- weftcodec/cli.py +252 -0
- weftcodec/crc32c.py +33 -0
- weftcodec/dcp.py +2108 -0
- weftcodec/dependency.py +224 -0
- weftcodec/errors.py +119 -0
- weftcodec/frame.py +455 -0
- weftcodec/hashing.py +56 -0
- weftcodec/huffman.py +212 -0
- weftcodec/identity.py +141 -0
- weftcodec/limits.py +58 -0
- weftcodec/modes.py +718 -0
- weftcodec/orbax.py +1734 -0
- weftcodec/service.py +959 -0
- weftcodec/torch.py +94 -0
- weftcodec/varint.py +88 -0
- weftcodec/wcb.py +310 -0
- weftcodec-1.2.0.dist-info/METADATA +257 -0
- weftcodec-1.2.0.dist-info/RECORD +24 -0
- weftcodec-1.2.0.dist-info/WHEEL +5 -0
- weftcodec-1.2.0.dist-info/entry_points.txt +2 -0
- weftcodec-1.2.0.dist-info/top_level.txt +1 -0
weftcodec/dcp.py
ADDED
|
@@ -0,0 +1,2108 @@
|
|
|
1
|
+
"""PyTorch Distributed Checkpoint (DCP) storage adapter over WeftFS.
|
|
2
|
+
|
|
3
|
+
``weftcodec.dcp`` implements the shape of PyTorch's
|
|
4
|
+
:class:`torch.distributed.checkpoint.StorageWriter` and ``StorageReader``
|
|
5
|
+
interfaces so that ``torch.distributed.checkpoint.save`` / ``load`` can write
|
|
6
|
+
and read a checkpoint through a WeftFS store, with the training process never
|
|
7
|
+
needing to mount anything.
|
|
8
|
+
|
|
9
|
+
Everything durable goes through the shipped Rust CLI:
|
|
10
|
+
|
|
11
|
+
* tensor bytes become one canonical ``weft.checkpoint.v1`` checkpoint written by
|
|
12
|
+
``weftfs checkpoint write`` (one mode-10 shard object per tensor slice, or an
|
|
13
|
+
exact ``zstd``/``raw`` shard when the caller asks for it);
|
|
14
|
+
* the DCP protocol metadata (state-dict schema, storage keys, planner data) is
|
|
15
|
+
serialised as canonical WCB and stored as an exact store file object at
|
|
16
|
+
``/checkpoints/<storage-name>/dcp.metadata``;
|
|
17
|
+
* reads use ``weftfs checkpoint restore`` and re-verify every recorded BLAKE3
|
|
18
|
+
content root before handing bytes to DCP. The default restore is read-only in
|
|
19
|
+
the store; ``import_restored=True`` opts into the CLI's ``--import``, which
|
|
20
|
+
publishes a second exact copy under ``/dcp-reads/<uuid>/``.
|
|
21
|
+
|
|
22
|
+
There is no approximate path and no silent fallback. A missing CLI, a missing
|
|
23
|
+
store, a non-canonical manifest, a content-root mismatch, or an unsupported
|
|
24
|
+
dtype raises a typed error.
|
|
25
|
+
|
|
26
|
+
``torch`` is optional. Importing :mod:`weftcodec.dcp` without PyTorch works and
|
|
27
|
+
exposes the same classes/methods (contract mode); operations that need tensors
|
|
28
|
+
raise :class:`TorchRequired`. When PyTorch is importable the classes subclass the
|
|
29
|
+
real DCP interfaces.
|
|
30
|
+
|
|
31
|
+
Not claimed: GPU-direct I/O, a multi-rank cluster run, or stock DCP binaries.
|
|
32
|
+
The adapters do not need a read-only store: ``checkpoint restore`` only reads
|
|
33
|
+
unless ``--import`` is requested.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import ctypes
|
|
39
|
+
import importlib
|
|
40
|
+
import io
|
|
41
|
+
import json
|
|
42
|
+
import math
|
|
43
|
+
import os
|
|
44
|
+
import shutil
|
|
45
|
+
import struct
|
|
46
|
+
import subprocess
|
|
47
|
+
import tempfile
|
|
48
|
+
import threading
|
|
49
|
+
import time
|
|
50
|
+
import uuid
|
|
51
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
52
|
+
from dataclasses import dataclass
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
|
|
55
|
+
from . import wcb
|
|
56
|
+
from .hashing import content_root
|
|
57
|
+
from .service import (
|
|
58
|
+
CheckpointService,
|
|
59
|
+
ServiceError,
|
|
60
|
+
ServiceProtocolError,
|
|
61
|
+
ServiceUnavailable,
|
|
62
|
+
canonical_element_bytes,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
__all__ = [
|
|
66
|
+
"AsyncSaveHandle",
|
|
67
|
+
"DCP_METADATA_NAME",
|
|
68
|
+
"DCP_METADATA_SCHEMA",
|
|
69
|
+
"DcpError",
|
|
70
|
+
"ExactnessError",
|
|
71
|
+
"InvalidCheckpointId",
|
|
72
|
+
"InvalidMetadata",
|
|
73
|
+
"MetadataMissing",
|
|
74
|
+
"RestoredEntry",
|
|
75
|
+
"SaveFailed",
|
|
76
|
+
"StagingLimitExceeded",
|
|
77
|
+
"StoreCommandError",
|
|
78
|
+
"StoreUnavailable",
|
|
79
|
+
"TORCH_AVAILABLE",
|
|
80
|
+
"TORCH_VERSION",
|
|
81
|
+
"TorchRequired",
|
|
82
|
+
"UnsupportedDtype",
|
|
83
|
+
"UnsupportedStateObject",
|
|
84
|
+
"WeftStorageReader",
|
|
85
|
+
"WeftStorageWriter",
|
|
86
|
+
"init_store",
|
|
87
|
+
"save_checkpoint_async",
|
|
88
|
+
"verify_checkpoint",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
DCP_METADATA_SCHEMA = "weft.dcp.metadata.v1"
|
|
93
|
+
DCP_METADATA_NAME = "dcp.metadata"
|
|
94
|
+
DEFAULT_STAGING_BYTES = 256 * 1024 * 1024
|
|
95
|
+
DEFAULT_TIMEOUT_SECONDS = 1800.0
|
|
96
|
+
DEFAULT_CLI = "/work/.docker-cache/target-rust/release/weftfs"
|
|
97
|
+
|
|
98
|
+
#: torch dtype spelling -> (canonical WEFT-TENSOR dtype, safetensors spelling,
|
|
99
|
+
#: element bytes). Only these dtypes have an exact Rust safetensors mapping.
|
|
100
|
+
_SAFETENSORS_BY_TORCH = {
|
|
101
|
+
"torch.float16": ("f16", "F16", 2),
|
|
102
|
+
"torch.bfloat16": ("bf16", "BF16", 2),
|
|
103
|
+
"torch.float32": ("f32", "F32", 4),
|
|
104
|
+
"torch.float64": ("f64", "F64", 8),
|
|
105
|
+
"torch.int8": ("i8", "I8", 1),
|
|
106
|
+
"torch.uint8": ("u8", "U8", 1),
|
|
107
|
+
"torch.int16": ("i16", "I16", 2),
|
|
108
|
+
"torch.int32": ("i32", "I32", 4),
|
|
109
|
+
"torch.int64": ("i64", "I64", 8),
|
|
110
|
+
}
|
|
111
|
+
_CANONICAL_TO_TORCH = {
|
|
112
|
+
canonical: torch_name
|
|
113
|
+
for torch_name, (canonical, _sf, _size) in _SAFETENSORS_BY_TORCH.items()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class DcpError(RuntimeError):
|
|
118
|
+
"""Base class for every adapter error."""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class StoreUnavailable(DcpError):
|
|
122
|
+
"""The WeftFS CLI or the store it names is not usable."""
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class StoreCommandError(DcpError):
|
|
126
|
+
"""A WeftFS CLI invocation failed."""
|
|
127
|
+
|
|
128
|
+
def __init__(self, message, *, command, returncode, stderr, error_class=None):
|
|
129
|
+
super().__init__(message)
|
|
130
|
+
self.command = tuple(command)
|
|
131
|
+
self.returncode = returncode
|
|
132
|
+
self.stderr = stderr
|
|
133
|
+
self.error_class = error_class
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class InvalidCheckpointId(DcpError):
|
|
137
|
+
"""The requested checkpoint id is not a legal single store name."""
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class UnsupportedDtype(DcpError):
|
|
141
|
+
"""A tensor dtype has no exact safetensors/WEFT-TENSOR v1 mapping."""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class UnsupportedStateObject(DcpError):
|
|
145
|
+
"""The state dict contains an object this adapter does not persist."""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class StagingLimitExceeded(DcpError):
|
|
149
|
+
"""A single resolved tensor payment does not fit the staging bound."""
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class MetadataMissing(DcpError):
|
|
153
|
+
"""No DCP metadata object and manifest synthesis is disabled."""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class InvalidMetadata(DcpError):
|
|
157
|
+
"""The stored DCP metadata is malformed or refers to unknown entries."""
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class ExactnessError(DcpError):
|
|
161
|
+
"""A stored content root disagrees with the bytes/manifests."""
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class TorchRequired(DcpError):
|
|
165
|
+
"""This operation needs PyTorch but it is not installed."""
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class SaveFailed(DcpError):
|
|
169
|
+
"""A save did not produce a usable checkpoint."""
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
# Optional PyTorch. Importing ``weftcodec`` never imports this module.
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _load_torch():
|
|
177
|
+
try:
|
|
178
|
+
return importlib.import_module("torch")
|
|
179
|
+
except ImportError:
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
_TORCH = _load_torch()
|
|
184
|
+
TORCH_AVAILABLE = _TORCH is not None
|
|
185
|
+
TORCH_VERSION = getattr(_TORCH, "__version__", None) if _TORCH is not None else None
|
|
186
|
+
|
|
187
|
+
if TORCH_AVAILABLE: # pragma: no branch - both paths are exercised by the court
|
|
188
|
+
try:
|
|
189
|
+
from torch.distributed.checkpoint.metadata import (
|
|
190
|
+
BytesStorageMetadata as _BytesStorageMetadata,
|
|
191
|
+
)
|
|
192
|
+
from torch.distributed.checkpoint.metadata import (
|
|
193
|
+
ChunkStorageMetadata as _ChunkStorageMetadata,
|
|
194
|
+
)
|
|
195
|
+
from torch.distributed.checkpoint.metadata import (
|
|
196
|
+
Metadata as _TorchMetadata,
|
|
197
|
+
)
|
|
198
|
+
from torch.distributed.checkpoint.metadata import (
|
|
199
|
+
MetadataIndex as _MetadataIndex,
|
|
200
|
+
)
|
|
201
|
+
from torch.distributed.checkpoint.metadata import (
|
|
202
|
+
StorageMeta as _TorchStorageMeta,
|
|
203
|
+
)
|
|
204
|
+
from torch.distributed.checkpoint.metadata import (
|
|
205
|
+
TensorProperties as _TensorProperties,
|
|
206
|
+
)
|
|
207
|
+
from torch.distributed.checkpoint.metadata import (
|
|
208
|
+
TensorStorageMetadata as _TensorStorageMetadata,
|
|
209
|
+
)
|
|
210
|
+
from torch.distributed.checkpoint.storage import (
|
|
211
|
+
StorageReader as _StorageReaderBase,
|
|
212
|
+
)
|
|
213
|
+
from torch.distributed.checkpoint.storage import (
|
|
214
|
+
StorageWriter as _StorageWriterBase,
|
|
215
|
+
)
|
|
216
|
+
from torch.futures import Future as _TorchFuture
|
|
217
|
+
|
|
218
|
+
_TORCH_IMPORT_ERROR = None
|
|
219
|
+
except ImportError as exc: # pragma: no cover - defensive for odd builds
|
|
220
|
+
TORCH_AVAILABLE = False
|
|
221
|
+
TORCH_VERSION = None
|
|
222
|
+
_TORCH = None
|
|
223
|
+
_TORCH_IMPORT_ERROR = str(exc)
|
|
224
|
+
else: # pragma: no cover - the court always has torch
|
|
225
|
+
_TORCH_IMPORT_ERROR = "torch is not installed"
|
|
226
|
+
|
|
227
|
+
if not TORCH_AVAILABLE:
|
|
228
|
+
|
|
229
|
+
class _StorageWriterBase: # type: ignore[no-redef]
|
|
230
|
+
"""Contract-mode stand-in when PyTorch is absent."""
|
|
231
|
+
|
|
232
|
+
class _StorageReaderBase: # type: ignore[no-redef]
|
|
233
|
+
"""Contract-mode stand-in when PyTorch is absent."""
|
|
234
|
+
|
|
235
|
+
class _TorchFuture: # type: ignore[no-redef]
|
|
236
|
+
"""Minimal future with the subset of ``torch.futures.Future`` used here."""
|
|
237
|
+
|
|
238
|
+
def __init__(self):
|
|
239
|
+
self._ready = threading.Event()
|
|
240
|
+
self._value = None
|
|
241
|
+
self._error = None
|
|
242
|
+
|
|
243
|
+
def set_result(self, value):
|
|
244
|
+
self._value = value
|
|
245
|
+
self._ready.set()
|
|
246
|
+
|
|
247
|
+
def set_exception(self, error):
|
|
248
|
+
self._error = error
|
|
249
|
+
self._ready.set()
|
|
250
|
+
|
|
251
|
+
def wait(self):
|
|
252
|
+
self._ready.wait()
|
|
253
|
+
return self
|
|
254
|
+
|
|
255
|
+
def value(self):
|
|
256
|
+
self._ready.wait()
|
|
257
|
+
if self._error is not None:
|
|
258
|
+
raise self._error
|
|
259
|
+
return self._value
|
|
260
|
+
|
|
261
|
+
def done(self):
|
|
262
|
+
return self._ready.is_set()
|
|
263
|
+
|
|
264
|
+
def add_done_callback(self, callback):
|
|
265
|
+
self._ready.wait()
|
|
266
|
+
callback(self)
|
|
267
|
+
|
|
268
|
+
@dataclass(frozen=True)
|
|
269
|
+
class _FallbackStorageMeta: # type: ignore[no-redef]
|
|
270
|
+
checkpoint_id: str = None
|
|
271
|
+
save_id: str = None
|
|
272
|
+
load_id: str = None
|
|
273
|
+
modules: tuple = ()
|
|
274
|
+
|
|
275
|
+
_TorchStorageMeta = _FallbackStorageMeta
|
|
276
|
+
_MetadataIndex = None
|
|
277
|
+
_TorchMetadata = None
|
|
278
|
+
_TensorStorageMetadata = None
|
|
279
|
+
_BytesStorageMetadata = None
|
|
280
|
+
_ChunkStorageMetadata = None
|
|
281
|
+
_TensorProperties = None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _require_torch(operation):
|
|
285
|
+
if not TORCH_AVAILABLE:
|
|
286
|
+
raise TorchRequired(
|
|
287
|
+
"%s requires PyTorch (install with `pip install weftcodec[torch]`); "
|
|
288
|
+
"weftcodec.dcp imported in contract mode: %s"
|
|
289
|
+
% (operation, _TORCH_IMPORT_ERROR)
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
# Small canonical helpers
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _u(value):
|
|
298
|
+
return wcb.U64(int(value))
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _i(value):
|
|
302
|
+
if isinstance(value, wcb.U64):
|
|
303
|
+
return int(value.value)
|
|
304
|
+
if isinstance(value, wcb.I64):
|
|
305
|
+
return int(value.value)
|
|
306
|
+
return int(value)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _u_list(values):
|
|
310
|
+
return [_u(value) for value in values]
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _i_list(values):
|
|
314
|
+
return tuple(_i(value) for value in values)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _dtype_name(dtype) -> str:
|
|
318
|
+
"""Canonical torch dtype spelling, e.g. ``torch.float32``."""
|
|
319
|
+
text = str(dtype)
|
|
320
|
+
if not text.startswith("torch."):
|
|
321
|
+
raise UnsupportedDtype("unrecognised torch dtype %r" % (dtype,))
|
|
322
|
+
return text
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _canonical_dtype(torch_dtype_name: str) -> str:
|
|
326
|
+
try:
|
|
327
|
+
return _SAFETENSORS_BY_TORCH[torch_dtype_name][0]
|
|
328
|
+
except KeyError:
|
|
329
|
+
raise UnsupportedDtype(
|
|
330
|
+
"dtype %s has no exact WEFT-TENSOR/safetensors v1 mapping; "
|
|
331
|
+
"supported: %s"
|
|
332
|
+
% (torch_dtype_name, ", ".join(sorted(_SAFETENSORS_BY_TORCH)))
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _safetensors_dtype(torch_dtype_name: str) -> str:
|
|
337
|
+
try:
|
|
338
|
+
return _SAFETENSORS_BY_TORCH[torch_dtype_name][1]
|
|
339
|
+
except KeyError:
|
|
340
|
+
raise UnsupportedDtype(
|
|
341
|
+
"dtype %s has no exact safetensors mapping; supported: %s"
|
|
342
|
+
% (torch_dtype_name, ", ".join(sorted(_SAFETENSORS_BY_TORCH)))
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _element_bytes(torch_dtype_name: str) -> int:
|
|
347
|
+
try:
|
|
348
|
+
return _SAFETENSORS_BY_TORCH[torch_dtype_name][2]
|
|
349
|
+
except KeyError:
|
|
350
|
+
raise UnsupportedDtype("dtype %s has no element size" % torch_dtype_name)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _torch_dtype(torch_dtype_name: str):
|
|
354
|
+
_require_torch("resolving a tensor dtype")
|
|
355
|
+
try:
|
|
356
|
+
return getattr(_TORCH, torch_dtype_name.split(".", 1)[1])
|
|
357
|
+
except AttributeError:
|
|
358
|
+
raise UnsupportedDtype("this PyTorch has no dtype %s" % torch_dtype_name)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _validate_checkpoint_id(checkpoint_id) -> str:
|
|
362
|
+
if checkpoint_id is None:
|
|
363
|
+
raise InvalidCheckpointId("checkpoint id is required")
|
|
364
|
+
text = os.fspath(checkpoint_id)
|
|
365
|
+
if not isinstance(text, str) or not text:
|
|
366
|
+
raise InvalidCheckpointId("checkpoint id must be a non-empty path name")
|
|
367
|
+
if "/" in text or "\\" in text or text in (".", "..") or "\x00" in text:
|
|
368
|
+
raise InvalidCheckpointId(
|
|
369
|
+
"checkpoint id %r must be a single store name; the store directory is "
|
|
370
|
+
"selected with store_root" % text
|
|
371
|
+
)
|
|
372
|
+
return text
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _generation(name: str, base: str):
|
|
376
|
+
if name == base:
|
|
377
|
+
return 1
|
|
378
|
+
prefix = base + ".v"
|
|
379
|
+
if name.startswith(prefix):
|
|
380
|
+
suffix = name[len(prefix) :]
|
|
381
|
+
if suffix.isdigit() and str(int(suffix)) == suffix and int(suffix) >= 2:
|
|
382
|
+
return int(suffix)
|
|
383
|
+
return None
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _parse_topology(topology, axis=None):
|
|
387
|
+
if isinstance(topology, (tuple, list)):
|
|
388
|
+
if len(topology) != 2:
|
|
389
|
+
raise ValueError("topology must be an int or a (shards, axis) pair")
|
|
390
|
+
shards, explicit_axis = topology
|
|
391
|
+
if axis is not None and int(axis) != int(explicit_axis):
|
|
392
|
+
raise ValueError("topology axis and the axis argument disagree")
|
|
393
|
+
axis = explicit_axis
|
|
394
|
+
else:
|
|
395
|
+
shards = topology
|
|
396
|
+
shards = int(shards)
|
|
397
|
+
axis = 0 if axis is None else int(axis)
|
|
398
|
+
if shards < 1:
|
|
399
|
+
raise ValueError("topology shard count must be >= 1")
|
|
400
|
+
if axis < 0 or axis > 0xFFFFFFFF:
|
|
401
|
+
raise ValueError("topology axis must be a u32")
|
|
402
|
+
return shards, axis
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _default_cli(cli=None):
|
|
406
|
+
if cli:
|
|
407
|
+
return os.fspath(cli)
|
|
408
|
+
env = os.environ.get("WEFTFS_CLI")
|
|
409
|
+
if env:
|
|
410
|
+
return env
|
|
411
|
+
if os.path.isfile(DEFAULT_CLI) and os.access(DEFAULT_CLI, os.X_OK):
|
|
412
|
+
return DEFAULT_CLI
|
|
413
|
+
found = shutil.which("weftfs")
|
|
414
|
+
if found:
|
|
415
|
+
return found
|
|
416
|
+
raise StoreUnavailable(
|
|
417
|
+
"no weftfs CLI found: set WEFTFS_CLI or pass weftfs_cli=... (looked at "
|
|
418
|
+
"%s and PATH)" % DEFAULT_CLI
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _parse_error_class(stderr: str):
|
|
423
|
+
first = stderr.strip().splitlines()[0] if stderr.strip() else ""
|
|
424
|
+
if ":" in first:
|
|
425
|
+
head = first.split(":", 1)[0].strip()
|
|
426
|
+
if head and head.replace("_", "").isalnum() and head[0].isupper():
|
|
427
|
+
return head
|
|
428
|
+
return None
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
class _Storage:
|
|
432
|
+
"""Thin, fail-closed wrapper around the ``weftfs`` CLI."""
|
|
433
|
+
|
|
434
|
+
def __init__(self, store_root, *, cli=None, timeout=DEFAULT_TIMEOUT_SECONDS):
|
|
435
|
+
self.root = Path(store_root)
|
|
436
|
+
self.cli = _default_cli(cli)
|
|
437
|
+
self.timeout = float(timeout)
|
|
438
|
+
self._lock = threading.Lock()
|
|
439
|
+
self.command_ms = 0.0
|
|
440
|
+
self.calls = 0
|
|
441
|
+
|
|
442
|
+
# -- process plumbing ---------------------------------------------------
|
|
443
|
+
|
|
444
|
+
def _run(self, args, *, binary=False):
|
|
445
|
+
command = [self.cli, "--store", os.fspath(self.root), *[str(a) for a in args]]
|
|
446
|
+
started = time.monotonic()
|
|
447
|
+
try:
|
|
448
|
+
proc = subprocess.run(
|
|
449
|
+
command,
|
|
450
|
+
capture_output=True,
|
|
451
|
+
timeout=self.timeout,
|
|
452
|
+
check=False,
|
|
453
|
+
)
|
|
454
|
+
except FileNotFoundError as exc:
|
|
455
|
+
raise StoreUnavailable("weftfs CLI not executable: %s" % exc) from exc
|
|
456
|
+
except subprocess.TimeoutExpired as exc:
|
|
457
|
+
raise StoreCommandError(
|
|
458
|
+
"weftfs command timed out after %.0fs: %s"
|
|
459
|
+
% (self.timeout, " ".join(command)),
|
|
460
|
+
command=command,
|
|
461
|
+
returncode=-1,
|
|
462
|
+
stderr="",
|
|
463
|
+
error_class="Timeout",
|
|
464
|
+
) from exc
|
|
465
|
+
elapsed = (time.monotonic() - started) * 1000.0
|
|
466
|
+
with self._lock:
|
|
467
|
+
self.command_ms += elapsed
|
|
468
|
+
self.calls += 1
|
|
469
|
+
if proc.returncode != 0:
|
|
470
|
+
stderr = proc.stderr.decode("utf-8", "replace").strip()
|
|
471
|
+
error_class = _parse_error_class(stderr)
|
|
472
|
+
raise StoreCommandError(
|
|
473
|
+
"weftfs %s failed (%d): %s"
|
|
474
|
+
% (" ".join(str(a) for a in args), proc.returncode, stderr),
|
|
475
|
+
command=command,
|
|
476
|
+
returncode=proc.returncode,
|
|
477
|
+
stderr=stderr,
|
|
478
|
+
error_class=error_class,
|
|
479
|
+
)
|
|
480
|
+
return proc
|
|
481
|
+
|
|
482
|
+
def _json(self, args):
|
|
483
|
+
proc = self._run([*args, "--json"])
|
|
484
|
+
text = proc.stdout.decode("utf-8")
|
|
485
|
+
try:
|
|
486
|
+
return json.loads(text)
|
|
487
|
+
except ValueError as exc:
|
|
488
|
+
raise StoreCommandError(
|
|
489
|
+
"weftfs %s produced invalid JSON: %s"
|
|
490
|
+
% (" ".join(str(a) for a in args), text[:400]),
|
|
491
|
+
command=[self.cli, "--store", os.fspath(self.root), *args],
|
|
492
|
+
returncode=0,
|
|
493
|
+
stderr=text[:400],
|
|
494
|
+
error_class="InvalidJson",
|
|
495
|
+
) from exc
|
|
496
|
+
|
|
497
|
+
# -- store verbs --------------------------------------------------------
|
|
498
|
+
|
|
499
|
+
def requires_store(self):
|
|
500
|
+
if not (self.root / "FORMAT").is_file():
|
|
501
|
+
raise StoreUnavailable(
|
|
502
|
+
"no WeftFS store at %s (missing FORMAT); call init_store() first"
|
|
503
|
+
% self.root
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
def init(self):
|
|
507
|
+
if (self.root / "FORMAT").is_file():
|
|
508
|
+
return
|
|
509
|
+
self._run(["init"])
|
|
510
|
+
|
|
511
|
+
def list_checkpoints(self):
|
|
512
|
+
payload = self._json(["checkpoint", "list"])
|
|
513
|
+
return [row["name"] for row in payload.get("checkpoints", [])]
|
|
514
|
+
|
|
515
|
+
def checkpoint_show(self, name):
|
|
516
|
+
payload = self._json(["checkpoint", "show", "--name", name])
|
|
517
|
+
manifest = payload.get("manifest")
|
|
518
|
+
if not isinstance(manifest, dict):
|
|
519
|
+
raise InvalidMetadata("checkpoint show --json has no manifest object")
|
|
520
|
+
return payload
|
|
521
|
+
|
|
522
|
+
def checkpoint_write(self, staging, name, *, shards, axis, strategy):
|
|
523
|
+
args = [
|
|
524
|
+
"checkpoint",
|
|
525
|
+
"write",
|
|
526
|
+
"--file",
|
|
527
|
+
os.fspath(staging),
|
|
528
|
+
"--name",
|
|
529
|
+
name,
|
|
530
|
+
"--shards",
|
|
531
|
+
str(shards),
|
|
532
|
+
"--axis",
|
|
533
|
+
str(axis),
|
|
534
|
+
]
|
|
535
|
+
if strategy:
|
|
536
|
+
args += ["--shard-strategy", strategy]
|
|
537
|
+
return self._json(args)
|
|
538
|
+
|
|
539
|
+
def restore(self, name, *, entry=None, topology=None, axis=0, host_out, import_restored=False):
|
|
540
|
+
"""Restore to ``host_out``; ``import_restored`` opts into a store copy.
|
|
541
|
+
|
|
542
|
+
``checkpoint restore`` is read-only in the store by default. Passing
|
|
543
|
+
``import_restored=True`` adds ``--import`` and publishes the exact
|
|
544
|
+
restored container under ``/dcp-reads/<uuid>/``, which is a real,
|
|
545
|
+
durable second copy and therefore opt-in.
|
|
546
|
+
"""
|
|
547
|
+
args = [
|
|
548
|
+
"checkpoint",
|
|
549
|
+
"restore",
|
|
550
|
+
"--name",
|
|
551
|
+
name,
|
|
552
|
+
"--host-out",
|
|
553
|
+
os.fspath(host_out),
|
|
554
|
+
]
|
|
555
|
+
receipt = os.environ.get("WEFT_LOAD_RECEIPT")
|
|
556
|
+
if receipt:
|
|
557
|
+
args += [
|
|
558
|
+
"--load-receipt",
|
|
559
|
+
receipt,
|
|
560
|
+
"--verify-load",
|
|
561
|
+
os.environ.get("WEFT_VERIFY_LOAD", "lazy"),
|
|
562
|
+
"--replica",
|
|
563
|
+
os.environ.get("WEFT_REPLICA", "local"),
|
|
564
|
+
]
|
|
565
|
+
if import_restored:
|
|
566
|
+
out_dir = "/dcp-reads/%s" % uuid.uuid4().hex
|
|
567
|
+
args += ["--import", "--out", out_dir]
|
|
568
|
+
if entry is not None:
|
|
569
|
+
args += ["--tensor", entry]
|
|
570
|
+
if topology is not None:
|
|
571
|
+
args += ["--topology", str(topology), "--axis", str(axis)]
|
|
572
|
+
return self._json(args)
|
|
573
|
+
|
|
574
|
+
def put_file(self, store_path, payload: bytes):
|
|
575
|
+
with tempfile.NamedTemporaryFile(
|
|
576
|
+
prefix="weft-dcp-put-", suffix=".bin", delete=False
|
|
577
|
+
) as handle:
|
|
578
|
+
handle.write(payload)
|
|
579
|
+
host_path = handle.name
|
|
580
|
+
try:
|
|
581
|
+
return self._json(["create", store_path, "--file", host_path])
|
|
582
|
+
finally:
|
|
583
|
+
try:
|
|
584
|
+
os.unlink(host_path)
|
|
585
|
+
except OSError:
|
|
586
|
+
pass
|
|
587
|
+
|
|
588
|
+
def get_file(self, store_path):
|
|
589
|
+
"""Exact bytes of a store file object."""
|
|
590
|
+
proc = self._run(["cat", store_path], binary=True)
|
|
591
|
+
return proc.stdout
|
|
592
|
+
|
|
593
|
+
def file_exists(self, store_path):
|
|
594
|
+
try:
|
|
595
|
+
self._run(["stat", store_path])
|
|
596
|
+
return True
|
|
597
|
+
except StoreCommandError as exc:
|
|
598
|
+
if exc.error_class in ("NotFound",):
|
|
599
|
+
return False
|
|
600
|
+
raise
|
|
601
|
+
|
|
602
|
+
def verify_full(self):
|
|
603
|
+
return self._json(["verify", "--full"])
|
|
604
|
+
|
|
605
|
+
def checkpoint_verify(self, name):
|
|
606
|
+
return self._json(["checkpoint", "verify", "--name", name])
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
# ---------------------------------------------------------------------------
|
|
610
|
+
# DCP metadata schema (canonical WCB)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
@dataclass(frozen=True)
|
|
614
|
+
class StorageInfo:
|
|
615
|
+
"""Where one DCP storage index lives inside the checkpoint container."""
|
|
616
|
+
|
|
617
|
+
entry: str
|
|
618
|
+
dtype: str # torch dtype spelling
|
|
619
|
+
shape: tuple
|
|
620
|
+
storage_offsets: tuple
|
|
621
|
+
lengths: tuple
|
|
622
|
+
content_root: bytes
|
|
623
|
+
|
|
624
|
+
def to_value(self):
|
|
625
|
+
return {
|
|
626
|
+
"entry": self.entry,
|
|
627
|
+
"dtype": self.dtype,
|
|
628
|
+
"shape": _u_list(self.shape),
|
|
629
|
+
"storage_offsets": _u_list(self.storage_offsets),
|
|
630
|
+
"lengths": _u_list(self.lengths),
|
|
631
|
+
"content_root": self.content_root,
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
@classmethod
|
|
635
|
+
def from_value(cls, value):
|
|
636
|
+
try:
|
|
637
|
+
return cls(
|
|
638
|
+
entry=value["entry"],
|
|
639
|
+
dtype=value["dtype"],
|
|
640
|
+
shape=_i_list(value["shape"]),
|
|
641
|
+
storage_offsets=_i_list(value.get("storage_offsets", [])),
|
|
642
|
+
lengths=_i_list(value.get("lengths", [])),
|
|
643
|
+
content_root=bytes(value["content_root"]),
|
|
644
|
+
)
|
|
645
|
+
except (KeyError, TypeError) as exc:
|
|
646
|
+
raise InvalidMetadata("bad storage entry: %s" % exc) from exc
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
@dataclass(frozen=True)
|
|
650
|
+
class RestoredEntry:
|
|
651
|
+
"""One entry restored (and re-verified) through the Rust CLI."""
|
|
652
|
+
|
|
653
|
+
entry: str
|
|
654
|
+
data: bytes
|
|
655
|
+
dtype: str
|
|
656
|
+
shape: tuple
|
|
657
|
+
content_root: bytes
|
|
658
|
+
topology_root: "str | None"
|
|
659
|
+
shards_read: int
|
|
660
|
+
store_bytes_read: int
|
|
661
|
+
|
|
662
|
+
@property
|
|
663
|
+
def content_root_hex(self) -> str:
|
|
664
|
+
return self.content_root.hex()
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _state_dict_value(state_dict_metadata):
|
|
668
|
+
rows = []
|
|
669
|
+
for fqn in sorted(state_dict_metadata):
|
|
670
|
+
item = state_dict_metadata[fqn]
|
|
671
|
+
if _TensorStorageMetadata is not None and isinstance(
|
|
672
|
+
item, _TensorStorageMetadata
|
|
673
|
+
):
|
|
674
|
+
rows.append(
|
|
675
|
+
{
|
|
676
|
+
"fqn": fqn,
|
|
677
|
+
"kind": "tensor",
|
|
678
|
+
"dtype": _dtype_name(item.properties.dtype),
|
|
679
|
+
"size": _u_list(item.size),
|
|
680
|
+
"chunks": [
|
|
681
|
+
{
|
|
682
|
+
"offsets": _u_list(chunk.offsets),
|
|
683
|
+
"sizes": _u_list(chunk.sizes),
|
|
684
|
+
}
|
|
685
|
+
for chunk in item.chunks
|
|
686
|
+
],
|
|
687
|
+
}
|
|
688
|
+
)
|
|
689
|
+
elif _BytesStorageMetadata is not None and isinstance(
|
|
690
|
+
item, _BytesStorageMetadata
|
|
691
|
+
):
|
|
692
|
+
rows.append({"fqn": fqn, "kind": "bytes"})
|
|
693
|
+
else:
|
|
694
|
+
raise UnsupportedStateObject(
|
|
695
|
+
"state dict entry %r has storage metadata %s; this adapter "
|
|
696
|
+
"persists plain tensors and byte objects only"
|
|
697
|
+
% (fqn, type(item).__name__)
|
|
698
|
+
)
|
|
699
|
+
return rows
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _planner_data_value(planner_data):
|
|
703
|
+
if planner_data is None:
|
|
704
|
+
return {"kind": "none"}
|
|
705
|
+
if isinstance(planner_data, dict) and all(
|
|
706
|
+
isinstance(key, str)
|
|
707
|
+
and isinstance(value, (tuple, list))
|
|
708
|
+
and all(isinstance(part, str) for part in value)
|
|
709
|
+
for key, value in planner_data.items()
|
|
710
|
+
):
|
|
711
|
+
return {
|
|
712
|
+
"kind": "dict_str_tuple",
|
|
713
|
+
"entries": [
|
|
714
|
+
{"key": key, "value": list(planner_data[key])}
|
|
715
|
+
for key in sorted(planner_data)
|
|
716
|
+
],
|
|
717
|
+
}
|
|
718
|
+
raise UnsupportedStateObject(
|
|
719
|
+
"planner_data of type %s is not persistable by this adapter; only None "
|
|
720
|
+
"and dict[str, tuple[str, ...]] are supported" % type(planner_data).__name__
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def serialize_metadata(metadata, *, checkpoint_id, storage_name) -> bytes:
|
|
725
|
+
"""Canonical WCB bytes for a DCP ``Metadata`` object."""
|
|
726
|
+
storage_rows = []
|
|
727
|
+
storage_data = metadata.storage_data
|
|
728
|
+
if storage_data is None:
|
|
729
|
+
raise InvalidMetadata("metadata.storage_data is None; write_data/finish "
|
|
730
|
+
"must run before metadata is serialised")
|
|
731
|
+
for key in sorted(storage_data, key=lambda k: (k.fqn, tuple(k.offset or ()))):
|
|
732
|
+
info = storage_data[key]
|
|
733
|
+
if not isinstance(info, StorageInfo):
|
|
734
|
+
raise InvalidMetadata(
|
|
735
|
+
"storage_data[%r] is %s, expected StorageInfo"
|
|
736
|
+
% (key, type(info).__name__)
|
|
737
|
+
)
|
|
738
|
+
storage_rows.append(
|
|
739
|
+
{
|
|
740
|
+
"fqn": key.fqn,
|
|
741
|
+
"offset": None if key.offset is None else _u_list(key.offset),
|
|
742
|
+
"index": None if key.index is None else _u(key.index),
|
|
743
|
+
"info": info.to_value(),
|
|
744
|
+
}
|
|
745
|
+
)
|
|
746
|
+
version = getattr(metadata, "version", None)
|
|
747
|
+
value = {
|
|
748
|
+
"schema": DCP_METADATA_SCHEMA,
|
|
749
|
+
"checkpoint_id": checkpoint_id,
|
|
750
|
+
"storage_name": storage_name,
|
|
751
|
+
"dcp_version": None if version is None else str(version),
|
|
752
|
+
"state_dict": _state_dict_value(metadata.state_dict_metadata),
|
|
753
|
+
"storage_data": storage_rows,
|
|
754
|
+
"planner_data": _planner_data_value(getattr(metadata, "planner_data", None)),
|
|
755
|
+
}
|
|
756
|
+
return wcb.encode(value)
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def _metadata_from_value(value, *, storage_name):
|
|
760
|
+
_require_torch("reconstructing DCP metadata")
|
|
761
|
+
if not isinstance(value, dict) or value.get("schema") != DCP_METADATA_SCHEMA:
|
|
762
|
+
raise InvalidMetadata(
|
|
763
|
+
"DCP metadata schema is %r, expected %r"
|
|
764
|
+
% (
|
|
765
|
+
value.get("schema") if isinstance(value, dict) else None,
|
|
766
|
+
DCP_METADATA_SCHEMA,
|
|
767
|
+
)
|
|
768
|
+
)
|
|
769
|
+
if value.get("storage_name") != storage_name:
|
|
770
|
+
raise InvalidMetadata(
|
|
771
|
+
"DCP metadata names storage %r but the resolved checkpoint is %r"
|
|
772
|
+
% (value.get("storage_name"), storage_name)
|
|
773
|
+
)
|
|
774
|
+
state_dict_metadata = {}
|
|
775
|
+
for row in value.get("state_dict", []):
|
|
776
|
+
fqn = row.get("fqn")
|
|
777
|
+
kind = row.get("kind")
|
|
778
|
+
if kind == "tensor":
|
|
779
|
+
dtype_name = row.get("dtype")
|
|
780
|
+
if dtype_name not in _SAFETENSORS_BY_TORCH:
|
|
781
|
+
raise UnsupportedDtype(
|
|
782
|
+
"stored tensor %r has unsupported dtype %r" % (fqn, dtype_name)
|
|
783
|
+
)
|
|
784
|
+
state_dict_metadata[fqn] = _TensorStorageMetadata(
|
|
785
|
+
properties=_TensorProperties(dtype=_torch_dtype(dtype_name)),
|
|
786
|
+
size=_TORCH.Size(_i_list(row.get("size", []))),
|
|
787
|
+
chunks=[
|
|
788
|
+
_ChunkStorageMetadata(
|
|
789
|
+
offsets=_TORCH.Size(_i_list(chunk.get("offsets", []))),
|
|
790
|
+
sizes=_TORCH.Size(_i_list(chunk.get("sizes", []))),
|
|
791
|
+
)
|
|
792
|
+
for chunk in row.get("chunks", [])
|
|
793
|
+
],
|
|
794
|
+
)
|
|
795
|
+
elif kind == "bytes":
|
|
796
|
+
state_dict_metadata[fqn] = _BytesStorageMetadata()
|
|
797
|
+
else:
|
|
798
|
+
raise InvalidMetadata("unknown state-dict entry kind %r" % (kind,))
|
|
799
|
+
|
|
800
|
+
storage_data = {}
|
|
801
|
+
for row in value.get("storage_data", []):
|
|
802
|
+
info = StorageInfo.from_value(row.get("info", {}))
|
|
803
|
+
offset = row.get("offset")
|
|
804
|
+
index = row.get("index")
|
|
805
|
+
key = _MetadataIndex(
|
|
806
|
+
row.get("fqn"),
|
|
807
|
+
offset=None if offset is None else _i_list(offset),
|
|
808
|
+
index=None if index is None else _i(index),
|
|
809
|
+
)
|
|
810
|
+
storage_data[key] = info
|
|
811
|
+
|
|
812
|
+
planner = value.get("planner_data") or {"kind": "none"}
|
|
813
|
+
if planner.get("kind") == "none":
|
|
814
|
+
planner_data = None
|
|
815
|
+
elif planner.get("kind") == "dict_str_tuple":
|
|
816
|
+
planner_data = {
|
|
817
|
+
entry["key"]: tuple(entry["value"]) for entry in planner.get("entries", [])
|
|
818
|
+
}
|
|
819
|
+
else:
|
|
820
|
+
raise InvalidMetadata("unknown planner_data kind %r" % (planner.get("kind"),))
|
|
821
|
+
|
|
822
|
+
return _TorchMetadata(
|
|
823
|
+
state_dict_metadata=state_dict_metadata,
|
|
824
|
+
planner_data=planner_data,
|
|
825
|
+
storage_data=storage_data,
|
|
826
|
+
storage_meta=_TorchStorageMeta(
|
|
827
|
+
checkpoint_id=value.get("checkpoint_id"),
|
|
828
|
+
save_id=value.get("storage_name"),
|
|
829
|
+
),
|
|
830
|
+
version=value.get("dcp_version"),
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
def _metadata_from_manifest(show, *, checkpoint_id, storage_name):
|
|
835
|
+
"""Build DCP metadata for a checkpoint written by the Rust CLI alone.
|
|
836
|
+
|
|
837
|
+
Every manifest tensor becomes a state-dict key of the same name with one
|
|
838
|
+
full-shape chunk. This is exact for plain safetensors checkpoints; it is
|
|
839
|
+
reported as ``metadata_source == "manifest"`` so callers can tell it apart
|
|
840
|
+
from adapter-written checkpoints that carry DCP metadata.
|
|
841
|
+
"""
|
|
842
|
+
_require_torch("synthesising DCP metadata from a manifest")
|
|
843
|
+
manifest = show["manifest"]
|
|
844
|
+
state_dict_metadata = {}
|
|
845
|
+
storage_data = {}
|
|
846
|
+
for tensor in manifest.get("tensors", []):
|
|
847
|
+
name = tensor["name"]
|
|
848
|
+
canonical = tensor["dtype"]
|
|
849
|
+
torch_name = _CANONICAL_TO_TORCH.get(canonical)
|
|
850
|
+
if torch_name is None:
|
|
851
|
+
raise UnsupportedDtype(
|
|
852
|
+
"manifest tensor %r has dtype %r with no exact torch dtype"
|
|
853
|
+
% (name, canonical)
|
|
854
|
+
)
|
|
855
|
+
shape = tuple(int(x) for x in tensor["shape"])
|
|
856
|
+
storage_offsets = tuple([0] * len(shape))
|
|
857
|
+
element_bytes = int(tensor["element_bytes"])
|
|
858
|
+
expected = 1
|
|
859
|
+
for extent in shape:
|
|
860
|
+
expected *= extent
|
|
861
|
+
expected *= _element_bytes(torch_name)
|
|
862
|
+
if expected != element_bytes:
|
|
863
|
+
raise InvalidMetadata(
|
|
864
|
+
"manifest tensor %r element_bytes %d != shape product %d"
|
|
865
|
+
% (name, element_bytes, expected)
|
|
866
|
+
)
|
|
867
|
+
key = _MetadataIndex(name, offset=storage_offsets)
|
|
868
|
+
state_dict_metadata[name] = _TensorStorageMetadata(
|
|
869
|
+
properties=_TensorProperties(dtype=_torch_dtype(torch_name)),
|
|
870
|
+
size=_TORCH.Size(shape),
|
|
871
|
+
chunks=[
|
|
872
|
+
_ChunkStorageMetadata(
|
|
873
|
+
offsets=_TORCH.Size([0] * len(shape)),
|
|
874
|
+
sizes=_TORCH.Size(shape),
|
|
875
|
+
)
|
|
876
|
+
],
|
|
877
|
+
)
|
|
878
|
+
storage_data[key] = StorageInfo(
|
|
879
|
+
entry=name,
|
|
880
|
+
dtype=torch_name,
|
|
881
|
+
shape=shape,
|
|
882
|
+
storage_offsets=storage_offsets,
|
|
883
|
+
lengths=shape,
|
|
884
|
+
content_root=bytes.fromhex(tensor["content_root"]),
|
|
885
|
+
)
|
|
886
|
+
return _TorchMetadata(
|
|
887
|
+
state_dict_metadata=state_dict_metadata,
|
|
888
|
+
planner_data=None,
|
|
889
|
+
storage_data=storage_data,
|
|
890
|
+
storage_meta=_TorchStorageMeta(
|
|
891
|
+
checkpoint_id=checkpoint_id, save_id=storage_name
|
|
892
|
+
),
|
|
893
|
+
version="1.0.0",
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
# ---------------------------------------------------------------------------
|
|
898
|
+
# safetensors helpers (writing the staging container, reading restored bytes)
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def _tensor_bytes(tensor):
|
|
902
|
+
"""Exact little-endian row-major bytes of a CPU tensor, without numpy.
|
|
903
|
+
|
|
904
|
+
``data_ptr()`` addresses the tensor's first logical element, so a single
|
|
905
|
+
``ctypes.string_at`` copies exactly ``numel * element_size`` bytes for a
|
|
906
|
+
contiguous tensor, including dtypes numpy cannot represent (bfloat16,
|
|
907
|
+
float8). This is a host-memory copy, not a zero-copy path.
|
|
908
|
+
"""
|
|
909
|
+
_require_torch("serialising a tensor")
|
|
910
|
+
t = tensor.detach()
|
|
911
|
+
if t.device.type != "cpu":
|
|
912
|
+
t = t.cpu()
|
|
913
|
+
if t.numel() == 0:
|
|
914
|
+
return b""
|
|
915
|
+
t = t.contiguous()
|
|
916
|
+
expected = t.numel() * t.element_size()
|
|
917
|
+
payload = ctypes.string_at(t.data_ptr(), expected)
|
|
918
|
+
if len(payload) != expected:
|
|
919
|
+
raise ExactnessError(
|
|
920
|
+
"tensor byte extraction produced %d bytes, expected %d"
|
|
921
|
+
% (len(payload), expected)
|
|
922
|
+
)
|
|
923
|
+
return payload
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def _narrow(tensor, offsets, lengths):
|
|
927
|
+
if not offsets:
|
|
928
|
+
return tensor
|
|
929
|
+
slices = tuple(
|
|
930
|
+
slice(int(offset), int(offset) + int(length))
|
|
931
|
+
for offset, length in zip(offsets, lengths)
|
|
932
|
+
)
|
|
933
|
+
return tensor[slices]
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
def _parse_safetensors(path):
|
|
937
|
+
with open(path, "rb") as handle:
|
|
938
|
+
prefix = handle.read(8)
|
|
939
|
+
if len(prefix) != 8:
|
|
940
|
+
raise InvalidMetadata("restored container is shorter than 8 bytes")
|
|
941
|
+
header_len = struct.unpack("<Q", prefix)[0]
|
|
942
|
+
if header_len > 64 * 1024 * 1024:
|
|
943
|
+
raise InvalidMetadata(
|
|
944
|
+
"restored container header length %d exceeds the 64 MiB bound"
|
|
945
|
+
% header_len
|
|
946
|
+
)
|
|
947
|
+
header_raw = handle.read(header_len)
|
|
948
|
+
if len(header_raw) != header_len:
|
|
949
|
+
raise InvalidMetadata("restored container header is truncated")
|
|
950
|
+
try:
|
|
951
|
+
header = json.loads(header_raw)
|
|
952
|
+
except ValueError as exc:
|
|
953
|
+
raise InvalidMetadata("restored container header is not JSON: %s" % exc)
|
|
954
|
+
data_start = 8 + header_len
|
|
955
|
+
entries = {}
|
|
956
|
+
for name, spec in header.items():
|
|
957
|
+
if name == "__metadata__":
|
|
958
|
+
continue
|
|
959
|
+
try:
|
|
960
|
+
begin, end = spec["data_offsets"]
|
|
961
|
+
dtype = spec["dtype"]
|
|
962
|
+
shape = tuple(int(x) for x in spec["shape"])
|
|
963
|
+
except (KeyError, TypeError, ValueError) as exc:
|
|
964
|
+
raise InvalidMetadata(
|
|
965
|
+
"restored container tensor %r descriptor is invalid: %s"
|
|
966
|
+
% (name, exc)
|
|
967
|
+
)
|
|
968
|
+
if begin < 0 or end < begin:
|
|
969
|
+
raise InvalidMetadata(
|
|
970
|
+
"restored container tensor %r has invalid data_offsets" % name
|
|
971
|
+
)
|
|
972
|
+
handle.seek(data_start + begin)
|
|
973
|
+
payload = handle.read(end - begin)
|
|
974
|
+
if len(payload) != end - begin:
|
|
975
|
+
raise InvalidMetadata(
|
|
976
|
+
"restored container tensor %r is truncated" % name
|
|
977
|
+
)
|
|
978
|
+
entries[name] = (dtype, shape, payload)
|
|
979
|
+
return entries
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
# ---------------------------------------------------------------------------
|
|
983
|
+
# Writer
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
class WeftStorageWriter(_StorageWriterBase):
|
|
987
|
+
"""DCP ``StorageWriter`` that publishes a canonical WeftFS checkpoint.
|
|
988
|
+
|
|
989
|
+
Parameters
|
|
990
|
+
----------
|
|
991
|
+
path:
|
|
992
|
+
DCP checkpoint id. It becomes a single WeftFS checkpoint name under
|
|
993
|
+
``/checkpoints``; it must not contain ``/``.
|
|
994
|
+
store_root:
|
|
995
|
+
Directory of an initialized WeftFS store (or pass ``init_store=True``).
|
|
996
|
+
topology:
|
|
997
|
+
``n`` or ``(n, axis)``: store every tensor split n ways along ``axis``.
|
|
998
|
+
``n == 1`` stores whole tensors. The Rust CLI rejects a non-divisible
|
|
999
|
+
extent ("no silent padding"); the adapter never pads or reinterprets.
|
|
1000
|
+
This applies to non-tensor (``BYTE_IO``) state objects too: they are
|
|
1001
|
+
stored as ``u8[len]`` tensors, so a split topology needs their
|
|
1002
|
+
serialized length to be divisible by the shard count. Use ``topology=1``
|
|
1003
|
+
for state dicts whose byte objects have arbitrary lengths.
|
|
1004
|
+
shard_strategy:
|
|
1005
|
+
``zstd`` (default; exact, works for any shard length), ``raw`` (exact),
|
|
1006
|
+
or ``tensor-sharded`` (mode-10 byte-word transport). ``tensor-sharded``
|
|
1007
|
+
additionally requires every shard's byte length to be divisible by 16
|
|
1008
|
+
(the smallest mode-10 block); the Rust CLI rejects anything else rather
|
|
1009
|
+
than pad.
|
|
1010
|
+
staging_bytes:
|
|
1011
|
+
Hard bound on one resolved tensor copy staged for the safetensors
|
|
1012
|
+
container. Tensors above the bound raise :class:`StagingLimitExceeded`.
|
|
1013
|
+
overwrite:
|
|
1014
|
+
When True (default) a second save to the same id is written as a new
|
|
1015
|
+
generation ``<id>.v2``, ``<id>.v3``, ... (no store-side delete exists).
|
|
1016
|
+
When False an existing generation raises ``FileExistsError``.
|
|
1017
|
+
"""
|
|
1018
|
+
|
|
1019
|
+
def __init__(
|
|
1020
|
+
self,
|
|
1021
|
+
path,
|
|
1022
|
+
*,
|
|
1023
|
+
store_root,
|
|
1024
|
+
topology=1,
|
|
1025
|
+
axis=None,
|
|
1026
|
+
shard_strategy="zstd",
|
|
1027
|
+
staging_bytes=DEFAULT_STAGING_BYTES,
|
|
1028
|
+
overwrite=True,
|
|
1029
|
+
init_store=False,
|
|
1030
|
+
keep_staging=False,
|
|
1031
|
+
weftfs_cli=None,
|
|
1032
|
+
timeout=DEFAULT_TIMEOUT_SECONDS,
|
|
1033
|
+
):
|
|
1034
|
+
self._path = _validate_checkpoint_id(path)
|
|
1035
|
+
self.store_root = Path(store_root)
|
|
1036
|
+
self._topology = _parse_topology(topology, axis)
|
|
1037
|
+
if shard_strategy not in ("tensor-sharded", "tensor-tiled", "zstd", "raw"):
|
|
1038
|
+
raise ValueError(
|
|
1039
|
+
"shard_strategy must be tensor-sharded, tensor-tiled, zstd, or raw"
|
|
1040
|
+
)
|
|
1041
|
+
if shard_strategy == "tensor-tiled":
|
|
1042
|
+
raise ValueError(
|
|
1043
|
+
"tensor-tiled checkpoint shards need a tile shape, which this "
|
|
1044
|
+
"adapter does not expose; use tensor-sharded, zstd, or raw"
|
|
1045
|
+
)
|
|
1046
|
+
staging_bytes = int(staging_bytes)
|
|
1047
|
+
if staging_bytes <= 0:
|
|
1048
|
+
raise ValueError("staging_bytes must be a positive byte count")
|
|
1049
|
+
self.staging_bytes = staging_bytes
|
|
1050
|
+
self.shard_strategy = shard_strategy
|
|
1051
|
+
self.overwrite = bool(overwrite)
|
|
1052
|
+
self.keep_staging = bool(keep_staging)
|
|
1053
|
+
self._store = _Storage(self.store_root, cli=weftfs_cli, timeout=timeout)
|
|
1054
|
+
if init_store:
|
|
1055
|
+
self._store.init()
|
|
1056
|
+
self._store.requires_store()
|
|
1057
|
+
self._service_cli = _default_cli(weftfs_cli)
|
|
1058
|
+
self._timeout = float(timeout)
|
|
1059
|
+
self._service = None
|
|
1060
|
+
self._lock = threading.RLock()
|
|
1061
|
+
self._save_id = uuid.uuid4().hex
|
|
1062
|
+
self._storage_name = None
|
|
1063
|
+
self._entries = {}
|
|
1064
|
+
self._results = []
|
|
1065
|
+
self._pending_metadata = None
|
|
1066
|
+
self._finished = False
|
|
1067
|
+
self._manifest = None
|
|
1068
|
+
self._stats = {
|
|
1069
|
+
"entries": 0,
|
|
1070
|
+
"tensor_bytes": 0,
|
|
1071
|
+
"staging_peak_bytes": 0,
|
|
1072
|
+
"checkpoint_write_ms": 0.0,
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
# -- DCP interface ------------------------------------------------------
|
|
1076
|
+
|
|
1077
|
+
@classmethod
|
|
1078
|
+
def validate_checkpoint_id(cls, checkpoint_id) -> bool:
|
|
1079
|
+
try:
|
|
1080
|
+
_validate_checkpoint_id(checkpoint_id)
|
|
1081
|
+
return True
|
|
1082
|
+
except (InvalidCheckpointId, TypeError):
|
|
1083
|
+
return False
|
|
1084
|
+
|
|
1085
|
+
@property
|
|
1086
|
+
def checkpoint_id(self):
|
|
1087
|
+
return self._path
|
|
1088
|
+
|
|
1089
|
+
def _get_service(self):
|
|
1090
|
+
"""One helper process for every operation of this writer."""
|
|
1091
|
+
if self._service is None:
|
|
1092
|
+
self._service = CheckpointService(
|
|
1093
|
+
self.store_root, cli=self._service_cli, timeout=self._timeout
|
|
1094
|
+
)
|
|
1095
|
+
return self._service
|
|
1096
|
+
|
|
1097
|
+
def close(self):
|
|
1098
|
+
if self._service is not None:
|
|
1099
|
+
self._service.close()
|
|
1100
|
+
self._service = None
|
|
1101
|
+
|
|
1102
|
+
@property
|
|
1103
|
+
def store_name(self):
|
|
1104
|
+
return self._storage_name
|
|
1105
|
+
|
|
1106
|
+
@property
|
|
1107
|
+
def stats(self):
|
|
1108
|
+
payload = dict(self._stats)
|
|
1109
|
+
payload.update(
|
|
1110
|
+
{
|
|
1111
|
+
"checkpoint_id": self._path,
|
|
1112
|
+
"storage_name": self._storage_name,
|
|
1113
|
+
"store_root": os.fspath(self.store_root),
|
|
1114
|
+
"store_command_ms": self._store.command_ms,
|
|
1115
|
+
"store_command_calls": self._store.calls,
|
|
1116
|
+
"service": (
|
|
1117
|
+
None if self._service is None else dict(self._service.stats)
|
|
1118
|
+
),
|
|
1119
|
+
"topology": self._topology,
|
|
1120
|
+
"shard_strategy": self.shard_strategy,
|
|
1121
|
+
"staging_bytes": self.staging_bytes,
|
|
1122
|
+
}
|
|
1123
|
+
)
|
|
1124
|
+
return payload
|
|
1125
|
+
|
|
1126
|
+
def reset(self, checkpoint_id=None):
|
|
1127
|
+
with self._lock:
|
|
1128
|
+
if checkpoint_id is not None:
|
|
1129
|
+
self._path = _validate_checkpoint_id(checkpoint_id)
|
|
1130
|
+
self._save_id = uuid.uuid4().hex
|
|
1131
|
+
self._storage_name = None
|
|
1132
|
+
self._entries = {}
|
|
1133
|
+
self._results = []
|
|
1134
|
+
self._pending_metadata = None
|
|
1135
|
+
self._finished = False
|
|
1136
|
+
self._manifest = None
|
|
1137
|
+
self._stats = {
|
|
1138
|
+
"entries": 0,
|
|
1139
|
+
"tensor_bytes": 0,
|
|
1140
|
+
"staging_peak_bytes": 0,
|
|
1141
|
+
"checkpoint_write_ms": 0.0,
|
|
1142
|
+
}
|
|
1143
|
+
return None
|
|
1144
|
+
|
|
1145
|
+
def set_up_storage_writer(self, is_coordinator, *args, **kwargs):
|
|
1146
|
+
self._is_coordinator = bool(is_coordinator)
|
|
1147
|
+
return None
|
|
1148
|
+
|
|
1149
|
+
def prepare_local_plan(self, plan):
|
|
1150
|
+
return plan
|
|
1151
|
+
|
|
1152
|
+
def prepare_global_plan(self, plans):
|
|
1153
|
+
return plans
|
|
1154
|
+
|
|
1155
|
+
def storage_meta(self):
|
|
1156
|
+
return _TorchStorageMeta(checkpoint_id=self._path, save_id=self._save_id)
|
|
1157
|
+
|
|
1158
|
+
def write_metadata(self, metadata):
|
|
1159
|
+
"""Record the DCP metadata object.
|
|
1160
|
+
|
|
1161
|
+
PyTorch 2.15's ``save`` does not call this method (it writes metadata in
|
|
1162
|
+
``finish``); the method is implemented for interface parity and older
|
|
1163
|
+
callers. The metadata is persisted once, in ``finish``, because the
|
|
1164
|
+
storage keys are only final after ``write_data``.
|
|
1165
|
+
"""
|
|
1166
|
+
if metadata is None:
|
|
1167
|
+
raise InvalidMetadata("write_metadata(None)")
|
|
1168
|
+
with self._lock:
|
|
1169
|
+
self._pending_metadata = metadata
|
|
1170
|
+
return None
|
|
1171
|
+
|
|
1172
|
+
def write_data(self, plan, planner):
|
|
1173
|
+
future = _TorchFuture()
|
|
1174
|
+
try:
|
|
1175
|
+
results = self._execute(plan, planner)
|
|
1176
|
+
except BaseException as exc: # propagated through the DCP future
|
|
1177
|
+
future.set_exception(exc)
|
|
1178
|
+
return future
|
|
1179
|
+
future.set_result(results)
|
|
1180
|
+
return future
|
|
1181
|
+
|
|
1182
|
+
def finish(self, metadata, results):
|
|
1183
|
+
with self._lock:
|
|
1184
|
+
if self._storage_name is None:
|
|
1185
|
+
raise SaveFailed(
|
|
1186
|
+
"finish() called before a successful write_data(); no "
|
|
1187
|
+
"checkpoint was published"
|
|
1188
|
+
)
|
|
1189
|
+
if self._finished:
|
|
1190
|
+
raise SaveFailed("finish() called twice for one save")
|
|
1191
|
+
merged = {}
|
|
1192
|
+
for group in results:
|
|
1193
|
+
for result in group:
|
|
1194
|
+
merged[result.index] = result.storage_data
|
|
1195
|
+
metadata.storage_data = merged
|
|
1196
|
+
metadata.storage_meta = self.storage_meta()
|
|
1197
|
+
if getattr(metadata, "version", None) is None:
|
|
1198
|
+
metadata.version = "1.0.0"
|
|
1199
|
+
blob = serialize_metadata(
|
|
1200
|
+
metadata,
|
|
1201
|
+
checkpoint_id=self._path,
|
|
1202
|
+
storage_name=self._storage_name,
|
|
1203
|
+
)
|
|
1204
|
+
try:
|
|
1205
|
+
self._get_service().write_file(
|
|
1206
|
+
"/checkpoints/%s/%s" % (self._storage_name, DCP_METADATA_NAME),
|
|
1207
|
+
blob,
|
|
1208
|
+
)
|
|
1209
|
+
except (StoreCommandError, ServiceError) as exc:
|
|
1210
|
+
raise SaveFailed(
|
|
1211
|
+
"checkpoint %r was published but its DCP metadata was not: %s"
|
|
1212
|
+
% (self._storage_name, exc)
|
|
1213
|
+
) from exc
|
|
1214
|
+
self._finished = True
|
|
1215
|
+
self._metadata = metadata
|
|
1216
|
+
return None
|
|
1217
|
+
|
|
1218
|
+
# -- internals ----------------------------------------------------------
|
|
1219
|
+
|
|
1220
|
+
def _allocate_storage_name(self):
|
|
1221
|
+
existing = set(self._get_service().list_checkpoints())
|
|
1222
|
+
if self._path not in existing:
|
|
1223
|
+
return self._path
|
|
1224
|
+
if not self.overwrite:
|
|
1225
|
+
raise FileExistsError(
|
|
1226
|
+
"checkpoint %r already exists in %s and overwrite=False"
|
|
1227
|
+
% (self._path, self.store_root)
|
|
1228
|
+
)
|
|
1229
|
+
highest = 1
|
|
1230
|
+
for name in existing:
|
|
1231
|
+
generation = _generation(name, self._path)
|
|
1232
|
+
if generation is not None and generation > highest:
|
|
1233
|
+
highest = generation
|
|
1234
|
+
return "%s.v%d" % (self._path, highest + 1)
|
|
1235
|
+
|
|
1236
|
+
def _entry_names(self, items):
|
|
1237
|
+
used = set()
|
|
1238
|
+
names = {}
|
|
1239
|
+
for item in items:
|
|
1240
|
+
fqn = item.index.fqn
|
|
1241
|
+
name = fqn
|
|
1242
|
+
suffix = 1
|
|
1243
|
+
while name in used:
|
|
1244
|
+
name = "%s.__weftwp_%d" % (fqn, suffix)
|
|
1245
|
+
suffix += 1
|
|
1246
|
+
used.add(name)
|
|
1247
|
+
names[(fqn, tuple(item.index.offset or ()), item.index.index)] = name
|
|
1248
|
+
return names
|
|
1249
|
+
|
|
1250
|
+
def _execute(self, plan, planner):
|
|
1251
|
+
_require_torch("writing a DCP checkpoint")
|
|
1252
|
+
items = list(plan.items)
|
|
1253
|
+
if not items:
|
|
1254
|
+
raise SaveFailed(
|
|
1255
|
+
"save plan contains no items; refusing to publish an empty "
|
|
1256
|
+
"checkpoint"
|
|
1257
|
+
)
|
|
1258
|
+
items.sort(
|
|
1259
|
+
key=lambda item: (
|
|
1260
|
+
item.index.fqn,
|
|
1261
|
+
tuple(item.index.offset or ()),
|
|
1262
|
+
-1 if item.index.index is None else int(item.index.index),
|
|
1263
|
+
)
|
|
1264
|
+
)
|
|
1265
|
+
names = self._entry_names(items)
|
|
1266
|
+
|
|
1267
|
+
# Pass 1: derive lengths, dtypes and shapes; cache byte payloads.
|
|
1268
|
+
staged = []
|
|
1269
|
+
byte_payloads = {}
|
|
1270
|
+
byte_bytes = 0
|
|
1271
|
+
for item in items:
|
|
1272
|
+
key = (item.index.fqn, tuple(item.index.offset or ()), item.index.index)
|
|
1273
|
+
entry = names[key]
|
|
1274
|
+
if getattr(item.type, "name", str(item.type)) == "BYTE_IO":
|
|
1275
|
+
data = planner.resolve_data(item)
|
|
1276
|
+
if isinstance(data, io.BytesIO):
|
|
1277
|
+
payload = data.getvalue()
|
|
1278
|
+
elif isinstance(data, (bytes, bytearray, memoryview)):
|
|
1279
|
+
payload = bytes(data)
|
|
1280
|
+
else:
|
|
1281
|
+
raise UnsupportedStateObject(
|
|
1282
|
+
"BYTE_IO write item %r resolved to %s, expected bytes"
|
|
1283
|
+
% (item.index.fqn, type(data).__name__)
|
|
1284
|
+
)
|
|
1285
|
+
byte_bytes += len(payload)
|
|
1286
|
+
if byte_bytes > self.staging_bytes:
|
|
1287
|
+
raise StagingLimitExceeded(
|
|
1288
|
+
"byte state objects need %d staged bytes, above the %d "
|
|
1289
|
+
"staging_bytes bound" % (byte_bytes, self.staging_bytes)
|
|
1290
|
+
)
|
|
1291
|
+
byte_payloads[key] = payload
|
|
1292
|
+
staged.append(
|
|
1293
|
+
{
|
|
1294
|
+
"item": item,
|
|
1295
|
+
"entry": entry,
|
|
1296
|
+
"kind": "bytes",
|
|
1297
|
+
"dtype": "torch.uint8",
|
|
1298
|
+
"shape": (len(payload),),
|
|
1299
|
+
"payload": payload,
|
|
1300
|
+
"offsets": (),
|
|
1301
|
+
"lengths": (),
|
|
1302
|
+
}
|
|
1303
|
+
)
|
|
1304
|
+
continue
|
|
1305
|
+
|
|
1306
|
+
tensor_data = item.tensor_data
|
|
1307
|
+
if tensor_data is None:
|
|
1308
|
+
raise UnsupportedStateObject(
|
|
1309
|
+
"write item %r has no tensor_data" % (item.index.fqn,)
|
|
1310
|
+
)
|
|
1311
|
+
chunk = tensor_data.chunk
|
|
1312
|
+
shape = tuple(int(x) for x in tensor_data.size)
|
|
1313
|
+
offsets = tuple(int(x) for x in chunk.offsets)
|
|
1314
|
+
lengths = tuple(int(x) for x in chunk.sizes)
|
|
1315
|
+
if offsets != tuple([0] * len(shape)) or lengths != shape:
|
|
1316
|
+
raise UnsupportedStateObject(
|
|
1317
|
+
"write item %r is a chunk at %r of shape %r; sharded/"
|
|
1318
|
+
"chunked state objects are not persisted by this adapter"
|
|
1319
|
+
% (item.index.fqn, offsets, shape)
|
|
1320
|
+
)
|
|
1321
|
+
dtype = _dtype_name(tensor_data.properties.dtype)
|
|
1322
|
+
_canonical_dtype(dtype) # validates the exact mapping
|
|
1323
|
+
staged.append(
|
|
1324
|
+
{
|
|
1325
|
+
"item": item,
|
|
1326
|
+
"entry": entry,
|
|
1327
|
+
"kind": "tensor",
|
|
1328
|
+
"dtype": dtype,
|
|
1329
|
+
"shape": shape,
|
|
1330
|
+
"payload": None,
|
|
1331
|
+
"offsets": offsets,
|
|
1332
|
+
"lengths": lengths,
|
|
1333
|
+
}
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1336
|
+
# Header is fixed before data is streamed.
|
|
1337
|
+
header = {}
|
|
1338
|
+
cursor = 0
|
|
1339
|
+
for row in staged:
|
|
1340
|
+
nbytes = (
|
|
1341
|
+
len(row["payload"])
|
|
1342
|
+
if row["kind"] == "bytes"
|
|
1343
|
+
else _element_bytes(row["dtype"]) * math.prod(row["shape"])
|
|
1344
|
+
)
|
|
1345
|
+
header[row["entry"]] = {
|
|
1346
|
+
"dtype": _safetensors_dtype(row["dtype"]),
|
|
1347
|
+
"shape": list(row["shape"]),
|
|
1348
|
+
"data_offsets": [cursor, cursor + nbytes],
|
|
1349
|
+
}
|
|
1350
|
+
row["nbytes"] = nbytes
|
|
1351
|
+
cursor += nbytes
|
|
1352
|
+
header_raw = json.dumps(
|
|
1353
|
+
header, sort_keys=True, separators=(",", ":")
|
|
1354
|
+
).encode("utf-8")
|
|
1355
|
+
if len(header_raw) > 64 * 1024 * 1024:
|
|
1356
|
+
raise SaveFailed("staging safetensors header exceeds 64 MiB")
|
|
1357
|
+
|
|
1358
|
+
# Stage the canonical container in memory. It must exist as
|
|
1359
|
+
# safetensors bytes because the checkpoint manifest records the source
|
|
1360
|
+
# header and SHA-256, but it never reaches the disk, is never a path
|
|
1361
|
+
# argument to a CLI process, and is handed to the helper over a memfd.
|
|
1362
|
+
container = bytearray(struct.pack("<Q", len(header_raw)))
|
|
1363
|
+
container += header_raw
|
|
1364
|
+
infos = []
|
|
1365
|
+
peak = 0
|
|
1366
|
+
for row in staged:
|
|
1367
|
+
if row["kind"] == "bytes":
|
|
1368
|
+
payload = row["payload"]
|
|
1369
|
+
else:
|
|
1370
|
+
tensor = planner.resolve_data(row["item"])
|
|
1371
|
+
if not hasattr(tensor, "detach"):
|
|
1372
|
+
raise UnsupportedStateObject(
|
|
1373
|
+
"tensor write item %r resolved to %s"
|
|
1374
|
+
% (row["item"].index.fqn, type(tensor).__name__)
|
|
1375
|
+
)
|
|
1376
|
+
actual_dtype = _dtype_name(tensor.dtype)
|
|
1377
|
+
actual_shape = tuple(int(x) for x in tensor.shape)
|
|
1378
|
+
if actual_dtype != row["dtype"] or actual_shape != row["shape"]:
|
|
1379
|
+
raise SaveFailed(
|
|
1380
|
+
"planner resolved %r as %s%r, plan said %s%r"
|
|
1381
|
+
% (
|
|
1382
|
+
row["item"].index.fqn,
|
|
1383
|
+
actual_dtype,
|
|
1384
|
+
actual_shape,
|
|
1385
|
+
row["dtype"],
|
|
1386
|
+
row["shape"],
|
|
1387
|
+
)
|
|
1388
|
+
)
|
|
1389
|
+
if row["nbytes"] > self.staging_bytes:
|
|
1390
|
+
raise StagingLimitExceeded(
|
|
1391
|
+
"tensor %r needs %d staged bytes, above the %d "
|
|
1392
|
+
"staging_bytes bound"
|
|
1393
|
+
% (
|
|
1394
|
+
row["item"].index.fqn,
|
|
1395
|
+
row["nbytes"],
|
|
1396
|
+
self.staging_bytes,
|
|
1397
|
+
)
|
|
1398
|
+
)
|
|
1399
|
+
payload = _tensor_bytes(tensor)
|
|
1400
|
+
peak = max(peak, len(payload))
|
|
1401
|
+
if len(payload) != row["nbytes"]:
|
|
1402
|
+
raise SaveFailed(
|
|
1403
|
+
"entry %r produced %d bytes, header declared %d"
|
|
1404
|
+
% (row["entry"], len(payload), row["nbytes"])
|
|
1405
|
+
)
|
|
1406
|
+
container += payload
|
|
1407
|
+
infos.append(
|
|
1408
|
+
StorageInfo(
|
|
1409
|
+
entry=row["entry"],
|
|
1410
|
+
dtype=row["dtype"],
|
|
1411
|
+
shape=row["shape"],
|
|
1412
|
+
storage_offsets=row["offsets"],
|
|
1413
|
+
lengths=row["lengths"],
|
|
1414
|
+
content_root=content_root(payload),
|
|
1415
|
+
)
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1418
|
+
shards, axis = self._topology
|
|
1419
|
+
storage_name = self._allocate_storage_name()
|
|
1420
|
+
service = self._get_service()
|
|
1421
|
+
started = time.monotonic()
|
|
1422
|
+
write_report = service.write_checkpoint(
|
|
1423
|
+
storage_name,
|
|
1424
|
+
container,
|
|
1425
|
+
shards=shards,
|
|
1426
|
+
axis=axis,
|
|
1427
|
+
strategy=self.shard_strategy,
|
|
1428
|
+
)
|
|
1429
|
+
self._stats["checkpoint_write_ms"] = (time.monotonic() - started) * 1000.0
|
|
1430
|
+
self._stats["container_bytes"] = len(container)
|
|
1431
|
+
manifest_value = write_report.manifest.to_value()
|
|
1432
|
+
self._verify_report(
|
|
1433
|
+
{
|
|
1434
|
+
"complete": write_report.complete,
|
|
1435
|
+
"tensors": manifest_value["tensors"],
|
|
1436
|
+
},
|
|
1437
|
+
manifest_value["tensors"],
|
|
1438
|
+
infos,
|
|
1439
|
+
storage_name,
|
|
1440
|
+
)
|
|
1441
|
+
self._storage_name = storage_name
|
|
1442
|
+
self._manifest = {
|
|
1443
|
+
"manifest_root": write_report.manifest_root,
|
|
1444
|
+
"manifest": manifest_value,
|
|
1445
|
+
}
|
|
1446
|
+
self._stats["entries"] = len(infos)
|
|
1447
|
+
self._stats["tensor_bytes"] = sum(
|
|
1448
|
+
int(t["element_bytes"]) for t in manifest_value["tensors"]
|
|
1449
|
+
)
|
|
1450
|
+
self._stats["staging_peak_bytes"] = peak
|
|
1451
|
+
|
|
1452
|
+
results = []
|
|
1453
|
+
for row, info in zip(staged, infos):
|
|
1454
|
+
results.append(_write_result(row["item"], info, row["nbytes"]))
|
|
1455
|
+
with self._lock:
|
|
1456
|
+
self._results = results
|
|
1457
|
+
self._entries = {info.entry: info for info in infos}
|
|
1458
|
+
return results
|
|
1459
|
+
|
|
1460
|
+
def _verify_report(self, report, manifest_tensors, infos, storage_name):
|
|
1461
|
+
tensors = manifest_tensors
|
|
1462
|
+
if not isinstance(tensors, list) or not tensors:
|
|
1463
|
+
raise SaveFailed(
|
|
1464
|
+
"checkpoint manifest for %r has no tensor list" % storage_name
|
|
1465
|
+
)
|
|
1466
|
+
by_name = {row["name"]: row for row in tensors}
|
|
1467
|
+
if set(by_name) != {info.entry for info in infos}:
|
|
1468
|
+
raise SaveFailed(
|
|
1469
|
+
"checkpoint %r stored tensors %r, expected %r"
|
|
1470
|
+
% (
|
|
1471
|
+
storage_name,
|
|
1472
|
+
sorted(by_name),
|
|
1473
|
+
sorted(info.entry for info in infos),
|
|
1474
|
+
)
|
|
1475
|
+
)
|
|
1476
|
+
for info in infos:
|
|
1477
|
+
row = by_name[info.entry]
|
|
1478
|
+
if row.get("dtype") != _canonical_dtype(info.dtype):
|
|
1479
|
+
raise ExactnessError(
|
|
1480
|
+
"entry %r dtype %r != canonical %r"
|
|
1481
|
+
% (info.entry, row.get("dtype"), _canonical_dtype(info.dtype))
|
|
1482
|
+
)
|
|
1483
|
+
if tuple(int(x) for x in row.get("shape", [])) != info.shape:
|
|
1484
|
+
raise ExactnessError(
|
|
1485
|
+
"entry %r shape %r != %r"
|
|
1486
|
+
% (info.entry, row.get("shape"), info.shape)
|
|
1487
|
+
)
|
|
1488
|
+
stored = row.get("content_root")
|
|
1489
|
+
if stored != info.content_root.hex():
|
|
1490
|
+
raise ExactnessError(
|
|
1491
|
+
"entry %r content root %s != the bytes written (%s)"
|
|
1492
|
+
% (info.entry, stored, info.content_root.hex())
|
|
1493
|
+
)
|
|
1494
|
+
if report.get("complete") is not True:
|
|
1495
|
+
raise SaveFailed(
|
|
1496
|
+
"checkpoint %r is not complete" % storage_name
|
|
1497
|
+
)
|
|
1498
|
+
|
|
1499
|
+
|
|
1500
|
+
def _write_result(item, info, nbytes):
|
|
1501
|
+
_require_torch("building a DCP WriteResult")
|
|
1502
|
+
from torch.distributed.checkpoint.storage import WriteResult as _WriteResult
|
|
1503
|
+
|
|
1504
|
+
return _WriteResult(
|
|
1505
|
+
index=item.index, size_in_bytes=int(nbytes), storage_data=info
|
|
1506
|
+
)
|
|
1507
|
+
|
|
1508
|
+
|
|
1509
|
+
# ---------------------------------------------------------------------------
|
|
1510
|
+
# Reader
|
|
1511
|
+
|
|
1512
|
+
|
|
1513
|
+
class _RestoredHandle:
|
|
1514
|
+
"""One memfd-backed restored tensor plus its recorded metadata."""
|
|
1515
|
+
|
|
1516
|
+
__slots__ = ("entry", "buffer", "info")
|
|
1517
|
+
|
|
1518
|
+
def __init__(self, entry, buffer, info):
|
|
1519
|
+
self.entry = entry
|
|
1520
|
+
self.buffer = buffer
|
|
1521
|
+
self.info = info
|
|
1522
|
+
|
|
1523
|
+
|
|
1524
|
+
class WeftStorageReader(_StorageReaderBase):
|
|
1525
|
+
"""DCP ``StorageReader`` over a WeftFS checkpoint.
|
|
1526
|
+
|
|
1527
|
+
The checkpoint manifest (``weft.checkpoint.v1``) is the authority for tensor
|
|
1528
|
+
identity. If ``/checkpoints/<name>/dcp.metadata`` exists it supplies the DCP
|
|
1529
|
+
protocol metadata and is verified against the manifest; otherwise metadata is
|
|
1530
|
+
synthesised from the manifest itself (``metadata_source == "manifest"``,
|
|
1531
|
+
exact for plain-integer/packed safetensors checkpoints written by the Rust
|
|
1532
|
+
CLI).
|
|
1533
|
+
"""
|
|
1534
|
+
|
|
1535
|
+
def __init__(
|
|
1536
|
+
self,
|
|
1537
|
+
path,
|
|
1538
|
+
*,
|
|
1539
|
+
store_root,
|
|
1540
|
+
weftfs_cli=None,
|
|
1541
|
+
timeout=DEFAULT_TIMEOUT_SECONDS,
|
|
1542
|
+
allow_manifest_synthesis=True,
|
|
1543
|
+
import_restored=False,
|
|
1544
|
+
):
|
|
1545
|
+
self._path = _validate_checkpoint_id(path)
|
|
1546
|
+
self.store_root = Path(store_root)
|
|
1547
|
+
self.allow_manifest_synthesis = bool(allow_manifest_synthesis)
|
|
1548
|
+
# Restore is read-only in the store by default. Importing a second,
|
|
1549
|
+
# exact copy into ``/dcp-reads/...`` is opt-in because it is a durable
|
|
1550
|
+
# store mutation.
|
|
1551
|
+
self.import_restored = bool(import_restored)
|
|
1552
|
+
if self.import_restored:
|
|
1553
|
+
raise DcpError(
|
|
1554
|
+
"import_restored is no longer supported: the checkpoint service "
|
|
1555
|
+
"restores directly into caller buffers and never materializes a "
|
|
1556
|
+
"container to import"
|
|
1557
|
+
)
|
|
1558
|
+
self._store = _Storage(self.store_root, cli=weftfs_cli, timeout=timeout)
|
|
1559
|
+
self._store.requires_store()
|
|
1560
|
+
self._service_cli = _default_cli(weftfs_cli)
|
|
1561
|
+
self._timeout = float(timeout)
|
|
1562
|
+
self._service = None
|
|
1563
|
+
self._storage_name = None
|
|
1564
|
+
self._manifest = None
|
|
1565
|
+
self._metadata = None
|
|
1566
|
+
self._metadata_source = None
|
|
1567
|
+
self._storage_data = None
|
|
1568
|
+
self._stats = {"restores": 0, "entries_restored": 0, "restored_bytes": 0}
|
|
1569
|
+
self._lock = threading.RLock()
|
|
1570
|
+
|
|
1571
|
+
# -- DCP interface ------------------------------------------------------
|
|
1572
|
+
|
|
1573
|
+
@classmethod
|
|
1574
|
+
def validate_checkpoint_id(cls, checkpoint_id) -> bool:
|
|
1575
|
+
try:
|
|
1576
|
+
_validate_checkpoint_id(checkpoint_id)
|
|
1577
|
+
return True
|
|
1578
|
+
except (InvalidCheckpointId, TypeError):
|
|
1579
|
+
return False
|
|
1580
|
+
|
|
1581
|
+
@property
|
|
1582
|
+
def checkpoint_id(self):
|
|
1583
|
+
return self._path
|
|
1584
|
+
|
|
1585
|
+
@property
|
|
1586
|
+
def storage_name(self):
|
|
1587
|
+
return self._storage_name
|
|
1588
|
+
|
|
1589
|
+
def _get_service(self):
|
|
1590
|
+
"""One helper process for every read of this reader."""
|
|
1591
|
+
if self._service is None:
|
|
1592
|
+
self._service = CheckpointService(
|
|
1593
|
+
self.store_root, cli=self._service_cli, timeout=self._timeout
|
|
1594
|
+
)
|
|
1595
|
+
return self._service
|
|
1596
|
+
|
|
1597
|
+
def close(self):
|
|
1598
|
+
if self._service is not None:
|
|
1599
|
+
self._service.close()
|
|
1600
|
+
self._service = None
|
|
1601
|
+
|
|
1602
|
+
def __enter__(self):
|
|
1603
|
+
return self
|
|
1604
|
+
|
|
1605
|
+
def __exit__(self, *exc):
|
|
1606
|
+
self.close()
|
|
1607
|
+
return False
|
|
1608
|
+
|
|
1609
|
+
@property
|
|
1610
|
+
def metadata_source(self):
|
|
1611
|
+
return self._metadata_source
|
|
1612
|
+
|
|
1613
|
+
@property
|
|
1614
|
+
def stats(self):
|
|
1615
|
+
payload = dict(self._stats)
|
|
1616
|
+
payload.update(
|
|
1617
|
+
{
|
|
1618
|
+
"checkpoint_id": self._path,
|
|
1619
|
+
"storage_name": self._storage_name,
|
|
1620
|
+
"metadata_source": self._metadata_source,
|
|
1621
|
+
"store_root": os.fspath(self.store_root),
|
|
1622
|
+
"store_command_ms": self._store.command_ms,
|
|
1623
|
+
"store_command_calls": self._store.calls,
|
|
1624
|
+
"service": (
|
|
1625
|
+
None if self._service is None else dict(self._service.stats)
|
|
1626
|
+
),
|
|
1627
|
+
}
|
|
1628
|
+
)
|
|
1629
|
+
return payload
|
|
1630
|
+
|
|
1631
|
+
def reset(self, checkpoint_id=None):
|
|
1632
|
+
with self._lock:
|
|
1633
|
+
if checkpoint_id is not None:
|
|
1634
|
+
self._path = _validate_checkpoint_id(checkpoint_id)
|
|
1635
|
+
self._storage_name = None
|
|
1636
|
+
self._manifest = None
|
|
1637
|
+
self._metadata = None
|
|
1638
|
+
self._metadata_source = None
|
|
1639
|
+
self._storage_data = None
|
|
1640
|
+
self._stats = {"restores": 0, "entries_restored": 0, "restored_bytes": 0}
|
|
1641
|
+
return None
|
|
1642
|
+
|
|
1643
|
+
def _resolve_storage_name(self):
|
|
1644
|
+
names = set(self._get_service().list_checkpoints())
|
|
1645
|
+
highest = 1
|
|
1646
|
+
chosen = None
|
|
1647
|
+
for name in names:
|
|
1648
|
+
generation = _generation(name, self._path)
|
|
1649
|
+
if generation is not None and generation >= highest:
|
|
1650
|
+
highest = generation
|
|
1651
|
+
chosen = name
|
|
1652
|
+
if chosen is None:
|
|
1653
|
+
raise MetadataMissing(
|
|
1654
|
+
"checkpoint %r does not exist in %s" % (self._path, self.store_root)
|
|
1655
|
+
)
|
|
1656
|
+
return chosen
|
|
1657
|
+
|
|
1658
|
+
def read_metadata(self, *args, **kwargs):
|
|
1659
|
+
_require_torch("reading DCP metadata")
|
|
1660
|
+
with self._lock:
|
|
1661
|
+
if self._metadata is not None:
|
|
1662
|
+
return self._metadata
|
|
1663
|
+
storage_name = self._resolve_storage_name()
|
|
1664
|
+
service = self._get_service()
|
|
1665
|
+
# A reader is a long-lived object; refresh so commits made by
|
|
1666
|
+
# another process are visible before the manifest is read.
|
|
1667
|
+
service.refresh()
|
|
1668
|
+
descriptor = service.manifest(storage_name)
|
|
1669
|
+
manifest = descriptor.to_value()
|
|
1670
|
+
show = {
|
|
1671
|
+
"manifest_root": descriptor.manifest_root,
|
|
1672
|
+
"manifest": manifest,
|
|
1673
|
+
}
|
|
1674
|
+
manifest_rows = {
|
|
1675
|
+
row["name"]: row for row in manifest.get("tensors", [])
|
|
1676
|
+
}
|
|
1677
|
+
metadata_path = "/checkpoints/%s/%s" % (storage_name, DCP_METADATA_NAME)
|
|
1678
|
+
blob = None
|
|
1679
|
+
exists, _size = service.stat_file(metadata_path)
|
|
1680
|
+
if exists:
|
|
1681
|
+
with service.read_file(metadata_path) as view:
|
|
1682
|
+
blob = view.bytes
|
|
1683
|
+
if blob is not None:
|
|
1684
|
+
try:
|
|
1685
|
+
value = wcb.decode(blob)
|
|
1686
|
+
except Exception as exc:
|
|
1687
|
+
raise InvalidMetadata(
|
|
1688
|
+
"stored DCP metadata at %s is not canonical WCB: %s"
|
|
1689
|
+
% (metadata_path, exc)
|
|
1690
|
+
) from exc
|
|
1691
|
+
metadata = _metadata_from_value(value, storage_name=storage_name)
|
|
1692
|
+
source = "dcp"
|
|
1693
|
+
self._validate_against_manifest(metadata, manifest_rows)
|
|
1694
|
+
else:
|
|
1695
|
+
if not self.allow_manifest_synthesis:
|
|
1696
|
+
raise MetadataMissing(
|
|
1697
|
+
"checkpoint %r has no %s and manifest synthesis is "
|
|
1698
|
+
"disabled" % (storage_name, DCP_METADATA_NAME)
|
|
1699
|
+
)
|
|
1700
|
+
metadata = _metadata_from_manifest(
|
|
1701
|
+
show,
|
|
1702
|
+
checkpoint_id=self._path,
|
|
1703
|
+
storage_name=storage_name,
|
|
1704
|
+
)
|
|
1705
|
+
source = "manifest"
|
|
1706
|
+
metadata.storage_meta = _TorchStorageMeta(
|
|
1707
|
+
checkpoint_id=self._path,
|
|
1708
|
+
save_id=storage_name,
|
|
1709
|
+
load_id=uuid.uuid4().hex,
|
|
1710
|
+
)
|
|
1711
|
+
self._storage_name = storage_name
|
|
1712
|
+
self._manifest = show
|
|
1713
|
+
self._metadata = metadata
|
|
1714
|
+
self._metadata_source = source
|
|
1715
|
+
self._storage_data = metadata.storage_data
|
|
1716
|
+
return metadata
|
|
1717
|
+
|
|
1718
|
+
def set_up_storage_reader(self, metadata, is_coordinator, *args, **kwargs):
|
|
1719
|
+
if metadata.storage_data is None:
|
|
1720
|
+
raise InvalidMetadata("metadata.storage_data is None")
|
|
1721
|
+
with self._lock:
|
|
1722
|
+
self._metadata = metadata
|
|
1723
|
+
self._storage_data = metadata.storage_data
|
|
1724
|
+
if self._storage_name is None:
|
|
1725
|
+
# Metadata supplied by the caller rather than read_metadata().
|
|
1726
|
+
self._storage_name = getattr(
|
|
1727
|
+
getattr(metadata, "storage_meta", None), "save_id", None
|
|
1728
|
+
)
|
|
1729
|
+
return None
|
|
1730
|
+
|
|
1731
|
+
def prepare_local_plan(self, plan):
|
|
1732
|
+
return plan
|
|
1733
|
+
|
|
1734
|
+
def prepare_global_plan(self, plans):
|
|
1735
|
+
return plans
|
|
1736
|
+
|
|
1737
|
+
def read_data(self, plan, planner):
|
|
1738
|
+
future = _TorchFuture()
|
|
1739
|
+
try:
|
|
1740
|
+
self._read_plan(plan, planner)
|
|
1741
|
+
except BaseException as exc:
|
|
1742
|
+
future.set_exception(exc)
|
|
1743
|
+
return future
|
|
1744
|
+
future.set_result(None)
|
|
1745
|
+
return future
|
|
1746
|
+
|
|
1747
|
+
# -- helpers ------------------------------------------------------------
|
|
1748
|
+
|
|
1749
|
+
def _validate_against_manifest(self, metadata, manifest_rows):
|
|
1750
|
+
for info in metadata.storage_data.values():
|
|
1751
|
+
row = manifest_rows.get(info.entry)
|
|
1752
|
+
if row is None:
|
|
1753
|
+
raise InvalidMetadata(
|
|
1754
|
+
"DCP metadata names entry %r, absent from the checkpoint "
|
|
1755
|
+
"manifest" % info.entry
|
|
1756
|
+
)
|
|
1757
|
+
if row.get("content_root") != info.content_root.hex():
|
|
1758
|
+
raise ExactnessError(
|
|
1759
|
+
"DCP metadata content root for %r (%s) != manifest (%s)"
|
|
1760
|
+
% (info.entry, info.content_root.hex(), row.get("content_root"))
|
|
1761
|
+
)
|
|
1762
|
+
if tuple(int(x) for x in row.get("shape", [])) != info.shape:
|
|
1763
|
+
raise ExactnessError(
|
|
1764
|
+
"DCP metadata shape for %r (%r) != manifest (%r)"
|
|
1765
|
+
% (info.entry, info.shape, row.get("shape"))
|
|
1766
|
+
)
|
|
1767
|
+
expected_canonical = _canonical_dtype(info.dtype)
|
|
1768
|
+
if row.get("dtype") != expected_canonical:
|
|
1769
|
+
raise ExactnessError(
|
|
1770
|
+
"DCP metadata dtype for %r (%s) != manifest (%s)"
|
|
1771
|
+
% (info.entry, expected_canonical, row.get("dtype"))
|
|
1772
|
+
)
|
|
1773
|
+
|
|
1774
|
+
def _restore(self, entries, *, topology=None, axis=0):
|
|
1775
|
+
"""Restore entries straight into caller-owned memfds.
|
|
1776
|
+
|
|
1777
|
+
Returns ``{entry: _RestoredHandle}``. The handle owns a memfd-backed
|
|
1778
|
+
mapping; callers must ``close()`` it (or pass it to
|
|
1779
|
+
:meth:`_release_handles`). No file is written and the store is never
|
|
1780
|
+
mutated.
|
|
1781
|
+
"""
|
|
1782
|
+
if self._storage_name is None:
|
|
1783
|
+
raise DcpError("read_data() before read_metadata()")
|
|
1784
|
+
service = self._get_service()
|
|
1785
|
+
manifest_rows = {
|
|
1786
|
+
row["name"]: row for row in self._manifest["manifest"].get("tensors", [])
|
|
1787
|
+
}
|
|
1788
|
+
unknown = [entry for entry in entries if entry not in manifest_rows]
|
|
1789
|
+
if unknown:
|
|
1790
|
+
raise InvalidMetadata(
|
|
1791
|
+
"requested entries %r are not in checkpoint %r"
|
|
1792
|
+
% (sorted(unknown), self._storage_name)
|
|
1793
|
+
)
|
|
1794
|
+
topology_arg = None if topology is None else (int(topology), int(axis))
|
|
1795
|
+
found = {}
|
|
1796
|
+
try:
|
|
1797
|
+
for entry in sorted(entries):
|
|
1798
|
+
info = self._entry_info(entry)
|
|
1799
|
+
try:
|
|
1800
|
+
buffer = service.restore_tensor(
|
|
1801
|
+
self._storage_name,
|
|
1802
|
+
entry,
|
|
1803
|
+
topology=topology_arg,
|
|
1804
|
+
expected_size=int(manifest_rows[entry]["element_bytes"]),
|
|
1805
|
+
)
|
|
1806
|
+
except ServiceError as exc:
|
|
1807
|
+
raise self._translate(exc) from exc
|
|
1808
|
+
if tuple(buffer.shape) != tuple(info.shape):
|
|
1809
|
+
buffer.close()
|
|
1810
|
+
raise ExactnessError(
|
|
1811
|
+
"entry %r restored with shape %r, expected %r"
|
|
1812
|
+
% (entry, tuple(buffer.shape), info.shape)
|
|
1813
|
+
)
|
|
1814
|
+
actual = content_root(buffer.view)
|
|
1815
|
+
if actual != info.content_root:
|
|
1816
|
+
buffer.close()
|
|
1817
|
+
raise ExactnessError(
|
|
1818
|
+
"entry %r restored content root %s != recorded %s"
|
|
1819
|
+
% (entry, actual.hex(), info.content_root.hex())
|
|
1820
|
+
)
|
|
1821
|
+
found[entry] = _RestoredHandle(entry, buffer, info)
|
|
1822
|
+
except BaseException:
|
|
1823
|
+
self._release_handles(found)
|
|
1824
|
+
raise
|
|
1825
|
+
with self._lock:
|
|
1826
|
+
self._stats["restores"] += 1
|
|
1827
|
+
self._stats["entries_restored"] += len(found)
|
|
1828
|
+
self._stats["restored_bytes"] += sum(
|
|
1829
|
+
handle.buffer.size for handle in found.values()
|
|
1830
|
+
)
|
|
1831
|
+
return found
|
|
1832
|
+
|
|
1833
|
+
@staticmethod
|
|
1834
|
+
def _release_handles(handles):
|
|
1835
|
+
for handle in handles.values():
|
|
1836
|
+
handle.buffer.close()
|
|
1837
|
+
|
|
1838
|
+
@staticmethod
|
|
1839
|
+
def _translate(exc):
|
|
1840
|
+
"""Map a typed service error onto the adapter's error hierarchy."""
|
|
1841
|
+
message = str(exc)
|
|
1842
|
+
if exc.class_name == "NotFound":
|
|
1843
|
+
return MetadataMissing(message)
|
|
1844
|
+
if exc.class_name in ("Corrupt", "Malformed", "NonCanonical"):
|
|
1845
|
+
return ExactnessError(message)
|
|
1846
|
+
if exc.class_name in ("LimitExceeded", "BudgetExceeded"):
|
|
1847
|
+
return StagingLimitExceeded(message)
|
|
1848
|
+
return DcpError(message)
|
|
1849
|
+
|
|
1850
|
+
def _entry_info(self, entry):
|
|
1851
|
+
for info in self._storage_data.values():
|
|
1852
|
+
if info.entry == entry:
|
|
1853
|
+
return info
|
|
1854
|
+
raise InvalidMetadata("no storage entry %r in DCP metadata" % entry)
|
|
1855
|
+
|
|
1856
|
+
def restore_entry(self, entry, *, topology=None, axis=0):
|
|
1857
|
+
"""Restore exactly one manifest entry, optionally through a topology.
|
|
1858
|
+
|
|
1859
|
+
The service decodes the entry straight into a caller-owned memfd and
|
|
1860
|
+
verifies the shard roots, the whole-tensor logical root (and the
|
|
1861
|
+
re-layout topology root when requested). ``data`` is an explicit copy
|
|
1862
|
+
of those bytes; the zero-copy path is ``WeftStorageReader.load``.
|
|
1863
|
+
"""
|
|
1864
|
+
if self._metadata is None:
|
|
1865
|
+
self.read_metadata()
|
|
1866
|
+
handles = self._restore([entry], topology=topology, axis=axis)
|
|
1867
|
+
handle = handles[entry]
|
|
1868
|
+
try:
|
|
1869
|
+
buffer = handle.buffer
|
|
1870
|
+
return RestoredEntry(
|
|
1871
|
+
entry=entry,
|
|
1872
|
+
data=buffer.bytes,
|
|
1873
|
+
dtype=handle.info.dtype,
|
|
1874
|
+
shape=tuple(buffer.shape),
|
|
1875
|
+
content_root=handle.info.content_root,
|
|
1876
|
+
topology_root=buffer.topology_root,
|
|
1877
|
+
shards_read=buffer.shards_read,
|
|
1878
|
+
store_bytes_read=buffer.store_bytes_read,
|
|
1879
|
+
)
|
|
1880
|
+
finally:
|
|
1881
|
+
handle.buffer.close()
|
|
1882
|
+
|
|
1883
|
+
def _read_plan(self, plan, planner):
|
|
1884
|
+
_require_torch("reading checkpoint data")
|
|
1885
|
+
if not plan.items:
|
|
1886
|
+
return
|
|
1887
|
+
storage = self._storage_data
|
|
1888
|
+
if storage is None:
|
|
1889
|
+
raise DcpError("read_data() before read_metadata()")
|
|
1890
|
+
items_by_entry = {}
|
|
1891
|
+
for read_item in plan.items:
|
|
1892
|
+
key = (
|
|
1893
|
+
read_item.storage_index.fqn,
|
|
1894
|
+
None
|
|
1895
|
+
if read_item.storage_index.offset is None
|
|
1896
|
+
else tuple(int(x) for x in read_item.storage_index.offset),
|
|
1897
|
+
)
|
|
1898
|
+
info = None
|
|
1899
|
+
for stored_key, stored_info in storage.items():
|
|
1900
|
+
stored_offset = (
|
|
1901
|
+
None
|
|
1902
|
+
if stored_key.offset is None
|
|
1903
|
+
else tuple(int(x) for x in stored_key.offset)
|
|
1904
|
+
)
|
|
1905
|
+
if stored_key.fqn == key[0] and stored_offset == key[1]:
|
|
1906
|
+
info = stored_info
|
|
1907
|
+
break
|
|
1908
|
+
if info is None:
|
|
1909
|
+
raise InvalidMetadata(
|
|
1910
|
+
"load plan requests storage index %r, absent from DCP metadata"
|
|
1911
|
+
% (read_item.storage_index,)
|
|
1912
|
+
)
|
|
1913
|
+
items_by_entry.setdefault(info.entry, []).append((read_item, info))
|
|
1914
|
+
|
|
1915
|
+
entries = sorted(items_by_entry)
|
|
1916
|
+
handles = self._restore(entries)
|
|
1917
|
+
try:
|
|
1918
|
+
for entry in entries:
|
|
1919
|
+
info = items_by_entry[entry][0][1]
|
|
1920
|
+
handle = handles[entry]
|
|
1921
|
+
buffer = handle.buffer
|
|
1922
|
+
for read_item, _info in items_by_entry[entry]:
|
|
1923
|
+
item_type = getattr(read_item.type, "name", str(read_item.type))
|
|
1924
|
+
if item_type == "BYTE_IO":
|
|
1925
|
+
planner.load_bytes(read_item, io.BytesIO(buffer.bytes))
|
|
1926
|
+
continue
|
|
1927
|
+
dtype = _torch_dtype(info.dtype)
|
|
1928
|
+
if buffer.size:
|
|
1929
|
+
# Zero-copy: torch views the caller-owned memfd.
|
|
1930
|
+
base = _TORCH.frombuffer(buffer.view, dtype=_TORCH.uint8)
|
|
1931
|
+
base = base.view(dtype).reshape(tuple(info.shape))
|
|
1932
|
+
else:
|
|
1933
|
+
base = _TORCH.empty(tuple(info.shape), dtype=dtype)
|
|
1934
|
+
offsets = tuple(int(x) for x in read_item.storage_offsets)
|
|
1935
|
+
lengths = tuple(int(x) for x in read_item.lengths)
|
|
1936
|
+
narrowed = _narrow(base, offsets, lengths)
|
|
1937
|
+
target = planner.resolve_tensor(read_item).detach()
|
|
1938
|
+
if tuple(target.shape) != tuple(narrowed.shape):
|
|
1939
|
+
raise ExactnessError(
|
|
1940
|
+
"target %r shape %r != stored slice %r"
|
|
1941
|
+
% (
|
|
1942
|
+
read_item.dest_index.fqn,
|
|
1943
|
+
tuple(target.shape),
|
|
1944
|
+
tuple(narrowed.shape),
|
|
1945
|
+
)
|
|
1946
|
+
)
|
|
1947
|
+
# DCP allocates the destination tensor, so this copy is the
|
|
1948
|
+
# interface's own contract, not the adapter's staging.
|
|
1949
|
+
target.copy_(narrowed)
|
|
1950
|
+
planner.commit_tensor(read_item, target)
|
|
1951
|
+
del narrowed, base
|
|
1952
|
+
finally:
|
|
1953
|
+
self._release_handles(handles)
|
|
1954
|
+
|
|
1955
|
+
|
|
1956
|
+
# ---------------------------------------------------------------------------
|
|
1957
|
+
# Async save
|
|
1958
|
+
|
|
1959
|
+
|
|
1960
|
+
class AsyncSaveHandle:
|
|
1961
|
+
"""Handle returned by :func:`save_checkpoint_async`.
|
|
1962
|
+
|
|
1963
|
+
``result()`` blocks and re-raises any save error on the caller's thread;
|
|
1964
|
+
``done()``/``cancel()`` mirror ``concurrent.futures.Future``. The returned
|
|
1965
|
+
metadata object is the same one ``torch.distributed.checkpoint.save``
|
|
1966
|
+
returns.
|
|
1967
|
+
"""
|
|
1968
|
+
|
|
1969
|
+
def __init__(self, future, writer, executor):
|
|
1970
|
+
self._future = future
|
|
1971
|
+
self._writer = writer
|
|
1972
|
+
self._executor = executor
|
|
1973
|
+
self.metadata = None
|
|
1974
|
+
|
|
1975
|
+
def result(self, timeout=None):
|
|
1976
|
+
metadata = self._future.result(timeout=timeout)
|
|
1977
|
+
self.metadata = metadata
|
|
1978
|
+
return metadata
|
|
1979
|
+
|
|
1980
|
+
def done(self):
|
|
1981
|
+
return self._future.done()
|
|
1982
|
+
|
|
1983
|
+
def cancel(self):
|
|
1984
|
+
return self._future.cancel()
|
|
1985
|
+
|
|
1986
|
+
def add_done_callback(self, callback):
|
|
1987
|
+
return self._future.add_done_callback(callback)
|
|
1988
|
+
|
|
1989
|
+
@property
|
|
1990
|
+
def writer(self):
|
|
1991
|
+
return self._writer
|
|
1992
|
+
|
|
1993
|
+
@property
|
|
1994
|
+
def stats(self):
|
|
1995
|
+
return self._writer.stats
|
|
1996
|
+
|
|
1997
|
+
def wait(self, timeout=None):
|
|
1998
|
+
return self.result(timeout=timeout)
|
|
1999
|
+
|
|
2000
|
+
def __enter__(self):
|
|
2001
|
+
return self
|
|
2002
|
+
|
|
2003
|
+
def __exit__(self, exc_type, exc, tb):
|
|
2004
|
+
if exc_type is None:
|
|
2005
|
+
self.result()
|
|
2006
|
+
self._executor.shutdown(wait=True, cancel_futures=True)
|
|
2007
|
+
return False
|
|
2008
|
+
|
|
2009
|
+
|
|
2010
|
+
def save_checkpoint_async(
|
|
2011
|
+
state_dict,
|
|
2012
|
+
*,
|
|
2013
|
+
checkpoint_id,
|
|
2014
|
+
store_root,
|
|
2015
|
+
topology=1,
|
|
2016
|
+
axis=None,
|
|
2017
|
+
shard_strategy="zstd",
|
|
2018
|
+
staging_bytes=DEFAULT_STAGING_BYTES,
|
|
2019
|
+
overwrite=True,
|
|
2020
|
+
init_store=False,
|
|
2021
|
+
planner=None,
|
|
2022
|
+
weftfs_cli=None,
|
|
2023
|
+
timeout=DEFAULT_TIMEOUT_SECONDS,
|
|
2024
|
+
max_workers=1,
|
|
2025
|
+
):
|
|
2026
|
+
"""Run ``torch.distributed.checkpoint.save`` on a worker thread.
|
|
2027
|
+
|
|
2028
|
+
The encode/store work (resolving tensors, copying them to host memory,
|
|
2029
|
+
writing the staging safetensors container, running ``weftfs checkpoint
|
|
2030
|
+
write``, fsync, metadata publication) happens on a
|
|
2031
|
+
:class:`~concurrent.futures.ThreadPoolExecutor` worker, so a training loop
|
|
2032
|
+
can keep stepping while the checkpoint is stored.
|
|
2033
|
+
|
|
2034
|
+
This still **copies every tensor through host memory** (one tensor at a time,
|
|
2035
|
+
bounded by ``staging_bytes``); it is not a GPU-direct or zero-copy path. The
|
|
2036
|
+
worker count is bounded (default 1) and every worker uses the same hard
|
|
2037
|
+
staging bound.
|
|
2038
|
+
"""
|
|
2039
|
+
_require_torch("save_checkpoint_async")
|
|
2040
|
+
import torch.distributed.checkpoint as dcp
|
|
2041
|
+
|
|
2042
|
+
workers = int(max_workers)
|
|
2043
|
+
if workers < 1 or workers > 4:
|
|
2044
|
+
raise ValueError("max_workers must be between 1 and 4")
|
|
2045
|
+
writer = WeftStorageWriter(
|
|
2046
|
+
checkpoint_id,
|
|
2047
|
+
store_root=store_root,
|
|
2048
|
+
topology=topology,
|
|
2049
|
+
axis=axis,
|
|
2050
|
+
shard_strategy=shard_strategy,
|
|
2051
|
+
staging_bytes=staging_bytes,
|
|
2052
|
+
overwrite=overwrite,
|
|
2053
|
+
init_store=init_store,
|
|
2054
|
+
weftfs_cli=weftfs_cli,
|
|
2055
|
+
timeout=timeout,
|
|
2056
|
+
)
|
|
2057
|
+
executor = ThreadPoolExecutor(
|
|
2058
|
+
max_workers=workers, thread_name_prefix="weft-dcp-save"
|
|
2059
|
+
)
|
|
2060
|
+
|
|
2061
|
+
def _run():
|
|
2062
|
+
try:
|
|
2063
|
+
return dcp.save(
|
|
2064
|
+
state_dict,
|
|
2065
|
+
storage_writer=writer,
|
|
2066
|
+
planner=planner,
|
|
2067
|
+
no_dist=True,
|
|
2068
|
+
)
|
|
2069
|
+
finally:
|
|
2070
|
+
executor.shutdown(wait=False)
|
|
2071
|
+
|
|
2072
|
+
future = executor.submit(_run)
|
|
2073
|
+
return AsyncSaveHandle(future, writer, executor)
|
|
2074
|
+
|
|
2075
|
+
|
|
2076
|
+
# ---------------------------------------------------------------------------
|
|
2077
|
+
# Convenience helpers used by tests, docs, and the bench harness
|
|
2078
|
+
|
|
2079
|
+
|
|
2080
|
+
def init_store(store_root, *, weftfs_cli=None, timeout=DEFAULT_TIMEOUT_SECONDS):
|
|
2081
|
+
"""Create a WeftFS store at ``store_root`` if it does not exist."""
|
|
2082
|
+
store = _Storage(store_root, cli=weftfs_cli, timeout=timeout)
|
|
2083
|
+
store.init()
|
|
2084
|
+
store.requires_store()
|
|
2085
|
+
return Path(store_root)
|
|
2086
|
+
|
|
2087
|
+
|
|
2088
|
+
def verify_checkpoint(
|
|
2089
|
+
store_root,
|
|
2090
|
+
name,
|
|
2091
|
+
*,
|
|
2092
|
+
full=False,
|
|
2093
|
+
weftfs_cli=None,
|
|
2094
|
+
timeout=DEFAULT_TIMEOUT_SECONDS,
|
|
2095
|
+
):
|
|
2096
|
+
"""Run ``checkpoint verify`` (and optionally ``verify --full``).
|
|
2097
|
+
|
|
2098
|
+
Returns ``{"checkpoint": ..., "tensors_verified": N, "full": {...} | None}``.
|
|
2099
|
+
Every failure is a :class:`StoreCommandError`; nothing is swallowed.
|
|
2100
|
+
"""
|
|
2101
|
+
store = _Storage(store_root, cli=weftfs_cli, timeout=timeout)
|
|
2102
|
+
store.requires_store()
|
|
2103
|
+
result = {"checkpoint": name, "tensors_verified": None, "full": None}
|
|
2104
|
+
payload = store.checkpoint_verify(name)
|
|
2105
|
+
result["tensors_verified"] = payload.get("tensors_verified")
|
|
2106
|
+
if full:
|
|
2107
|
+
result["full"] = store.verify_full()
|
|
2108
|
+
return result
|