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/orbax.py ADDED
@@ -0,0 +1,1734 @@
1
+ """JAX / Orbax checkpoint adapter over WeftFS.
2
+
3
+ ``weftcodec.orbax`` provides an Orbax ``CheckpointHandler`` that stores a JAX
4
+ PyTree as one canonical ``weft.checkpoint.v1`` checkpoint through the shipped
5
+ Rust CLI. It reuses the storage plumbing of :mod:`weftcodec.dcp` and does not
6
+ introduce a wire format: leaves are written into a safetensors staging
7
+ container, the Rust CLI ingests that container, and a canonical WCB record
8
+ (``weft.orbax.v1``) binds each flattened leaf path to its dtype, shape,
9
+ content root, and logical root.
10
+
11
+ The "no silent fallback" rules of the DCP adapter apply unchanged:
12
+
13
+ * a missing CLI, store, tensor, or metadata record is a typed error;
14
+ * every restored leaf is checked against the recorded BLAKE3 content root,
15
+ dtype, shape, and the checkpoint manifest;
16
+ * a target PyTree with a missing, extra, or differently typed leaf fails
17
+ closed instead of guessing;
18
+ * staging is bounded (``staging_bytes``) and never silently exceeded.
19
+
20
+ Orbax and JAX are optional. Importing :mod:`weftcodec.orbax` without them works
21
+ and exposes the same classes/methods (contract mode); operations that need
22
+ JAX raise :class:`JaxRequired`.
23
+
24
+ Not claimed: GPU/TPU I/O, multi-process or multi-host JAX, a full ``orbax``
25
+ replacement, TensorStore/OCDBT compatibility, or a zero-copy path. The adapter
26
+ copies each leaf through host memory, one leaf at a time, bounded by
27
+ ``staging_bytes``.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import dataclasses
33
+ import importlib
34
+ import os
35
+ import shutil
36
+ import struct
37
+ import tempfile
38
+ import threading
39
+ import time
40
+ from concurrent.futures import ThreadPoolExecutor
41
+ from pathlib import Path
42
+
43
+ from . import wcb
44
+ from .dcp import (
45
+ DEFAULT_CLI,
46
+ DEFAULT_STAGING_BYTES,
47
+ DEFAULT_TIMEOUT_SECONDS,
48
+ DcpError,
49
+ ExactnessError,
50
+ InvalidCheckpointId,
51
+ InvalidMetadata,
52
+ MetadataMissing,
53
+ SaveFailed,
54
+ StagingLimitExceeded,
55
+ StoreCommandError,
56
+ StoreUnavailable,
57
+ AsyncSaveHandle,
58
+ _Storage,
59
+ _generation,
60
+ _parse_safetensors,
61
+ _parse_topology,
62
+ _validate_checkpoint_id,
63
+ init_store,
64
+ verify_checkpoint,
65
+ )
66
+ from .hashing import content_root
67
+ from .service import CheckpointService, ServiceError
68
+ from .dcp import _default_cli
69
+
70
+ __all__ = [
71
+ "DEFAULT_STAGING_BYTES",
72
+ "JAX_AVAILABLE",
73
+ "JAX_VERSION",
74
+ "ORBAX_AVAILABLE",
75
+ "ORBAX_METADATA_NAME",
76
+ "ORBAX_METADATA_SCHEMA",
77
+ "ORBAX_POINTER_NAME",
78
+ "ORBAX_VERSION",
79
+ "OrbaxAdapterError",
80
+ "OrbaxLeaf",
81
+ "OrbaxMetadata",
82
+ "OrbaxSaveResult",
83
+ "DuplicateLeafPath",
84
+ "JaxRequired",
85
+ "MismatchedLeaf",
86
+ "MissingLeaf",
87
+ "UnexpectedLeaf",
88
+ "UnsupportedLeaf",
89
+ "UnsupportedDtype",
90
+ "WeftOrbaxHandler",
91
+ "WeftSaveArgs",
92
+ "WeftRestoreArgs",
93
+ "flatten_pytree",
94
+ "leaf_paths",
95
+ "metadata_of",
96
+ "restore_pytree",
97
+ "save_async_pytree",
98
+ "save_pytree",
99
+ "verify_checkpoint",
100
+ ]
101
+
102
+ ORBAX_METADATA_SCHEMA = "weft.orbax.v1"
103
+ ORBAX_POINTER_SCHEMA = "weft.orbax.pointer.v1"
104
+ ORBAX_METADATA_NAME = "orbax.metadata"
105
+ ORBAX_POINTER_NAME = "weft.orbax.wcb"
106
+
107
+
108
+ # ---------------------------------------------------------------------------
109
+ # Optional JAX / Orbax. Importing weftcodec never imports these modules.
110
+
111
+
112
+ def _load_jax():
113
+ try:
114
+ return importlib.import_module("jax")
115
+ except Exception: # ImportError or a broken optional install
116
+ return None
117
+
118
+
119
+ _JAX = _load_jax()
120
+ JAX_AVAILABLE = _JAX is not None
121
+ JAX_VERSION = getattr(_JAX, "__version__", None) if _JAX is not None else None
122
+
123
+ _NP = None
124
+ _ML_DTYPES = None
125
+ if JAX_AVAILABLE: # pragma: no branch - both paths are exercised by the court
126
+ try:
127
+ import numpy as _NP # noqa: F401
128
+ import ml_dtypes as _ML_DTYPES # noqa: F401
129
+ _OPTIONAL_IMPORT_ERROR = None
130
+ except Exception as exc: # pragma: no cover - defensive
131
+ _JAX = None
132
+ JAX_AVAILABLE = False
133
+ JAX_VERSION = None
134
+ _OPTIONAL_IMPORT_ERROR = str(exc)
135
+ else:
136
+ _OPTIONAL_IMPORT_ERROR = "jax is not installed"
137
+
138
+
139
+ def _load_orbax():
140
+ try:
141
+ return importlib.import_module("orbax.checkpoint")
142
+ except Exception:
143
+ return None
144
+
145
+
146
+ _ORBAX = _load_orbax()
147
+ ORBAX_AVAILABLE = _ORBAX is not None
148
+ ORBAX_VERSION = getattr(_ORBAX, "__version__", None) if _ORBAX is not None else None
149
+
150
+ if ORBAX_AVAILABLE:
151
+ try:
152
+ from orbax.checkpoint import AsyncCheckpointHandler as _AsyncCheckpointHandler
153
+ from orbax.checkpoint import CheckpointHandler as _CheckpointHandler
154
+ from orbax.checkpoint import args as _orbax_args
155
+
156
+ _ORBAX_IMPORT_ERROR = None
157
+ except Exception as exc: # pragma: no cover - defensive
158
+ ORBAX_AVAILABLE = False
159
+ ORBAX_VERSION = None
160
+ _ORBAX = None
161
+ _ORBAX_IMPORT_ERROR = str(exc)
162
+ else: # pragma: no cover - the court always has orbax
163
+ _ORBAX_IMPORT_ERROR = "orbax.checkpoint is not installed"
164
+
165
+ if not ORBAX_AVAILABLE:
166
+
167
+ class _CheckpointHandler: # type: ignore[no-redef]
168
+ """Contract-mode stand-in when Orbax is absent."""
169
+
170
+ class _AsyncCheckpointHandler(_CheckpointHandler): # type: ignore[no-redef]
171
+ """Contract-mode stand-in when Orbax is absent."""
172
+
173
+ class _CheckpointArgs: # type: ignore[no-redef]
174
+ """Contract-mode stand-in for ``orbax.checkpoint.args.CheckpointArgs``."""
175
+
176
+ else:
177
+ _CheckpointArgs = _orbax_args.CheckpointArgs
178
+
179
+
180
+ def _require_jax(operation):
181
+ if not JAX_AVAILABLE:
182
+ raise JaxRequired(
183
+ "%s requires JAX (install with `pip install jax[cpu] orbax-checkpoint`); "
184
+ "weftcodec.orbax imported in contract mode: %s"
185
+ % (operation, _OPTIONAL_IMPORT_ERROR)
186
+ )
187
+
188
+
189
+ def _orbax_version():
190
+ return ORBAX_VERSION or "unavailable"
191
+
192
+
193
+ # ---------------------------------------------------------------------------
194
+ # Typed errors
195
+
196
+
197
+ class OrbaxAdapterError(DcpError):
198
+ """Base class for Orbax-adapter-specific failures."""
199
+
200
+
201
+ class JaxRequired(OrbaxAdapterError):
202
+ """This operation needs JAX but it is not installed."""
203
+
204
+
205
+ class UnsupportedLeaf(OrbaxAdapterError):
206
+ """A PyTree leaf is not an array with an exact WEFT-TENSOR v1 dtype."""
207
+
208
+
209
+ class UnsupportedDtype(OrbaxAdapterError):
210
+ """A leaf dtype has no exact safetensors/WEFT-TENSOR v1 mapping."""
211
+
212
+
213
+ class DuplicateLeafPath(OrbaxAdapterError):
214
+ """Two flattened leaves produced the same canonical path."""
215
+
216
+
217
+ class MissingLeaf(OrbaxAdapterError):
218
+ """A stored leaf is absent from the restore target PyTree."""
219
+
220
+
221
+ class UnexpectedLeaf(OrbaxAdapterError):
222
+ """The restore target has a leaf that is not in the checkpoint."""
223
+
224
+
225
+ class MismatchedLeaf(OrbaxAdapterError):
226
+ """A target leaf dtype or shape disagrees with the stored leaf."""
227
+
228
+
229
+ # ---------------------------------------------------------------------------
230
+ # Canonical leaf identity and WCB records
231
+
232
+
233
+ def _u(value):
234
+ return wcb.U64(int(value))
235
+
236
+
237
+ def _i(value):
238
+ if isinstance(value, wcb.U64):
239
+ return int(value.value)
240
+ if isinstance(value, wcb.I64):
241
+ return int(value.value)
242
+ return int(value)
243
+
244
+
245
+ def _u_list(values):
246
+ return [_u(value) for value in values]
247
+
248
+
249
+ def _i_list(values):
250
+ return tuple(_i(value) for value in values)
251
+
252
+
253
+ #: canonical WEFT-TENSOR dtype -> (safetensors spelling, element bytes, numpy
254
+ #: dtype factory name). Only these dtypes have an exact Rust mapping.
255
+ _DTYPE_TABLE = {
256
+ "f16": ("F16", 2, "float16"),
257
+ "bf16": ("BF16", 2, "bfloat16"),
258
+ "f32": ("F32", 4, "float32"),
259
+ "f64": ("F64", 8, "float64"),
260
+ "i8": ("I8", 1, "int8"),
261
+ "u8": ("U8", 1, "uint8"),
262
+ "i16": ("I16", 2, "int16"),
263
+ "i32": ("I32", 4, "int32"),
264
+ "i64": ("I64", 8, "int64"),
265
+ }
266
+
267
+ #: JAX/numpy dtype spelling -> canonical WEFT-TENSOR dtype.
268
+ _CANONICAL_BY_NAME = {
269
+ "float16": "f16",
270
+ "bfloat16": "bf16",
271
+ "float32": "f32",
272
+ "float64": "f64",
273
+ "int8": "i8",
274
+ "uint8": "u8",
275
+ "int16": "i16",
276
+ "int32": "i32",
277
+ "int64": "i64",
278
+ }
279
+
280
+
281
+ def _dtype_name(dtype) -> str:
282
+ """Normalise a JAX/numpy dtype to its short spelling."""
283
+ return str(dtype)
284
+
285
+
286
+ def _canonical_dtype(dtype) -> str:
287
+ name = _dtype_name(dtype)
288
+ try:
289
+ return _CANONICAL_BY_NAME[name]
290
+ except KeyError:
291
+ raise UnsupportedDtype(
292
+ "dtype %s has no exact WEFT-TENSOR/safetensors v1 mapping; "
293
+ "supported: %s"
294
+ % (name, ", ".join(sorted(_CANONICAL_BY_NAME)))
295
+ )
296
+
297
+
298
+ def _safetensors_dtype(canonical: str) -> str:
299
+ try:
300
+ return _DTYPE_TABLE[canonical][0]
301
+ except KeyError:
302
+ raise UnsupportedDtype("dtype %s has no safetensors mapping" % canonical)
303
+
304
+
305
+ def _element_bytes(canonical: str) -> int:
306
+ try:
307
+ return _DTYPE_TABLE[canonical][1]
308
+ except KeyError:
309
+ raise UnsupportedDtype("dtype %s has no element size" % canonical)
310
+
311
+
312
+ def _numpy_dtype(canonical: str):
313
+ _require_jax("materialising a restored leaf")
314
+ try:
315
+ name = _DTYPE_TABLE[canonical][2]
316
+ except KeyError:
317
+ raise UnsupportedDtype("dtype %s has no numpy mapping" % canonical)
318
+ if name == "bfloat16":
319
+ if _ML_DTYPES is None: # pragma: no cover - jax always brings ml_dtypes
320
+ raise UnsupportedDtype("bfloat16 needs ml_dtypes")
321
+ return _NP.dtype(_ML_DTYPES.bfloat16)
322
+ return _NP.dtype(name)
323
+
324
+
325
+ def _canonical_path(path_tuple) -> str:
326
+ try:
327
+ text = _JAX.tree_util.keystr(path_tuple)
328
+ except Exception: # pragma: no cover - defensive for exotic key types
329
+ text = "".join(repr(key) for key in path_tuple)
330
+ return text or "<root>"
331
+
332
+
333
+ def _path_types(path_tuple):
334
+ return [type(key).__name__ for key in path_tuple]
335
+
336
+
337
+ def _shape_product(shape) -> int:
338
+ product = 1
339
+ for extent in shape:
340
+ extent = int(extent)
341
+ if extent < 0:
342
+ raise OrbaxAdapterError("negative leaf extent %d" % extent)
343
+ product *= extent
344
+ return product
345
+
346
+
347
+ @dataclasses.dataclass(frozen=True)
348
+ class OrbaxLeaf:
349
+ """One flattened leaf as recorded in ``weft.orbax.v1``.
350
+
351
+ ``element_bytes`` is the total tensor byte length, matching the manifest's
352
+ ``element_bytes`` field (not the per-element width).
353
+ """
354
+
355
+ entry: str
356
+ path: str
357
+ path_types: tuple
358
+ dtype: str
359
+ shape: tuple
360
+ element_bytes: int
361
+ content_root: bytes
362
+ logical_root: bytes
363
+
364
+ def to_value(self):
365
+ return {
366
+ "entry": self.entry,
367
+ "path": self.path,
368
+ "path_types": list(self.path_types),
369
+ "dtype": self.dtype,
370
+ "shape": _u_list(self.shape),
371
+ "element_bytes": _u(self.element_bytes),
372
+ "content_root": self.content_root,
373
+ "logical_root": self.logical_root,
374
+ }
375
+
376
+ @classmethod
377
+ def from_value(cls, value):
378
+ try:
379
+ shape = _i_list(value["shape"])
380
+ element_bytes = _i(value["element_bytes"])
381
+ content = bytes(value["content_root"])
382
+ logical = bytes(value["logical_root"])
383
+ dtype = value["dtype"]
384
+ entry = value["entry"]
385
+ path = value["path"]
386
+ path_types = tuple(str(item) for item in value["path_types"])
387
+ except (KeyError, TypeError, ValueError) as exc:
388
+ raise InvalidMetadata("bad orbax leaf record: %s" % exc) from exc
389
+ if dtype not in _DTYPE_TABLE:
390
+ raise InvalidMetadata("orbax leaf %r has unknown dtype %r" % (path, dtype))
391
+ expected = _shape_product(shape) * _element_bytes(dtype)
392
+ if element_bytes != expected:
393
+ raise InvalidMetadata(
394
+ "orbax leaf %r element_bytes %d != %d (shape * width) for dtype %s"
395
+ % (path, element_bytes, expected, dtype)
396
+ )
397
+ if len(content) != 32 or len(logical) != 32:
398
+ raise InvalidMetadata("orbax leaf %r roots must be 32 bytes" % path)
399
+ if not entry or not path:
400
+ raise InvalidMetadata("orbax leaf entry/path must be non-empty")
401
+ return cls(
402
+ entry=entry,
403
+ path=path,
404
+ path_types=path_types,
405
+ dtype=dtype,
406
+ shape=shape,
407
+ element_bytes=element_bytes,
408
+ content_root=content,
409
+ logical_root=logical,
410
+ )
411
+
412
+
413
+ @dataclasses.dataclass(frozen=True)
414
+ class OrbaxMetadata:
415
+ """Decoded ``weft.orbax.v1`` metadata record."""
416
+
417
+ checkpoint_id: str
418
+ storage_name: str
419
+ leaves: tuple
420
+ write_topology: tuple
421
+ shard_strategy: str
422
+ staging_bytes: int
423
+ source_file_bytes: int
424
+ source_sha256: str
425
+ jax_version: str
426
+ orbax_version: str
427
+
428
+ @property
429
+ def leaf_count(self):
430
+ return len(self.leaves)
431
+
432
+ @property
433
+ def total_element_bytes(self):
434
+ return sum(leaf.element_bytes for leaf in self.leaves)
435
+
436
+ def by_path(self):
437
+ return {leaf.path: leaf for leaf in self.leaves}
438
+
439
+ def by_entry(self):
440
+ return {leaf.entry: leaf for leaf in self.leaves}
441
+
442
+ def to_value(self):
443
+ return {
444
+ "schema": ORBAX_METADATA_SCHEMA,
445
+ "checkpoint_id": self.checkpoint_id,
446
+ "storage_name": self.storage_name,
447
+ "leaf_count": _u(self.leaf_count),
448
+ "leaves": [leaf.to_value() for leaf in self.leaves],
449
+ "write_topology": _u_list(self.write_topology),
450
+ "shard_strategy": self.shard_strategy,
451
+ "staging_bytes": _u(self.staging_bytes),
452
+ "source_file_bytes": _u(self.source_file_bytes),
453
+ "source_sha256": self.source_sha256,
454
+ "jax_version": self.jax_version,
455
+ "orbax_version": self.orbax_version,
456
+ }
457
+
458
+
459
+ def serialize_metadata(metadata: OrbaxMetadata) -> bytes:
460
+ return wcb.encode(metadata.to_value())
461
+
462
+
463
+ _METADATA_KEYS = {
464
+ "schema",
465
+ "checkpoint_id",
466
+ "storage_name",
467
+ "leaf_count",
468
+ "leaves",
469
+ "write_topology",
470
+ "shard_strategy",
471
+ "staging_bytes",
472
+ "source_file_bytes",
473
+ "source_sha256",
474
+ "jax_version",
475
+ "orbax_version",
476
+ }
477
+
478
+
479
+ def metadata_from_value(value) -> OrbaxMetadata:
480
+ if not isinstance(value, dict) or value.get("schema") != ORBAX_METADATA_SCHEMA:
481
+ raise InvalidMetadata(
482
+ "orbax metadata schema is %r, expected %r"
483
+ % (
484
+ value.get("schema") if isinstance(value, dict) else None,
485
+ ORBAX_METADATA_SCHEMA,
486
+ )
487
+ )
488
+ if set(value) != _METADATA_KEYS:
489
+ raise InvalidMetadata(
490
+ "orbax metadata keys %r != the exact schema key set"
491
+ % sorted(value)
492
+ )
493
+ leaves = tuple(OrbaxLeaf.from_value(row) for row in value["leaves"])
494
+ if _i(value["leaf_count"]) != len(leaves):
495
+ raise InvalidMetadata(
496
+ "orbax metadata leaf_count %d != %d leaf records"
497
+ % (_i(value["leaf_count"]), len(leaves))
498
+ )
499
+ if len({leaf.entry for leaf in leaves}) != len(leaves):
500
+ raise InvalidMetadata("orbax metadata has duplicate entry names")
501
+ if len({leaf.path for leaf in leaves}) != len(leaves):
502
+ raise InvalidMetadata("orbax metadata has duplicate leaf paths")
503
+ topology = _i_list(value["write_topology"])
504
+ if len(topology) != 2:
505
+ raise InvalidMetadata("write_topology must be [shards, axis]")
506
+ shards, axis = topology
507
+ if shards < 1 or axis < 0:
508
+ raise InvalidMetadata("invalid write_topology %r" % (topology,))
509
+ source_sha = value["source_sha256"]
510
+ if not isinstance(source_sha, str) or len(source_sha) != 64:
511
+ raise InvalidMetadata("source_sha256 must be 64 hex characters")
512
+ try:
513
+ bytes.fromhex(source_sha)
514
+ except ValueError as exc:
515
+ raise InvalidMetadata("source_sha256 is not hex: %s" % exc) from exc
516
+ for key in ("checkpoint_id", "storage_name", "shard_strategy"):
517
+ if not isinstance(value[key], str) or not value[key]:
518
+ raise InvalidMetadata("%s must be a non-empty string" % key)
519
+ return OrbaxMetadata(
520
+ checkpoint_id=value["checkpoint_id"],
521
+ storage_name=value["storage_name"],
522
+ leaves=leaves,
523
+ write_topology=(int(shards), int(axis)),
524
+ shard_strategy=value["shard_strategy"],
525
+ staging_bytes=_i(value["staging_bytes"]),
526
+ source_file_bytes=_i(value["source_file_bytes"]),
527
+ source_sha256=source_sha,
528
+ jax_version=str(value["jax_version"]),
529
+ orbax_version=str(value["orbax_version"]),
530
+ )
531
+
532
+
533
+ def serialize_pointer(*, store_root, checkpoint_id, storage_name, metadata_bytes):
534
+ value = {
535
+ "schema": ORBAX_POINTER_SCHEMA,
536
+ "store_root": os.path.abspath(os.fspath(store_root)),
537
+ "checkpoint_id": checkpoint_id,
538
+ "storage_name": storage_name,
539
+ "metadata_root": content_root(metadata_bytes),
540
+ }
541
+ return wcb.encode(value)
542
+
543
+
544
+ _POINTER_KEYS = {
545
+ "schema",
546
+ "store_root",
547
+ "checkpoint_id",
548
+ "storage_name",
549
+ "metadata_root",
550
+ }
551
+
552
+
553
+ def pointer_from_value(value) -> dict:
554
+ if not isinstance(value, dict) or value.get("schema") != ORBAX_POINTER_SCHEMA:
555
+ raise InvalidMetadata(
556
+ "orbax pointer schema is %r, expected %r"
557
+ % (
558
+ value.get("schema") if isinstance(value, dict) else None,
559
+ ORBAX_POINTER_SCHEMA,
560
+ )
561
+ )
562
+ if set(value) != _POINTER_KEYS:
563
+ raise InvalidMetadata(
564
+ "orbax pointer keys %r != the exact schema key set" % sorted(value)
565
+ )
566
+ root = bytes(value["metadata_root"])
567
+ if len(root) != 32:
568
+ raise InvalidMetadata("orbax pointer metadata_root must be 32 bytes")
569
+ for key in ("store_root", "checkpoint_id", "storage_name"):
570
+ if not isinstance(value[key], str) or not value[key]:
571
+ raise InvalidMetadata("orbax pointer %s must be a non-empty string" % key)
572
+ return {
573
+ "store_root": value["store_root"],
574
+ "checkpoint_id": value["checkpoint_id"],
575
+ "storage_name": value["storage_name"],
576
+ "metadata_root": root,
577
+ }
578
+
579
+
580
+ # ---------------------------------------------------------------------------
581
+ # Orbax argument dataclasses (registered with the real handler when available)
582
+
583
+
584
+ @dataclasses.dataclass
585
+ class WeftSaveArgs(_CheckpointArgs):
586
+ """Arguments for one WeftFS/Orbax save.
587
+
588
+ ``checkpoint_id`` is the checkpoint name inside the WeftFS store (a single
589
+ store name, not a filesystem path). The Orbax directory passed to the
590
+ handler is a locator: it records the store path and the exact canonical
591
+ metadata record.
592
+ """
593
+
594
+ item: object = None
595
+ store_root: object = None
596
+ checkpoint_id: object = None
597
+ write_topology: object = (1, 0)
598
+ shard_strategy: str = "zstd"
599
+ staging_bytes: object = None
600
+ overwrite: bool = True
601
+ init_store: bool = False
602
+
603
+
604
+ @dataclasses.dataclass
605
+ class WeftRestoreArgs(_CheckpointArgs):
606
+ """Arguments for one WeftFS/Orbax restore.
607
+
608
+ ``item`` is the target PyTree (concrete arrays or ``jax.ShapeDtypeStruct``).
609
+ ``restore_topology`` is an optional Rust-side proof layout
610
+ (``n`` or ``(n, axis)``); it is independent of the JAX target sharding.
611
+ """
612
+
613
+ item: object = None
614
+ store_root: object = None
615
+ checkpoint_id: object = None
616
+ storage_name: object = None
617
+ restore_topology: object = None
618
+ partial_restore: bool = False
619
+ staging_bytes: object = None
620
+
621
+
622
+ # Registration happens after the handler class is defined (below).
623
+
624
+
625
+ # ---------------------------------------------------------------------------
626
+ # Result objects
627
+
628
+
629
+ @dataclasses.dataclass(frozen=True)
630
+ class OrbaxSaveResult:
631
+ """What one save published."""
632
+
633
+ checkpoint_id: str
634
+ storage_name: str
635
+ store_root: str
636
+ directory: str
637
+ leaves: tuple
638
+ write_topology: tuple
639
+ shard_strategy: str
640
+ staging_bytes: int
641
+ staging_peak_bytes: int
642
+ checkpoint_write_ms: float
643
+ store_command_ms: float
644
+ source_file_bytes: int
645
+ source_sha256: str
646
+ manifest: dict
647
+ metadata: OrbaxMetadata
648
+
649
+ @property
650
+ def leaf_count(self):
651
+ return len(self.leaves)
652
+
653
+ @property
654
+ def leaf_bytes(self):
655
+ return sum(leaf.element_bytes for leaf in self.leaves)
656
+
657
+ def stats(self):
658
+ return {
659
+ "checkpoint_id": self.checkpoint_id,
660
+ "storage_name": self.storage_name,
661
+ "store_root": self.store_root,
662
+ "directory": self.directory,
663
+ "leaf_count": self.leaf_count,
664
+ "leaf_bytes": self.leaf_bytes,
665
+ "staging_bytes": self.staging_bytes,
666
+ "staging_peak_bytes": self.staging_peak_bytes,
667
+ "checkpoint_write_ms": self.checkpoint_write_ms,
668
+ "store_command_ms": self.store_command_ms,
669
+ "source_file_bytes": self.source_file_bytes,
670
+ "source_sha256": self.source_sha256,
671
+ "write_topology": self.write_topology,
672
+ "shard_strategy": self.shard_strategy,
673
+ }
674
+
675
+
676
+ # ---------------------------------------------------------------------------
677
+ # Leaf materialisation / target validation
678
+
679
+
680
+ def _array_bytes(leaf):
681
+ _require_jax("serialising a PyTree leaf")
682
+ array = _NP.asarray(leaf)
683
+ if not array.flags.c_contiguous:
684
+ array = _NP.ascontiguousarray(array)
685
+ if array.dtype.byteorder == ">":
686
+ array = array.byteswap().view(array.dtype.newbyteorder("<"))
687
+ return array.tobytes(order="C")
688
+
689
+
690
+ def _leaf_dtype_shape(leaf):
691
+ _require_jax("inspecting a PyTree leaf")
692
+ if isinstance(leaf, _JAX.ShapeDtypeStruct):
693
+ return _dtype_name(leaf.dtype), tuple(int(x) for x in leaf.shape), leaf
694
+ if isinstance(leaf, _JAX.Array):
695
+ return _dtype_name(leaf.dtype), tuple(int(x) for x in leaf.shape), leaf
696
+ if _NP is not None and isinstance(leaf, _NP.ndarray):
697
+ return _dtype_name(leaf.dtype), tuple(int(x) for x in leaf.shape), leaf
698
+ raise UnsupportedLeaf(
699
+ "PyTree leaf %r is %s; only jax.Array, numpy.ndarray, and "
700
+ "jax.ShapeDtypeStruct are supported"
701
+ % (leaf, type(leaf).__name__)
702
+ )
703
+
704
+
705
+ def _target_sharding(leaf):
706
+ return getattr(leaf, "sharding", None)
707
+
708
+
709
+ def _to_target(buffer, *, canonical, shape, target):
710
+ """Materialise one restored leaf from its caller-owned memfd buffer.
711
+
712
+ ``buffer`` is the service's :class:`weftcodec.service.TensorBuffer`; the
713
+ dtype/shape were already verified against the metadata, so the mapping is
714
+ used directly. For a JAX destination the host array is handed to
715
+ ``device_put`` with no extra host copy; for a numpy destination the caller
716
+ owns the result, so exactly one bounded copy is made.
717
+ """
718
+ _require_jax("materialising a restored leaf")
719
+ np_dtype = _numpy_dtype(canonical)
720
+ array = _NP.frombuffer(buffer.view, dtype=np_dtype).reshape(tuple(shape))
721
+ if _NP is not None and isinstance(target, _NP.ndarray):
722
+ return _NP.array(array, copy=True, order="C")
723
+ jnp = _JAX.numpy
724
+ value = jnp.asarray(array)
725
+ sharding = _target_sharding(target)
726
+ if sharding is not None:
727
+ value = _JAX.device_put(value, sharding)
728
+ return value
729
+
730
+
731
+ # ---------------------------------------------------------------------------
732
+ # Async plumbing
733
+
734
+
735
+ class _LazyFuture:
736
+ """A bounded-worker future that starts work on first ``result()``.
737
+
738
+ Orbax's ``AsyncCheckpointer`` creates its temporary directory as one commit
739
+ operation and then waits on the handler's futures. Writing into that
740
+ directory before the creation operation completes is a real race (the
741
+ creation helper removes and recreates it). Starting the save only when
742
+ ``result()`` is called means the directory is ready, while the caller's
743
+ ``save()`` still returns immediately.
744
+ """
745
+
746
+ def __init__(self, executor, function):
747
+ self._executor = executor
748
+ self._function = function
749
+ self._lock = threading.Lock()
750
+ self._future = None
751
+ self._cancelled = False
752
+
753
+ def result(self, timeout=None):
754
+ with self._lock:
755
+ if self._cancelled:
756
+ raise RuntimeError("orbax save future was cancelled")
757
+ if self._future is None:
758
+ self._future = self._executor.submit(self._function)
759
+ future = self._future
760
+ return future.result(timeout=timeout)
761
+
762
+ def cancel(self):
763
+ with self._lock:
764
+ if self._future is not None:
765
+ return self._future.cancel()
766
+ self._cancelled = True
767
+ return True
768
+
769
+ def done(self):
770
+ with self._lock:
771
+ future = self._future
772
+ return future.done() if future is not None else self._cancelled
773
+
774
+
775
+ # ---------------------------------------------------------------------------
776
+ # Handler
777
+
778
+
779
+ class WeftOrbaxHandler(_AsyncCheckpointHandler):
780
+ """Orbax ``AsyncCheckpointHandler`` that publishes canonical WeftFS checkpoints.
781
+
782
+ ``save``/``restore`` are also usable directly. The real Orbax
783
+ ``Checkpointer``, ``AsyncCheckpointer``, and (when the installed Orbax
784
+ supports it) ``CheckpointManager`` can drive this handler through
785
+ :class:`WeftSaveArgs` / :class:`WeftRestoreArgs`.
786
+
787
+ ``staging_bytes`` bounds the total leaf bytes of one staging container and
788
+ the total restored bytes materialised for one restore. The executor is
789
+ bounded to ``max_workers`` (1..4); each worker uses the same hard bound.
790
+ """
791
+
792
+ def __init__(
793
+ self,
794
+ *,
795
+ staging_bytes=DEFAULT_STAGING_BYTES,
796
+ max_workers=1,
797
+ weftfs_cli=None,
798
+ timeout=DEFAULT_TIMEOUT_SECONDS,
799
+ ):
800
+ staging_bytes = int(staging_bytes)
801
+ if staging_bytes <= 0:
802
+ raise ValueError("staging_bytes must be a positive byte count")
803
+ workers = int(max_workers)
804
+ if workers < 1 or workers > 4:
805
+ raise ValueError("max_workers must be between 1 and 4")
806
+ self.staging_bytes = staging_bytes
807
+ self.weftfs_cli = weftfs_cli
808
+ self.timeout = float(timeout)
809
+ self._executor = ThreadPoolExecutor(
810
+ max_workers=workers, thread_name_prefix="weft-orbax-save"
811
+ )
812
+ self._service_cli = _default_cli(weftfs_cli)
813
+ self._service = None
814
+ self._service_root = None
815
+ self._lock = threading.RLock()
816
+ self._last_result = None
817
+ self._stats = {
818
+ "saves": 0,
819
+ "restores": 0,
820
+ "leaves_restored": 0,
821
+ "restored_bytes": 0,
822
+ }
823
+
824
+ # -- properties ---------------------------------------------------------
825
+
826
+ def _get_service(self, store_root):
827
+ """One helper process for every operation of this handler."""
828
+ root = os.path.abspath(os.fspath(store_root))
829
+ if self._service is None or self._service_root != root:
830
+ if self._service is not None:
831
+ self._service.close()
832
+ self._service = CheckpointService(
833
+ root, cli=self._service_cli, timeout=self.timeout
834
+ )
835
+ self._service_root = root
836
+ return self._service
837
+
838
+ @property
839
+ def stats(self):
840
+ with self._lock:
841
+ payload = dict(self._stats)
842
+ result = self.last_result
843
+ if result is not None:
844
+ payload["last_save"] = result.stats()
845
+ payload["staging_bytes"] = self.staging_bytes
846
+ return payload
847
+
848
+ @property
849
+ def last_result(self):
850
+ with self._lock:
851
+ return self._last_result
852
+
853
+ @classmethod
854
+ def typestr(cls):
855
+ return "%s.%s" % (cls.__module__, cls.__qualname__)
856
+
857
+ # -- Orbax interface ----------------------------------------------------
858
+
859
+ def save(self, directory, *args, **kwargs):
860
+ save_args = _extract_args(args, kwargs, WeftSaveArgs, "save")
861
+ return self._save_sync(Path(os.fspath(directory)), save_args)
862
+
863
+ async def async_save(self, directory, *args, **kwargs):
864
+ save_args = _extract_args(args, kwargs, WeftSaveArgs, "async_save")
865
+ if save_args.item is None:
866
+ raise SaveFailed("WeftSaveArgs.item is required")
867
+ return [
868
+ _LazyFuture(
869
+ self._executor,
870
+ lambda: self._save_sync(Path(os.fspath(directory)), save_args),
871
+ )
872
+ ]
873
+
874
+ def restore(self, directory, *args, **kwargs):
875
+ restore_args = _extract_args(args, kwargs, WeftRestoreArgs, "restore")
876
+ return self._restore_sync(Path(os.fspath(directory)), restore_args)
877
+
878
+ def metadata(self, directory):
879
+ return read_metadata(Path(os.fspath(directory)))[2]
880
+
881
+ def finalize(self, directory):
882
+ # The store publish and the locator write are already durable; Orbax
883
+ # owns the atomic directory rename/commit of the locator.
884
+ return None
885
+
886
+ def close(self):
887
+ if self._service is not None:
888
+ self._service.close()
889
+ self._service = None
890
+ self._service_root = None
891
+ self._executor.shutdown(wait=True, cancel_futures=False)
892
+
893
+ @staticmethod
894
+ def _translate(exc):
895
+ return _translate_service_error(exc)
896
+
897
+ # -- save ---------------------------------------------------------------
898
+
899
+ def _save_sync(self, directory, save_args):
900
+ _require_jax("writing a PyTree checkpoint")
901
+ if save_args.item is None:
902
+ raise SaveFailed("WeftSaveArgs.item is required")
903
+ store_root = _resolve_save_store_root(save_args.store_root)
904
+ checkpoint_id = _validate_checkpoint_id(save_args.checkpoint_id)
905
+ shards, axis = _parse_topology(save_args.write_topology)
906
+ if save_args.shard_strategy not in ("zstd", "raw", "tensor-sharded"):
907
+ raise ValueError(
908
+ "shard_strategy must be zstd, raw, or tensor-sharded "
909
+ "(tensor-tiled needs a tile shape this adapter does not expose)"
910
+ )
911
+ staging_bound = (
912
+ int(save_args.staging_bytes)
913
+ if save_args.staging_bytes is not None
914
+ else self.staging_bytes
915
+ )
916
+ if staging_bound <= 0:
917
+ raise ValueError("staging_bytes must be a positive byte count")
918
+
919
+ flat = _flatten(save_args.item)
920
+ if not flat:
921
+ raise SaveFailed("refusing to save an empty PyTree")
922
+ entries = _entry_names(len(flat))
923
+ leaves = []
924
+ total = 0
925
+ for index, (path_tuple, leaf) in enumerate(flat):
926
+ path = _canonical_path(path_tuple)
927
+ dtype_name, shape, _source = _leaf_dtype_shape(leaf)
928
+ canonical = _canonical_dtype(dtype_name)
929
+ nbytes = _shape_product(shape) * _element_bytes(canonical)
930
+ total += nbytes
931
+ if total > staging_bound:
932
+ raise StagingLimitExceeded(
933
+ "PyTree leaves need %d staged bytes, above the %d "
934
+ "staging_bytes bound" % (total, staging_bound)
935
+ )
936
+ leaves.append(
937
+ {
938
+ "entry": entries[index],
939
+ "path": path,
940
+ "path_types": tuple(_path_types(path_tuple)),
941
+ "dtype": canonical,
942
+ "shape": tuple(int(x) for x in shape),
943
+ "element_bytes": nbytes,
944
+ "nbytes": nbytes,
945
+ }
946
+ )
947
+ if len({leaf["path"] for leaf in leaves}) != len(leaves):
948
+ raise DuplicateLeafPath(
949
+ "flattened PyTree produced duplicate canonical leaf paths"
950
+ )
951
+
952
+ directory = Path(directory)
953
+ directory.mkdir(parents=True, exist_ok=True)
954
+ pointer_path = directory / ORBAX_POINTER_NAME
955
+ peak = 0
956
+ storage_name = None
957
+ store = _Storage(store_root, cli=self.weftfs_cli, timeout=self.timeout)
958
+ if save_args.init_store:
959
+ store.init()
960
+ store.requires_store()
961
+ service = self._get_service(store_root)
962
+ try:
963
+ header = {}
964
+ cursor = 0
965
+ for leaf in leaves:
966
+ header[leaf["entry"]] = {
967
+ "dtype": _safetensors_dtype(leaf["dtype"]),
968
+ "shape": list(leaf["shape"]),
969
+ "data_offsets": [cursor, cursor + leaf["nbytes"]],
970
+ }
971
+ leaf["offset"] = cursor
972
+ cursor += leaf["nbytes"]
973
+ header_raw = _json_bytes(header)
974
+ if len(header_raw) > 64 * 1024 * 1024:
975
+ raise SaveFailed("staging safetensors header exceeds 64 MiB")
976
+ # The container must exist as safetensors bytes (the checkpoint
977
+ # manifest records its header and SHA-256), but it is staged in
978
+ # memory only: no host file, no CLI argument, one memfd hand-off.
979
+ container = bytearray(struct.pack("<Q", len(header_raw)))
980
+ container += header_raw
981
+ for leaf, (path_tuple, source_leaf) in zip(leaves, flat):
982
+ payload = _array_bytes(source_leaf)
983
+ if len(payload) != leaf["nbytes"]:
984
+ raise SaveFailed(
985
+ "leaf %r produced %d bytes, header declared %d"
986
+ % (leaf["path"], len(payload), leaf["nbytes"])
987
+ )
988
+ peak = max(peak, len(payload))
989
+ container += payload
990
+ leaf["content_root"] = content_root(payload)
991
+
992
+ storage_name = _allocate_storage_name(
993
+ store, checkpoint_id, bool(save_args.overwrite)
994
+ )
995
+ started = time.monotonic()
996
+ write_report = service.write_checkpoint(
997
+ storage_name,
998
+ container,
999
+ shards=shards,
1000
+ axis=axis,
1001
+ strategy=save_args.shard_strategy,
1002
+ )
1003
+ checkpoint_write_ms = (time.monotonic() - started) * 1000.0
1004
+ manifest = write_report.manifest.to_value()
1005
+ show = {
1006
+ "manifest_root": write_report.manifest_root,
1007
+ "manifest": manifest,
1008
+ }
1009
+ _verify_manifest(show, leaves, storage_name)
1010
+
1011
+ manifest_rows = {row["name"]: row for row in manifest.get("tensors", [])}
1012
+ recorded = []
1013
+ for index, leaf in enumerate(leaves):
1014
+ row = manifest_rows[leaf["entry"]]
1015
+ recorded.append(
1016
+ OrbaxLeaf(
1017
+ entry=leaf["entry"],
1018
+ path=leaf["path"],
1019
+ path_types=leaf["path_types"],
1020
+ dtype=leaf["dtype"],
1021
+ shape=leaf["shape"],
1022
+ element_bytes=leaf["element_bytes"],
1023
+ content_root=leaf["content_root"],
1024
+ logical_root=bytes.fromhex(row["logical_root"]),
1025
+ )
1026
+ )
1027
+ recorded.sort(key=lambda item: item.path)
1028
+ metadata = OrbaxMetadata(
1029
+ checkpoint_id=checkpoint_id,
1030
+ storage_name=storage_name,
1031
+ leaves=tuple(recorded),
1032
+ write_topology=(int(shards), int(axis)),
1033
+ shard_strategy=save_args.shard_strategy,
1034
+ staging_bytes=staging_bound,
1035
+ source_file_bytes=int(manifest.get("source_file_bytes", 0)),
1036
+ source_sha256=str(manifest.get("source_sha256", "")),
1037
+ jax_version=JAX_VERSION or "unavailable",
1038
+ orbax_version=_orbax_version(),
1039
+ )
1040
+ metadata_bytes = serialize_metadata(metadata)
1041
+ service.write_file(
1042
+ "/checkpoints/%s/%s" % (storage_name, ORBAX_METADATA_NAME),
1043
+ metadata_bytes,
1044
+ )
1045
+ pointer_bytes = serialize_pointer(
1046
+ store_root=store_root,
1047
+ checkpoint_id=checkpoint_id,
1048
+ storage_name=storage_name,
1049
+ metadata_bytes=metadata_bytes,
1050
+ )
1051
+ with open(pointer_path, "wb") as handle:
1052
+ handle.write(pointer_bytes)
1053
+ handle.flush()
1054
+ os.fsync(handle.fileno())
1055
+ _fsync_directory(directory)
1056
+
1057
+ result = OrbaxSaveResult(
1058
+ checkpoint_id=checkpoint_id,
1059
+ storage_name=storage_name,
1060
+ store_root=os.fspath(store_root),
1061
+ directory=os.fspath(directory),
1062
+ leaves=tuple(recorded),
1063
+ write_topology=(int(shards), int(axis)),
1064
+ shard_strategy=save_args.shard_strategy,
1065
+ staging_bytes=staging_bound,
1066
+ staging_peak_bytes=peak,
1067
+ checkpoint_write_ms=checkpoint_write_ms,
1068
+ store_command_ms=store.command_ms,
1069
+ source_file_bytes=int(manifest.get("source_file_bytes", 0)),
1070
+ source_sha256=str(manifest.get("source_sha256", "")),
1071
+ manifest=manifest,
1072
+ metadata=metadata,
1073
+ )
1074
+ with self._lock:
1075
+ self._stats["saves"] += 1
1076
+ self._last_result = result
1077
+ return result
1078
+ finally:
1079
+ pass
1080
+
1081
+ # -- restore ------------------------------------------------------------
1082
+
1083
+ def _restore_sync(self, directory, restore_args):
1084
+ _require_jax("reading a PyTree checkpoint")
1085
+ target = restore_args.item
1086
+ if target is None:
1087
+ raise InvalidMetadata("WeftRestoreArgs.item is required")
1088
+ store, storage_name, metadata, show, _pointer = _resolve_metadata(
1089
+ directory,
1090
+ store_root=restore_args.store_root,
1091
+ checkpoint_id=restore_args.checkpoint_id,
1092
+ storage_name=restore_args.storage_name,
1093
+ weftfs_cli=self.weftfs_cli,
1094
+ timeout=self.timeout,
1095
+ service_factory=self._get_service,
1096
+ )
1097
+ _verify_metadata_manifest(metadata, show)
1098
+ staging_bound = (
1099
+ int(restore_args.staging_bytes)
1100
+ if restore_args.staging_bytes is not None
1101
+ else self.staging_bytes
1102
+ )
1103
+ restore_topology = restore_args.restore_topology
1104
+ shards, axis = (
1105
+ (None, 0)
1106
+ if restore_topology is None
1107
+ else _parse_topology(restore_topology)
1108
+ )
1109
+
1110
+ flat = _flatten(target)
1111
+ target_paths = [_canonical_path(path_tuple) for path_tuple, _ in flat]
1112
+ if len(set(target_paths)) != len(target_paths):
1113
+ raise DuplicateLeafPath("restore target has duplicate canonical paths")
1114
+ stored = metadata.by_path()
1115
+ missing = [path for path in stored if path not in set(target_paths)]
1116
+ extra = [path for path in target_paths if path not in stored]
1117
+ if extra:
1118
+ raise UnexpectedLeaf(
1119
+ "restore target has leaves absent from checkpoint %r: %r"
1120
+ % (storage_name, sorted(extra))
1121
+ )
1122
+ if missing and not restore_args.partial_restore:
1123
+ raise MissingLeaf(
1124
+ "checkpoint %r has leaves absent from the restore target: %r"
1125
+ % (storage_name, sorted(missing))
1126
+ )
1127
+ if not target_paths:
1128
+ raise InvalidMetadata("restore target has no leaves")
1129
+
1130
+ # Fail closed on a target dtype/shape mismatch before reading any data.
1131
+ for (path_tuple, leaf), path in zip(flat, target_paths):
1132
+ dtype_name, shape, _source = _leaf_dtype_shape(leaf)
1133
+ canonical = _canonical_dtype(dtype_name)
1134
+ leaf_meta = stored[path]
1135
+ if canonical != leaf_meta.dtype:
1136
+ raise MismatchedLeaf(
1137
+ "target leaf %r dtype %s != stored dtype %s"
1138
+ % (path, canonical, leaf_meta.dtype)
1139
+ )
1140
+ if tuple(int(x) for x in shape) != leaf_meta.shape:
1141
+ raise MismatchedLeaf(
1142
+ "target leaf %r shape %r != stored shape %r"
1143
+ % (path, tuple(shape), leaf_meta.shape)
1144
+ )
1145
+
1146
+ wanted = [stored[path] for path in target_paths]
1147
+ total_wanted = sum(leaf.element_bytes for leaf in wanted)
1148
+ if total_wanted > staging_bound:
1149
+ raise StagingLimitExceeded(
1150
+ "restore needs %d bytes, above the %d staging_bytes bound"
1151
+ % (total_wanted, staging_bound)
1152
+ )
1153
+
1154
+ all_entries = metadata.by_entry()
1155
+ full = len(wanted) == len(all_entries)
1156
+ buffers = {}
1157
+ try:
1158
+ for leaf in wanted:
1159
+ try:
1160
+ buffer = store.restore_tensor(
1161
+ storage_name,
1162
+ leaf.entry,
1163
+ topology=None if shards is None else (shards, axis),
1164
+ expected_size=int(leaf.element_bytes),
1165
+ )
1166
+ except ServiceError as exc:
1167
+ raise self._translate(exc) from exc
1168
+ if tuple(buffer.shape) != leaf.shape:
1169
+ buffer.close()
1170
+ raise ExactnessError(
1171
+ "leaf %r restored shape %r, expected %r"
1172
+ % (leaf.path, tuple(buffer.shape), leaf.shape)
1173
+ )
1174
+ actual = content_root(buffer.view)
1175
+ if actual != leaf.content_root:
1176
+ buffer.close()
1177
+ raise ExactnessError(
1178
+ "leaf %r restored content root %s != recorded %s"
1179
+ % (leaf.path, actual.hex(), leaf.content_root.hex())
1180
+ )
1181
+ buffers[leaf.entry] = buffer
1182
+ _record_restore_stats(
1183
+ self, len(wanted), sum(buffer.size for buffer in buffers.values())
1184
+ )
1185
+ restored_leaves = []
1186
+ for (path_tuple, target_leaf), path in zip(flat, target_paths):
1187
+ leaf_meta = stored[path]
1188
+ restored_leaves.append(
1189
+ _to_target(
1190
+ buffers[leaf_meta.entry],
1191
+ canonical=leaf_meta.dtype,
1192
+ shape=leaf_meta.shape,
1193
+ target=target_leaf,
1194
+ )
1195
+ )
1196
+ return _JAX.tree_util.tree_unflatten(_treedef(target), restored_leaves)
1197
+ finally:
1198
+ for buffer in buffers.values():
1199
+ buffer.close()
1200
+
1201
+ # -- extension: single-leaf restore -------------------------------------
1202
+
1203
+ def restore_leaf(self, directory, path, *, target=None, **kwargs):
1204
+ """Restore exactly one recorded leaf by canonical path.
1205
+
1206
+ This is an adapter extension, not part of the Orbax interface. It calls
1207
+ ``weftfs checkpoint restore --tensor ENTRY`` for the one leaf and
1208
+ verifies the recorded content root, dtype, and shape before returning.
1209
+ """
1210
+ _require_jax("restoring one leaf")
1211
+ restore_topology = kwargs.pop("restore_topology", None)
1212
+ axis_arg = kwargs.pop("axis", 0)
1213
+ store, storage_name, metadata, show, _pointer = _resolve_metadata(
1214
+ directory,
1215
+ weftfs_cli=self.weftfs_cli,
1216
+ timeout=self.timeout,
1217
+ service_factory=self._get_service,
1218
+ **kwargs,
1219
+ )
1220
+ _verify_metadata_manifest(metadata, show)
1221
+ stored = metadata.by_path()
1222
+ if path not in stored:
1223
+ raise MissingLeaf(
1224
+ "checkpoint %r has no leaf %r; paths: %r"
1225
+ % (storage_name, path, sorted(stored))
1226
+ )
1227
+ leaf = stored[path]
1228
+ if target is not None:
1229
+ dtype_name, shape, _source = _leaf_dtype_shape(target)
1230
+ if _canonical_dtype(dtype_name) != leaf.dtype or tuple(shape) != leaf.shape:
1231
+ raise MismatchedLeaf(
1232
+ "target leaf %r type/shape disagrees with the stored leaf" % path
1233
+ )
1234
+ shards, axis = (
1235
+ (None, 0)
1236
+ if restore_topology is None
1237
+ else _parse_topology(restore_topology, axis_arg)
1238
+ )
1239
+ buffer = None
1240
+ try:
1241
+ try:
1242
+ buffer = store.restore_tensor(
1243
+ storage_name,
1244
+ leaf.entry,
1245
+ topology=None if shards is None else (shards, axis),
1246
+ expected_size=int(leaf.element_bytes),
1247
+ )
1248
+ except ServiceError as exc:
1249
+ raise self._translate(exc) from exc
1250
+ if tuple(buffer.shape) != leaf.shape:
1251
+ raise ExactnessError(
1252
+ "leaf %r restored shape %r, expected %r"
1253
+ % (path, tuple(buffer.shape), leaf.shape)
1254
+ )
1255
+ actual = content_root(buffer.view)
1256
+ if actual != leaf.content_root:
1257
+ raise ExactnessError(
1258
+ "leaf %r restored content root %s != recorded %s"
1259
+ % (path, actual.hex(), leaf.content_root.hex())
1260
+ )
1261
+ _record_restore_stats(self, 1, buffer.size)
1262
+ if target is None:
1263
+ # The returned numpy array owns its bytes; this is the one
1264
+ # explicit copy on the "no destination given" path.
1265
+ return (
1266
+ _NP.frombuffer(buffer.view, dtype=_numpy_dtype(leaf.dtype))
1267
+ .reshape(leaf.shape)
1268
+ .copy()
1269
+ )
1270
+ return _to_target(
1271
+ buffer, canonical=leaf.dtype, shape=leaf.shape, target=target
1272
+ )
1273
+ finally:
1274
+ if buffer is not None:
1275
+ buffer.close()
1276
+
1277
+ def leaf_paths(self, directory):
1278
+ metadata = metadata_of(Path(directory))
1279
+ return [leaf.path for leaf in metadata.leaves]
1280
+
1281
+
1282
+ if ORBAX_AVAILABLE: # pragma: no branch - registered only with a real Orbax
1283
+ _orbax_args.register_with_handler(WeftOrbaxHandler, for_save=True)(WeftSaveArgs)
1284
+ _orbax_args.register_with_handler(WeftOrbaxHandler, for_restore=True)(
1285
+ WeftRestoreArgs
1286
+ )
1287
+
1288
+
1289
+ # ---------------------------------------------------------------------------
1290
+ # Argument / environment helpers
1291
+
1292
+
1293
+ def _extract_args(args, kwargs, expected, operation):
1294
+ value = kwargs.get("args")
1295
+ if value is None:
1296
+ for candidate in args:
1297
+ if isinstance(candidate, expected):
1298
+ value = candidate
1299
+ break
1300
+ if value is None:
1301
+ raise SaveFailed(
1302
+ "%s needs a %s instance (pass args=%s(...))"
1303
+ % (operation, expected.__name__, expected.__name__)
1304
+ )
1305
+ if not isinstance(value, expected):
1306
+ raise SaveFailed(
1307
+ "%s got args of type %s, expected %s"
1308
+ % (operation, type(value).__name__, expected.__name__)
1309
+ )
1310
+ return value
1311
+
1312
+
1313
+ def _resolve_save_store_root(store_root):
1314
+ if store_root is None:
1315
+ store_root = os.environ.get("WEFTFS_STORE")
1316
+ if store_root is None:
1317
+ raise StoreUnavailable(
1318
+ "WeftSaveArgs.store_root is required (or set WEFTFS_STORE)"
1319
+ )
1320
+ return Path(os.fspath(store_root))
1321
+
1322
+
1323
+ def _flatten(tree):
1324
+ _require_jax("flattening a PyTree")
1325
+ return _JAX.tree_util.tree_flatten_with_path(tree)[0]
1326
+
1327
+
1328
+ def _treedef(tree):
1329
+ return _JAX.tree_util.tree_structure(tree)
1330
+
1331
+
1332
+ def _entry_names(count):
1333
+ width = max(4, len(str(max(count - 1, 0))))
1334
+ return ["leaf_%0*d" % (width, index) for index in range(count)]
1335
+
1336
+
1337
+ def _json_bytes(value):
1338
+ import json
1339
+
1340
+ return json.dumps(value, sort_keys=True, separators=(",", ":")).encode("utf-8")
1341
+
1342
+
1343
+ def _fsync_directory(path):
1344
+ try:
1345
+ fd = os.open(os.fspath(path), os.O_RDONLY)
1346
+ except OSError:
1347
+ return
1348
+ try:
1349
+ os.fsync(fd)
1350
+ finally:
1351
+ os.close(fd)
1352
+
1353
+
1354
+ def _allocate_storage_name(store, checkpoint_id, overwrite):
1355
+ existing = set(store.list_checkpoints())
1356
+ if checkpoint_id not in existing:
1357
+ return checkpoint_id
1358
+ if not overwrite:
1359
+ raise FileExistsError(
1360
+ "checkpoint %r already exists and overwrite=False" % checkpoint_id
1361
+ )
1362
+ highest = 1
1363
+ for name in existing:
1364
+ generation = _generation(name, checkpoint_id)
1365
+ if generation is not None and generation > highest:
1366
+ highest = generation
1367
+ return "%s.v%d" % (checkpoint_id, highest + 1)
1368
+
1369
+
1370
+ # ---------------------------------------------------------------------------
1371
+ # Manifest / metadata validation
1372
+
1373
+
1374
+ def _verify_manifest(show, leaves, storage_name):
1375
+ manifest = show.get("manifest")
1376
+ if not isinstance(manifest, dict):
1377
+ raise SaveFailed("checkpoint %r has no manifest object" % storage_name)
1378
+ if manifest.get("complete") is not True:
1379
+ raise SaveFailed("checkpoint %r is not complete" % storage_name)
1380
+ rows = manifest.get("tensors")
1381
+ if not isinstance(rows, list) or not rows:
1382
+ raise SaveFailed("checkpoint %r has no tensor list" % storage_name)
1383
+ by_name = {row["name"]: row for row in rows}
1384
+ expected = {leaf["entry"] for leaf in leaves}
1385
+ if set(by_name) != expected:
1386
+ raise SaveFailed(
1387
+ "checkpoint %r stored tensors %r, expected %r"
1388
+ % (storage_name, sorted(by_name), sorted(expected))
1389
+ )
1390
+ for leaf in leaves:
1391
+ row = by_name[leaf["entry"]]
1392
+ if row.get("dtype") != leaf["dtype"]:
1393
+ raise ExactnessError(
1394
+ "leaf %r dtype %r != canonical %r"
1395
+ % (leaf["path"], row.get("dtype"), leaf["dtype"])
1396
+ )
1397
+ if tuple(int(x) for x in row.get("shape", [])) != leaf["shape"]:
1398
+ raise ExactnessError(
1399
+ "leaf %r shape %r != %r"
1400
+ % (leaf["path"], row.get("shape"), leaf["shape"])
1401
+ )
1402
+ if int(row.get("element_bytes", -1)) != leaf["element_bytes"]:
1403
+ raise ExactnessError(
1404
+ "leaf %r element_bytes %r != %r"
1405
+ % (leaf["path"], row.get("element_bytes"), leaf["element_bytes"])
1406
+ )
1407
+ if row.get("content_root") != leaf["content_root"].hex():
1408
+ raise ExactnessError(
1409
+ "leaf %r content root %s != the bytes written (%s)"
1410
+ % (
1411
+ leaf["path"],
1412
+ row.get("content_root"),
1413
+ leaf["content_root"].hex(),
1414
+ )
1415
+ )
1416
+
1417
+
1418
+ def _verify_metadata_manifest(metadata, show):
1419
+ manifest = show.get("manifest")
1420
+ if not isinstance(manifest, dict) or manifest.get("complete") is not True:
1421
+ raise InvalidMetadata(
1422
+ "checkpoint %r manifest is missing or incomplete" % metadata.storage_name
1423
+ )
1424
+ rows = {row["name"]: row for row in manifest.get("tensors", [])}
1425
+ if set(rows) != {leaf.entry for leaf in metadata.leaves}:
1426
+ raise InvalidMetadata(
1427
+ "checkpoint %r manifest tensors %r != metadata entries %r"
1428
+ % (
1429
+ metadata.storage_name,
1430
+ sorted(rows),
1431
+ sorted(leaf.entry for leaf in metadata.leaves),
1432
+ )
1433
+ )
1434
+ for leaf in metadata.leaves:
1435
+ row = rows[leaf.entry]
1436
+ if row.get("dtype") != leaf.dtype:
1437
+ raise ExactnessError(
1438
+ "leaf %r manifest dtype %r != metadata %r"
1439
+ % (leaf.path, row.get("dtype"), leaf.dtype)
1440
+ )
1441
+ if tuple(int(x) for x in row.get("shape", [])) != leaf.shape:
1442
+ raise ExactnessError(
1443
+ "leaf %r manifest shape %r != metadata %r"
1444
+ % (leaf.path, row.get("shape"), leaf.shape)
1445
+ )
1446
+ if int(row.get("element_bytes", -1)) != leaf.element_bytes:
1447
+ raise ExactnessError(
1448
+ "leaf %r manifest element_bytes %r != metadata %r"
1449
+ % (leaf.path, row.get("element_bytes"), leaf.element_bytes)
1450
+ )
1451
+ if row.get("content_root") != leaf.content_root.hex():
1452
+ raise ExactnessError(
1453
+ "leaf %r manifest content root %s != metadata %s"
1454
+ % (leaf.path, row.get("content_root"), leaf.content_root.hex())
1455
+ )
1456
+ if row.get("logical_root") != leaf.logical_root.hex():
1457
+ raise ExactnessError(
1458
+ "leaf %r manifest logical root %s != metadata %s"
1459
+ % (leaf.path, row.get("logical_root"), leaf.logical_root.hex())
1460
+ )
1461
+
1462
+
1463
+ def _check_restored_payloads(parsed, leaves, storage_name):
1464
+ for leaf in leaves:
1465
+ if leaf.entry not in parsed:
1466
+ raise InvalidMetadata(
1467
+ "restored container for %r has no entry %r (has %r)"
1468
+ % (storage_name, leaf.entry, sorted(parsed))
1469
+ )
1470
+ sf_dtype, shape, payload = parsed[leaf.entry]
1471
+ if sf_dtype != _safetensors_dtype(leaf.dtype):
1472
+ raise ExactnessError(
1473
+ "leaf %r restored dtype %s, expected %s"
1474
+ % (leaf.path, sf_dtype, _safetensors_dtype(leaf.dtype))
1475
+ )
1476
+ if tuple(int(x) for x in shape) != leaf.shape:
1477
+ raise ExactnessError(
1478
+ "leaf %r restored shape %r, expected %r"
1479
+ % (leaf.path, tuple(shape), leaf.shape)
1480
+ )
1481
+ actual = content_root(payload)
1482
+ if actual != leaf.content_root:
1483
+ raise ExactnessError(
1484
+ "leaf %r restored content root %s != recorded %s"
1485
+ % (leaf.path, actual.hex(), leaf.content_root.hex())
1486
+ )
1487
+
1488
+
1489
+ def _record_restore_stats(handler, leaves, nbytes):
1490
+ with handler._lock:
1491
+ handler._stats["restores"] += 1
1492
+ handler._stats["leaves_restored"] += leaves
1493
+ handler._stats["restored_bytes"] += nbytes
1494
+
1495
+
1496
+ # ---------------------------------------------------------------------------
1497
+ # Locator / metadata resolution
1498
+
1499
+
1500
+ def read_pointer(directory):
1501
+ path = Path(directory) / ORBAX_POINTER_NAME
1502
+ try:
1503
+ raw = path.read_bytes()
1504
+ except FileNotFoundError as exc:
1505
+ raise MetadataMissing(
1506
+ "directory %s has no %s locator" % (directory, ORBAX_POINTER_NAME)
1507
+ ) from exc
1508
+ try:
1509
+ value = wcb.decode(raw)
1510
+ except Exception as exc:
1511
+ raise InvalidMetadata(
1512
+ "orbax locator at %s is not canonical WCB: %s" % (path, exc)
1513
+ ) from exc
1514
+ return pointer_from_value(value)
1515
+
1516
+
1517
+ def _resolve_metadata(
1518
+ directory,
1519
+ *,
1520
+ store_root=None,
1521
+ checkpoint_id=None,
1522
+ storage_name=None,
1523
+ weftfs_cli=None,
1524
+ timeout=DEFAULT_TIMEOUT_SECONDS,
1525
+ service=None,
1526
+ service_factory=None,
1527
+ ):
1528
+ directory = Path(os.fspath(directory))
1529
+ pointer = None
1530
+ pointer_path = directory / ORBAX_POINTER_NAME
1531
+ if pointer_path.is_file():
1532
+ pointer = read_pointer(directory)
1533
+ if store_root is None:
1534
+ if pointer is None:
1535
+ raise StoreUnavailable(
1536
+ "no %s in %s and no store_root given"
1537
+ % (ORBAX_POINTER_NAME, directory)
1538
+ )
1539
+ store_root = pointer["store_root"]
1540
+ store_root = Path(os.fspath(store_root))
1541
+ if pointer is not None:
1542
+ if os.path.abspath(os.fspath(store_root)) != os.path.abspath(
1543
+ pointer["store_root"]
1544
+ ):
1545
+ raise InvalidMetadata(
1546
+ "explicit store_root %s != locator store_root %s"
1547
+ % (store_root, pointer["store_root"])
1548
+ )
1549
+ if checkpoint_id is not None and checkpoint_id != pointer["checkpoint_id"]:
1550
+ raise InvalidMetadata(
1551
+ "explicit checkpoint_id %r != locator checkpoint_id %r"
1552
+ % (checkpoint_id, pointer["checkpoint_id"])
1553
+ )
1554
+ if storage_name is not None and storage_name != pointer["storage_name"]:
1555
+ raise InvalidMetadata(
1556
+ "explicit storage_name %r != locator storage_name %r"
1557
+ % (storage_name, pointer["storage_name"])
1558
+ )
1559
+ checkpoint_id = pointer["checkpoint_id"]
1560
+ storage_name = pointer["storage_name"]
1561
+ if service is None:
1562
+ if service_factory is not None:
1563
+ service = service_factory(store_root)
1564
+ else:
1565
+ service = CheckpointService(store_root, cli=weftfs_cli, timeout=timeout)
1566
+ store = service
1567
+ # Refresh before reading so a commit made by another process (the CLI oracle
1568
+ # in the adversarial tests, for example) is visible on this long-lived
1569
+ # service. The refresh reopens the read handle; it is not a write.
1570
+ store.refresh()
1571
+ if storage_name is None:
1572
+ if checkpoint_id is None:
1573
+ raise InvalidMetadata(
1574
+ "restore needs checkpoint_id or storage_name when the directory "
1575
+ "has no locator"
1576
+ )
1577
+ storage_name = _latest_storage_name(store, checkpoint_id)
1578
+ elif checkpoint_id is not None:
1579
+ if _generation(storage_name, checkpoint_id) is None:
1580
+ raise InvalidMetadata(
1581
+ "storage_name %r is not a generation of checkpoint_id %r"
1582
+ % (storage_name, checkpoint_id)
1583
+ )
1584
+ metadata_path = "/checkpoints/%s/%s" % (storage_name, ORBAX_METADATA_NAME)
1585
+ exists, _size = store.stat_file(metadata_path)
1586
+ if not exists:
1587
+ raise MetadataMissing(
1588
+ "checkpoint %r has no %s; was it written by the Orbax adapter?"
1589
+ % (metadata_path, ORBAX_METADATA_NAME)
1590
+ )
1591
+ with store.read_file(metadata_path) as view:
1592
+ blob = view.bytes
1593
+ if pointer is not None and content_root(blob) != pointer["metadata_root"]:
1594
+ raise InvalidMetadata(
1595
+ "store metadata root %s != locator metadata_root %s"
1596
+ % (content_root(blob).hex(), pointer["metadata_root"].hex())
1597
+ )
1598
+ try:
1599
+ value = wcb.decode(blob)
1600
+ except Exception as exc:
1601
+ raise InvalidMetadata(
1602
+ "store metadata at %s is not canonical WCB: %s" % (metadata_path, exc)
1603
+ ) from exc
1604
+ metadata = metadata_from_value(value)
1605
+ if metadata.storage_name != storage_name:
1606
+ raise InvalidMetadata(
1607
+ "store metadata names storage %r but resolved %r"
1608
+ % (metadata.storage_name, storage_name)
1609
+ )
1610
+ if checkpoint_id is not None and metadata.checkpoint_id != checkpoint_id:
1611
+ raise InvalidMetadata(
1612
+ "store metadata names checkpoint %r but resolved %r"
1613
+ % (metadata.checkpoint_id, checkpoint_id)
1614
+ )
1615
+ show = _show_from_manifest(store.manifest(storage_name))
1616
+ return store, storage_name, metadata, show, pointer
1617
+
1618
+
1619
+ def _show_from_manifest(descriptor):
1620
+ return {
1621
+ "manifest_root": descriptor.manifest_root,
1622
+ "manifest": descriptor.to_value(),
1623
+ }
1624
+
1625
+
1626
+ def _translate_service_error(exc):
1627
+ """Map a typed service error onto this adapter's error hierarchy."""
1628
+ message = str(exc)
1629
+ class_name = getattr(exc, "class_name", "")
1630
+ if class_name == "NotFound":
1631
+ return MetadataMissing(message)
1632
+ if class_name in ("Corrupt", "Malformed", "NonCanonical", "DependencyMismatch"):
1633
+ return ExactnessError(message)
1634
+ if class_name in ("LimitExceeded", "BudgetExceeded"):
1635
+ return StagingLimitExceeded(message)
1636
+ return SaveFailed(message)
1637
+
1638
+
1639
+ def _latest_storage_name(store, checkpoint_id):
1640
+ names = set(store.list_checkpoints())
1641
+ highest = 1
1642
+ chosen = None
1643
+ for name in names:
1644
+ generation = _generation(name, checkpoint_id)
1645
+ if generation is not None and generation >= highest:
1646
+ highest = generation
1647
+ chosen = name
1648
+ if chosen is None:
1649
+ raise MetadataMissing("checkpoint %r does not exist in the store" % checkpoint_id)
1650
+ return chosen
1651
+
1652
+
1653
+ def read_metadata(directory, **kwargs):
1654
+ """Read and validate the canonical metadata for an Orbax directory."""
1655
+ return _resolve_metadata(Path(directory), **kwargs)
1656
+
1657
+
1658
+ # ---------------------------------------------------------------------------
1659
+ # Convenience wrappers
1660
+
1661
+
1662
+ def save_pytree(item, *, directory, store_root, checkpoint_id, **kwargs):
1663
+ """Synchronous save without constructing a handler by hand."""
1664
+ handler = WeftOrbaxHandler(
1665
+ staging_bytes=kwargs.pop("handler_staging_bytes", DEFAULT_STAGING_BYTES),
1666
+ max_workers=kwargs.pop("max_workers", 1),
1667
+ weftfs_cli=kwargs.pop("weftfs_cli", None),
1668
+ timeout=kwargs.pop("timeout", DEFAULT_TIMEOUT_SECONDS),
1669
+ )
1670
+ try:
1671
+ args = WeftSaveArgs(
1672
+ item=item,
1673
+ store_root=store_root,
1674
+ checkpoint_id=checkpoint_id,
1675
+ **kwargs,
1676
+ )
1677
+ return handler.save(directory, args)
1678
+ finally:
1679
+ handler.close()
1680
+
1681
+
1682
+ def save_async_pytree(item, *, directory, store_root, checkpoint_id, **kwargs):
1683
+ """Run a save on one bounded worker; returns a DCP-style async handle."""
1684
+ handler = WeftOrbaxHandler(
1685
+ staging_bytes=kwargs.pop("handler_staging_bytes", DEFAULT_STAGING_BYTES),
1686
+ max_workers=kwargs.pop("max_workers", 1),
1687
+ weftfs_cli=kwargs.pop("weftfs_cli", None),
1688
+ timeout=kwargs.pop("timeout", DEFAULT_TIMEOUT_SECONDS),
1689
+ )
1690
+ executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="weft-orbax-async")
1691
+ args = WeftSaveArgs(
1692
+ item=item,
1693
+ store_root=store_root,
1694
+ checkpoint_id=checkpoint_id,
1695
+ **kwargs,
1696
+ )
1697
+ future = executor.submit(handler.save, Path(os.fspath(directory)), args)
1698
+ future.add_done_callback(lambda _future: handler.close())
1699
+ return AsyncSaveHandle(future, handler, executor)
1700
+
1701
+
1702
+ def restore_pytree(directory, item, **kwargs):
1703
+ """Synchronous restore without constructing a handler by hand."""
1704
+ handler = WeftOrbaxHandler(
1705
+ staging_bytes=kwargs.pop("handler_staging_bytes", DEFAULT_STAGING_BYTES),
1706
+ weftfs_cli=kwargs.pop("weftfs_cli", None),
1707
+ timeout=kwargs.pop("timeout", DEFAULT_TIMEOUT_SECONDS),
1708
+ )
1709
+ try:
1710
+ return handler.restore(directory, WeftRestoreArgs(item=item, **kwargs))
1711
+ finally:
1712
+ handler.close()
1713
+
1714
+
1715
+ def leaf_paths(item):
1716
+ """Canonical paths of each leaf of a PyTree, in flatten order."""
1717
+ return [_canonical_path(path_tuple) for path_tuple, _ in _flatten(item)]
1718
+
1719
+
1720
+ def flatten_pytree(item):
1721
+ """``[(canonical_path, leaf)]`` in JAX flatten order."""
1722
+ return [(_canonical_path(path_tuple), leaf) for path_tuple, leaf in _flatten(item)]
1723
+
1724
+
1725
+ def metadata_of(directory, **kwargs):
1726
+ """Return the decoded :class:`OrbaxMetadata` for a directory."""
1727
+ resolved = read_metadata(Path(directory), **kwargs)
1728
+ service = resolved[0]
1729
+ try:
1730
+ return resolved[2]
1731
+ finally:
1732
+ close = getattr(service, "close", None)
1733
+ if callable(close):
1734
+ close()