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/service.py ADDED
@@ -0,0 +1,959 @@
1
+ """Persistent, in-process WeftFS checkpoint service client.
2
+
3
+ One ``CheckpointService`` owns exactly one ``weftfs checkpoint-service``
4
+ process for its whole lifetime. Every call is a bounded, length-prefixed frame
5
+ over an ``AF_UNIX`` socket; tensor payloads never travel through the frame —
6
+ they cross the process boundary as ``SCM_RIGHTS`` memfd descriptors.
7
+
8
+ Why a socket rather than stdin/stdout: a caller-owned restore buffer (and the
9
+ staged container handed to a write) must be transferred without a copy through
10
+ the helper's address space, and ``SCM_RIGHTS`` file descriptors are only
11
+ expressible on a socket. The protocol, bounds, and failure semantics are
12
+ normative in ``docs/CHECKPOINT-SERVICE.md``.
13
+
14
+ The service never starts a subprocess per operation, never writes a staging
15
+ file for a restore, and a restore never mutates the store (the Rust side opens
16
+ one read-only handle and the write path is a separate, explicit operation).
17
+
18
+ This module has no dependency on torch, JAX, or numpy at import time; numpy is
19
+ imported only when a caller asks for an array view.
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import array
25
+ import hashlib
26
+ import mmap
27
+ import os
28
+ import socket
29
+ import struct
30
+ import subprocess
31
+ import threading
32
+ import time
33
+
34
+ PROTOCOL_VERSION = 2
35
+ MAX_FRAME_BYTES = 1 << 20
36
+ DEFAULT_MAX_TENSOR_BYTES = 4 << 30
37
+ MAX_FILE_BYTES = 16 << 20
38
+ DEFAULT_TIMEOUT_SECONDS = 1800.0
39
+ DEFAULT_CLI = "/work/.docker-cache/target-rust/release/weftfs"
40
+
41
+ OP_INFO = 0x01
42
+ OP_LIST_CHECKPOINTS = 0x02
43
+ OP_CHECKPOINT_MANIFEST = 0x03
44
+ OP_RESTORE_TENSOR = 0x04
45
+ OP_WRITE_CHECKPOINT = 0x05
46
+ OP_VERIFY_CHECKPOINT = 0x06
47
+ OP_REFRESH = 0x07
48
+ OP_WRITE_FILE = 0x08
49
+ OP_STAT_FILE = 0x09
50
+ OP_READ_FILE = 0x0A
51
+ OP_SHUTDOWN = 0x7F
52
+
53
+ STATUS_OK = 0
54
+ STATUS_ERROR = 1
55
+
56
+ _FRAME_HEADER = struct.Struct("<I") # u32 length
57
+
58
+
59
+ class ServiceError(RuntimeError):
60
+ """A typed error class returned by the service."""
61
+
62
+ def __init__(self, class_name, message, *, request_id=None):
63
+ super().__init__("%s: %s" % (class_name, message))
64
+ self.class_name = class_name
65
+ self.plain_message = message
66
+ self.request_id = request_id
67
+
68
+
69
+ class ServiceUnavailable(RuntimeError):
70
+ """The helper process could not be started or has exited."""
71
+
72
+
73
+ class ServiceProtocolError(RuntimeError):
74
+ """A framing violation; the connection is no longer usable."""
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Small encoders (the Rust side is the normative decoder)
79
+
80
+
81
+ def _u32(value):
82
+ return struct.pack("<I", int(value))
83
+
84
+
85
+ def _u64(value):
86
+ return struct.pack("<Q", int(value))
87
+
88
+
89
+ def _string(text):
90
+ data = text.encode("utf-8") if isinstance(text, str) else bytes(text)
91
+ return _u32(len(data)) + data
92
+
93
+
94
+ class _Reader:
95
+ __slots__ = ("data", "at")
96
+
97
+ def __init__(self, data):
98
+ self.data = data
99
+ self.at = 0
100
+
101
+ def take(self, count):
102
+ if self.at + count > len(self.data):
103
+ raise ServiceProtocolError("truncated response body")
104
+ chunk = self.data[self.at : self.at + count]
105
+ self.at += count
106
+ return chunk
107
+
108
+ def u8(self):
109
+ return self.take(1)[0]
110
+
111
+ def u32(self):
112
+ return struct.unpack("<I", self.take(4))[0]
113
+
114
+ def u64(self):
115
+ return struct.unpack("<Q", self.take(8))[0]
116
+
117
+ def string(self):
118
+ return self.take(self.u32()).decode("utf-8")
119
+
120
+ def hash_hex(self):
121
+ return self.take(32).hex()
122
+
123
+ def rest(self):
124
+ return self.data[self.at :]
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Result records
129
+
130
+
131
+ class ShardRow:
132
+ __slots__ = (
133
+ "axis",
134
+ "shards",
135
+ "index",
136
+ "object_name",
137
+ "object_id",
138
+ "content_root",
139
+ "logical_root",
140
+ "byte_len",
141
+ )
142
+
143
+ def __init__(self, reader):
144
+ self.axis = reader.u32()
145
+ self.shards = reader.u32()
146
+ self.index = reader.u32()
147
+ self.object_name = reader.string()
148
+ self.object_id = reader.hash_hex()
149
+ self.content_root = reader.hash_hex()
150
+ self.logical_root = reader.hash_hex()
151
+ self.byte_len = reader.u64()
152
+
153
+ def to_value(self):
154
+ return {
155
+ "axis": self.axis,
156
+ "shards": self.shards,
157
+ "index": self.index,
158
+ "object_name": self.object_name,
159
+ "object_id": self.object_id,
160
+ "content_root": self.content_root,
161
+ "logical_root": self.logical_root,
162
+ "byte_len": self.byte_len,
163
+ }
164
+
165
+
166
+ class TensorRow:
167
+ __slots__ = (
168
+ "name",
169
+ "dtype",
170
+ "shape",
171
+ "logical_root",
172
+ "content_root",
173
+ "element_bytes",
174
+ "source_offset",
175
+ "shards",
176
+ )
177
+
178
+ def __init__(self, reader):
179
+ self.name = reader.string()
180
+ self.dtype = reader.string()
181
+ self.shape = tuple(reader.u64() for _ in range(reader.u32()))
182
+ self.logical_root = reader.hash_hex()
183
+ self.content_root = reader.hash_hex()
184
+ self.element_bytes = reader.u64()
185
+ self.source_offset = reader.u64()
186
+ self.shards = tuple(ShardRow(reader) for _ in range(reader.u32()))
187
+
188
+ def to_value(self):
189
+ return {
190
+ "name": self.name,
191
+ "dtype": self.dtype,
192
+ "shape": list(self.shape),
193
+ "logical_root": self.logical_root,
194
+ "content_root": self.content_root,
195
+ "element_bytes": self.element_bytes,
196
+ "source_offset": self.source_offset,
197
+ "shards": [shard.to_value() for shard in self.shards],
198
+ }
199
+
200
+
201
+ class Manifest:
202
+ __slots__ = (
203
+ "manifest_root",
204
+ "name",
205
+ "container",
206
+ "complete",
207
+ "element_bytes",
208
+ "source_file_bytes",
209
+ "source_sha256",
210
+ "tensors",
211
+ )
212
+
213
+ def __init__(self, data):
214
+ reader = _Reader(data)
215
+ self.manifest_root = reader.hash_hex()
216
+ self.name = reader.string()
217
+ self.container = reader.string()
218
+ self.complete = bool(reader.u8())
219
+ self.element_bytes = reader.u64()
220
+ self.source_file_bytes = reader.u64() if reader.u8() else None
221
+ self.source_sha256 = reader.hash_hex() if reader.u8() else None
222
+ self.tensors = tuple(TensorRow(reader) for _ in range(reader.u32()))
223
+ if reader.at != len(reader.data):
224
+ raise ServiceProtocolError("manifest has trailing bytes")
225
+
226
+ def by_name(self):
227
+ return {row.name: row for row in self.tensors}
228
+
229
+ def to_value(self):
230
+ return {
231
+ "manifest_root": self.manifest_root,
232
+ "name": self.name,
233
+ "container": self.container,
234
+ "complete": self.complete,
235
+ "element_bytes": self.element_bytes,
236
+ "source_file_bytes": self.source_file_bytes,
237
+ "source_sha256": self.source_sha256,
238
+ "tensors": [row.to_value() for row in self.tensors],
239
+ }
240
+
241
+
242
+ class WriteReport:
243
+ __slots__ = (
244
+ "checkpoint_id",
245
+ "head",
246
+ "manifest_root",
247
+ "source_sha256",
248
+ "source_file_bytes",
249
+ "element_bytes",
250
+ "tensor_count",
251
+ "shard_objects",
252
+ "complete",
253
+ "elapsed_us",
254
+ "manifest",
255
+ )
256
+
257
+ def __init__(self, data):
258
+ reader = _Reader(data)
259
+ self.checkpoint_id = reader.hash_hex()
260
+ self.head = reader.hash_hex()
261
+ self.manifest_root = reader.hash_hex()
262
+ self.source_sha256 = reader.hash_hex()
263
+ self.source_file_bytes = reader.u64()
264
+ self.element_bytes = reader.u64()
265
+ self.tensor_count = reader.u32()
266
+ self.shard_objects = reader.u32()
267
+ self.complete = bool(reader.u8())
268
+ self.elapsed_us = reader.u64()
269
+ length = reader.u32()
270
+ self.manifest = Manifest(reader.take(length))
271
+ if reader.at != len(reader.data):
272
+ raise ServiceProtocolError("write response has trailing bytes")
273
+
274
+ def to_value(self):
275
+ return {
276
+ "checkpoint_id": self.checkpoint_id,
277
+ "head": self.head,
278
+ "manifest_root": self.manifest_root,
279
+ "source_sha256": self.source_sha256,
280
+ "source_file_bytes": self.source_file_bytes,
281
+ "element_bytes": self.element_bytes,
282
+ "tensor_count": self.tensor_count,
283
+ "shard_objects": self.shard_objects,
284
+ "complete": self.complete,
285
+ "elapsed_us": self.elapsed_us,
286
+ "manifest": self.manifest.to_value(),
287
+ }
288
+
289
+
290
+ _NUMPY_DTYPES = {
291
+ "f16": "float16",
292
+ "bf16": None, # numpy has no native bfloat16 without ml_dtypes
293
+ "f32": "float32",
294
+ "f64": "float64",
295
+ "i8": "int8",
296
+ "u8": "uint8",
297
+ "i16": "int16",
298
+ "i32": "int32",
299
+ "i64": "int64",
300
+ }
301
+
302
+ _ELEMENT_BYTES = {
303
+ "f16": 2,
304
+ "bf16": 2,
305
+ "f32": 4,
306
+ "f64": 8,
307
+ "i8": 1,
308
+ "u8": 1,
309
+ "i16": 2,
310
+ "i32": 4,
311
+ "i64": 8,
312
+ }
313
+
314
+
315
+ def canonical_element_bytes(dtype):
316
+ try:
317
+ return _ELEMENT_BYTES[dtype]
318
+ except KeyError:
319
+ raise ServiceError(
320
+ "UnsupportedVersion", "dtype %r has no canonical element size" % (dtype,)
321
+ )
322
+
323
+
324
+ class TensorBuffer:
325
+ """The exact restored bytes of one tensor, in a caller-owned memfd.
326
+
327
+ ``array`` is a numpy view over the caller's mapping (created here because
328
+ the caller asked for one), ``view`` is a writable ``memoryview``, and
329
+ ``bytes`` performs an explicit copy. The service wrote directly into this
330
+ buffer: there is no second copy between decode and the array.
331
+ """
332
+
333
+ __slots__ = (
334
+ "_mm",
335
+ "_fd",
336
+ "size",
337
+ "dtype",
338
+ "shape",
339
+ "full_shape",
340
+ "content_root",
341
+ "full_logical_root",
342
+ "topology_root",
343
+ "full_root_verified",
344
+ "shard_roots_verified",
345
+ "shards_read",
346
+ "units_touched",
347
+ "store_bytes_read",
348
+ "store_reads",
349
+ "elapsed_us",
350
+ )
351
+
352
+ def __init__(self, data):
353
+ reader = _Reader(data)
354
+ self.dtype = reader.string()
355
+ self.shape = tuple(reader.u64() for _ in range(reader.u32()))
356
+ self.size = reader.u64()
357
+ self.content_root = reader.hash_hex()
358
+ self.full_logical_root = reader.hash_hex()
359
+ self.full_root_verified = bool(reader.u8())
360
+ self.shard_roots_verified = bool(reader.u8())
361
+ self.topology_root = reader.hash_hex() if reader.u8() else None
362
+ self.shards_read = reader.u32()
363
+ self.units_touched = reader.u64()
364
+ self.store_bytes_read = reader.u64()
365
+ self.store_reads = reader.u64()
366
+ self.elapsed_us = reader.u64()
367
+ if reader.at != len(reader.data):
368
+ raise ServiceProtocolError("restore response has trailing bytes")
369
+ self._mm = None
370
+ self._fd = None
371
+
372
+ def _attach(self, fd):
373
+ self._fd = fd
374
+ if self.size:
375
+ self._mm = mmap.mmap(fd, self.size, access=mmap.ACCESS_WRITE)
376
+ else:
377
+ self._mm = mmap.mmap(-1, 1)
378
+
379
+ @property
380
+ def view(self):
381
+ return memoryview(self._mm)[: self.size]
382
+
383
+ @property
384
+ def array(self):
385
+ import numpy as np
386
+
387
+ name = _NUMPY_DTYPES.get(self.dtype)
388
+ if name is None:
389
+ raise ServiceError(
390
+ "UnsupportedVersion",
391
+ "dtype %r has no native numpy dtype; use .view or .copy_into"
392
+ % (self.dtype,),
393
+ )
394
+ dtype = np.dtype(name)
395
+ count = self.size // dtype.itemsize
396
+ if count == 0:
397
+ return np.empty(self.shape, dtype=dtype)
398
+ return np.frombuffer(self._mm, dtype=dtype, count=count).reshape(self.shape)
399
+
400
+ @property
401
+ def bytes(self):
402
+ return bytes(self.view)
403
+
404
+ def sha256(self):
405
+ return hashlib.sha256(self.view).digest()
406
+
407
+ def copy_into(self, destination):
408
+ """Explicit bounded copy into a caller-supplied writable buffer."""
409
+ target = memoryview(destination)
410
+ if target.nbytes < self.size:
411
+ raise ValueError(
412
+ "destination holds %d bytes, tensor needs %d"
413
+ % (target.nbytes, self.size)
414
+ )
415
+ target[: self.size] = self.view
416
+ return destination
417
+
418
+ def close(self):
419
+ if self._mm is not None:
420
+ mapping = self._mm
421
+ try:
422
+ mapping.close()
423
+ self._mm = None
424
+ except (BufferError, ValueError):
425
+ # An exported view (for example a torch tensor) still points
426
+ # into the mapping. Dropping our only reference would be a
427
+ # use-after-free hazard, so keep the mapping alive.
428
+ self._mm = mapping
429
+ if self._fd is not None:
430
+ try:
431
+ os.close(self._fd)
432
+ except OSError:
433
+ pass
434
+ self._fd = None
435
+
436
+ def __enter__(self):
437
+ return self
438
+
439
+ def __exit__(self, *exc):
440
+ self.close()
441
+ return False
442
+
443
+
444
+ # ---------------------------------------------------------------------------
445
+ # Client
446
+
447
+
448
+ class CheckpointService:
449
+ """One helper process, reused across every checkpoint operation."""
450
+
451
+ def __init__(
452
+ self,
453
+ store_root,
454
+ *,
455
+ cli=None,
456
+ timeout=DEFAULT_TIMEOUT_SECONDS,
457
+ max_frame_bytes=MAX_FRAME_BYTES,
458
+ max_tensor_bytes=DEFAULT_MAX_TENSOR_BYTES,
459
+ extra_args=(),
460
+ ):
461
+ self.store_root = os.fspath(store_root)
462
+ self.cli = os.fspath(cli) if cli is not None else os.fspath(DEFAULT_CLI)
463
+ self.timeout = float(timeout)
464
+ self.max_frame_bytes = int(max_frame_bytes)
465
+ self.max_tensor_bytes = int(max_tensor_bytes)
466
+ self.extra_args = tuple(str(arg) for arg in extra_args)
467
+ self._lock = threading.Lock()
468
+ self._request_id = 0
469
+ self._closed = False
470
+ self._socket = None
471
+ self._process = None
472
+ self.stats = {
473
+ "requests": 0,
474
+ "restores": 0,
475
+ "writes": 0,
476
+ "restored_bytes": 0,
477
+ "written_bytes": 0,
478
+ "request_ms": 0.0,
479
+ }
480
+ self._start()
481
+
482
+ # -- lifecycle ----------------------------------------------------------
483
+
484
+ def _start(self):
485
+ parent, child = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
486
+ child.set_inheritable(True)
487
+ command = [
488
+ self.cli,
489
+ "--store",
490
+ self.store_root,
491
+ "checkpoint-service",
492
+ "--fd",
493
+ str(child.fileno()),
494
+ "--max-frame-bytes",
495
+ str(self.max_frame_bytes),
496
+ "--max-tensor-bytes",
497
+ str(self.max_tensor_bytes),
498
+ *self.extra_args,
499
+ ]
500
+ try:
501
+ self._process = subprocess.Popen(
502
+ command,
503
+ stdin=subprocess.DEVNULL,
504
+ stdout=subprocess.DEVNULL,
505
+ stderr=subprocess.PIPE,
506
+ pass_fds=(child.fileno(),),
507
+ close_fds=True,
508
+ )
509
+ except FileNotFoundError as exc:
510
+ parent.close()
511
+ child.close()
512
+ raise ServiceUnavailable(
513
+ "weftfs CLI not executable at %s: %s" % (self.cli, exc)
514
+ ) from exc
515
+ child.close()
516
+ self._socket = parent
517
+ self._socket.settimeout(self.timeout)
518
+ info = self.info()
519
+ if info["protocol"] != PROTOCOL_VERSION:
520
+ self.close()
521
+ raise ServiceProtocolError(
522
+ "service speaks protocol %r, client speaks %d"
523
+ % (info["protocol"], PROTOCOL_VERSION)
524
+ )
525
+
526
+ def close(self):
527
+ with self._lock:
528
+ if self._closed:
529
+ return
530
+ self._closed = True
531
+ if self._socket is not None:
532
+ try:
533
+ frame = self._encode(OP_SHUTDOWN, 0, b"")
534
+ self._socket.sendall(frame)
535
+ self._socket.settimeout(2.0)
536
+ self._socket.recv(1 << 16)
537
+ except OSError:
538
+ pass
539
+ try:
540
+ self._socket.close()
541
+ except OSError:
542
+ pass
543
+ self._socket = None
544
+ if self._process is not None:
545
+ try:
546
+ self._process.wait(timeout=5)
547
+ except subprocess.TimeoutExpired:
548
+ self._process.kill()
549
+ self._process.wait()
550
+ self._process = None
551
+
552
+ def __enter__(self):
553
+ return self
554
+
555
+ def __exit__(self, *exc):
556
+ self.close()
557
+ return False
558
+
559
+ def __del__(self):
560
+ try:
561
+ self.close()
562
+ except Exception:
563
+ pass
564
+
565
+ @property
566
+ def pid(self):
567
+ return None if self._process is None else self._process.pid
568
+
569
+ # -- framing ------------------------------------------------------------
570
+
571
+ def _encode(self, op, request_id, body, status=0):
572
+ length = 6 + len(body)
573
+ if length + 4 > self.max_frame_bytes + 4:
574
+ raise ServiceError(
575
+ "LimitExceeded",
576
+ "request body is %d bytes, above the frame bound" % (len(body),),
577
+ )
578
+ return _FRAME_HEADER.pack(length) + bytes((op,)) + _u32(request_id) + bytes((status,)) + body
579
+
580
+ def _call(self, op, body=b"", fd=None):
581
+ with self._lock:
582
+ if self._closed or self._socket is None:
583
+ raise ServiceUnavailable("checkpoint service is closed")
584
+ self._request_id += 1
585
+ request_id = self._request_id & 0xFFFFFFFF
586
+ frame = self._encode(op, request_id, body)
587
+ started = time.monotonic()
588
+ ancillary = []
589
+ if fd is not None:
590
+ ancillary = [
591
+ (socket.SOL_SOCKET, socket.SCM_RIGHTS, array.array("i", [int(fd)]))
592
+ ]
593
+ try:
594
+ self._socket.sendmsg([frame], ancillary)
595
+ response = self._recv_frame()
596
+ except OSError as exc:
597
+ self._raise_process_failure(exc)
598
+ elapsed_ms = (time.monotonic() - started) * 1000.0
599
+ self.stats["requests"] += 1
600
+ self.stats["request_ms"] += elapsed_ms
601
+ if response[0] != request_id:
602
+ raise ServiceProtocolError(
603
+ "response id %r does not match request %r"
604
+ % (response[0], request_id)
605
+ )
606
+ if response[1] != STATUS_OK:
607
+ reader = _Reader(response[2])
608
+ class_name = reader.string()
609
+ message = reader.string()
610
+ raise ServiceError(class_name, message, request_id=request_id)
611
+ return response[2]
612
+
613
+ def _raise_process_failure(self, exc):
614
+ detail = ""
615
+ if self._process is not None:
616
+ try:
617
+ _, stderr = self._process.communicate(timeout=2)
618
+ detail = (stderr or b"").decode("utf-8", "replace").strip()
619
+ except Exception:
620
+ detail = ""
621
+ raise ServiceUnavailable(
622
+ "checkpoint service connection failed (%s); process=%r stderr=%r"
623
+ % (exc, None if self._process is None else self._process.pid, detail)
624
+ ) from exc
625
+
626
+ def _recv_frame(self):
627
+ header = self._recv_exact(4, allow_fds=True)
628
+ if header is None:
629
+ raise ServiceProtocolError("service closed the connection")
630
+ (length,) = _FRAME_HEADER.unpack(header)
631
+ if length < 6 or length > self.max_frame_bytes:
632
+ raise ServiceProtocolError("service sent implausible frame length %d" % length)
633
+ payload = self._recv_exact(length)
634
+ if payload is None:
635
+ raise ServiceProtocolError("service closed mid-frame")
636
+ op = payload[0]
637
+ request_id = struct.unpack("<I", payload[1:5])[0]
638
+ status = payload[5]
639
+ if status not in (STATUS_OK, STATUS_ERROR):
640
+ raise ServiceProtocolError("unknown response status %d" % status)
641
+ body = payload[6:]
642
+ if status == STATUS_OK and op == OP_RESTORE_TENSOR:
643
+ # The restore response carries no descriptor: the caller supplied
644
+ # the buffer. The protocol leaves the fd space unused here.
645
+ pass
646
+ return request_id, status, body
647
+
648
+ def _recv_exact(self, count, *, allow_fds=False):
649
+ buffer = bytearray()
650
+ first = True
651
+ fds = []
652
+ while len(buffer) < count:
653
+ budget = socket.CMSG_SPACE(4 * 4)
654
+ data, ancillary, flags, _address = self._socket.recvmsg(
655
+ count - len(buffer), budget if (allow_fds and first) else 0
656
+ )
657
+ if not data:
658
+ return None
659
+ buffer.extend(data)
660
+ if first and allow_fds:
661
+ for level, kind, payload in ancillary:
662
+ if level == socket.SOL_SOCKET and kind == socket.SCM_RIGHTS:
663
+ values = array.array("i")
664
+ values.frombytes(payload[: len(payload) - (len(payload) % 4)])
665
+ for fd in values:
666
+ try:
667
+ os.close(fd)
668
+ except OSError:
669
+ pass
670
+ if flags & socket.MSG_CTRUNC:
671
+ raise ServiceProtocolError("ancillary data was truncated")
672
+ first = False
673
+ return bytes(buffer)
674
+
675
+ # -- operations ---------------------------------------------------------
676
+
677
+ def info(self):
678
+ body = self._call(OP_INFO)
679
+ reader = _Reader(body)
680
+ return {
681
+ "protocol": reader.u32(),
682
+ "max_frame_bytes": reader.u32(),
683
+ "max_tensor_bytes": reader.u64(),
684
+ "generation": reader.u64(),
685
+ "head": reader.hash_hex(),
686
+ "store_root": reader.string(),
687
+ "elapsed_us": reader.u64(),
688
+ }
689
+
690
+ def list_checkpoints(self):
691
+ body = self._call(OP_LIST_CHECKPOINTS)
692
+ reader = _Reader(body)
693
+ return [reader.string() for _ in range(reader.u32())]
694
+
695
+ def manifest(self, name):
696
+ return Manifest(self._call(OP_CHECKPOINT_MANIFEST, _string(name)))
697
+
698
+ def verify(self, name):
699
+ body = self._call(OP_VERIFY_CHECKPOINT, _string(name))
700
+ return _Reader(body).u32()
701
+
702
+ def refresh(self):
703
+ body = self._call(OP_REFRESH)
704
+ reader = _Reader(body)
705
+ return {"generation": reader.u64(), "head": reader.hash_hex()}
706
+
707
+ def _allocate_memfd(self, size, name="weft-tensor"):
708
+ fd = os.memfd_create(name, os.MFD_CLOEXEC)
709
+ if size:
710
+ os.ftruncate(fd, size)
711
+ return fd
712
+
713
+ def restore_tensor(self, name, tensor, *, region=None, topology=None, expected_size=None):
714
+ """Restore one tensor straight into a caller-owned memfd.
715
+
716
+ ``region`` is ``(start, end)`` in full-tensor coordinates; ``topology``
717
+ is ``(shards, axis)`` and re-lays-out the bytes before verifying the
718
+ same full logical root. ``expected_size`` skips the manifest round trip
719
+ when the caller already knows the payload length (the adapters do).
720
+ The returned :class:`TensorBuffer` owns the mapping; call ``close()``
721
+ (or use it as a context manager).
722
+ """
723
+ row = None
724
+ if region is None and expected_size is not None:
725
+ size = int(expected_size)
726
+ else:
727
+ manifest = self.manifest(name)
728
+ row = manifest.by_name().get(tensor)
729
+ if row is None:
730
+ raise ServiceError(
731
+ "NotFound", "checkpoint %r has no tensor %r" % (name, tensor)
732
+ )
733
+ if region is not None:
734
+ start, end = region
735
+ if len(start) != len(row.shape) or len(end) != len(row.shape):
736
+ raise ValueError("region rank does not match the tensor rank")
737
+ extent = tuple(int(b) - int(a) for a, b in zip(start, end))
738
+ else:
739
+ extent = row.shape
740
+ elements = 1
741
+ for dim in extent:
742
+ elements *= int(dim)
743
+ size = elements * canonical_element_bytes(row.dtype)
744
+ if size > self.max_tensor_bytes:
745
+ raise ServiceError(
746
+ "LimitExceeded",
747
+ "restore of %d bytes exceeds max_tensor_bytes (%d)"
748
+ % (size, self.max_tensor_bytes),
749
+ )
750
+ fd = self._allocate_memfd(size, "weft-restore")
751
+ try:
752
+ body = bytearray()
753
+ body += _string(name)
754
+ body += _string(tensor)
755
+ body += bytes((1,)) if region is not None else bytes((0,))
756
+ if region is not None:
757
+ body += bytes((len(region[0]),))
758
+ for value in region[0]:
759
+ body += _u64(value)
760
+ for value in region[1]:
761
+ body += _u64(value)
762
+ body += bytes((1,)) if topology is not None else bytes((0,))
763
+ if topology is not None:
764
+ body += _u32(topology[0])
765
+ body += _u32(topology[1])
766
+ response = self._call(OP_RESTORE_TENSOR, bytes(body), fd=fd)
767
+ result = TensorBuffer(response)
768
+ if result.size != size:
769
+ raise ServiceProtocolError(
770
+ "service restored %d bytes, expected %d" % (result.size, size)
771
+ )
772
+ result._attach(fd)
773
+ fd = None
774
+ self.stats["restores"] += 1
775
+ self.stats["restored_bytes"] += size
776
+ return result
777
+ finally:
778
+ if fd is not None:
779
+ try:
780
+ os.close(fd)
781
+ except OSError:
782
+ pass
783
+
784
+ def write_checkpoint(
785
+ self,
786
+ name,
787
+ container,
788
+ *,
789
+ shards=1,
790
+ axis=0,
791
+ strategy="zstd",
792
+ tile_shape=None,
793
+ write_policy=None,
794
+ fast_codec=None,
795
+ fast_zstd_level=None,
796
+ critical_path=False,
797
+ deadline_ms=None,
798
+ ):
799
+ """Publish one checkpoint from a caller-supplied safetensors container.
800
+
801
+ ``write_policy`` is ``planned``, ``fast``, or ``auto``. ``None`` reads
802
+ ``$WEFT_WRITE_POLICY`` (default ``planned``), so a training job can set
803
+ the critical-path policy for every save without changing its code.
804
+ """
805
+ if write_policy is None:
806
+ write_policy = os.environ.get("WEFT_WRITE_POLICY", "planned")
807
+ if fast_codec is None:
808
+ fast_codec = os.environ.get("WEFT_FAST_CODEC", "zstd")
809
+ if fast_zstd_level is None:
810
+ fast_zstd_level = int(os.environ.get("WEFT_FAST_ZSTD_LEVEL", "1"))
811
+ policy_codes = {"planned": 0, "fast": 1, "auto": 2}
812
+ if write_policy not in policy_codes:
813
+ raise ValueError("write_policy must be planned, fast, or auto")
814
+ codec_codes = {"raw": 0, "zstd": 1}
815
+ if fast_codec not in codec_codes:
816
+ raise ValueError("fast_codec must be raw or zstd")
817
+ view = memoryview(container)
818
+ fd = self._allocate_memfd(len(view), "weft-container")
819
+ try:
820
+ offset = 0
821
+ while offset < len(view):
822
+ offset += os.write(fd, view[offset:])
823
+ body = bytearray()
824
+ body += _string(name)
825
+ body += _u32(shards)
826
+ body += _u32(axis)
827
+ body += _string(strategy)
828
+ shape = tuple(tile_shape or ())
829
+ body += _u32(len(shape))
830
+ for dim in shape:
831
+ body += _u64(dim)
832
+ body += bytes((policy_codes[write_policy],))
833
+ body += bytes((codec_codes[fast_codec],))
834
+ body += _u32(int(fast_zstd_level))
835
+ flags = 1 if critical_path else 0
836
+ body += bytes((flags,))
837
+ if deadline_ms is None:
838
+ body += bytes((0,))
839
+ else:
840
+ body += bytes((1,))
841
+ body += _u64(int(deadline_ms))
842
+ response = self._call(OP_WRITE_CHECKPOINT, bytes(body), fd=fd)
843
+ report = WriteReport(response)
844
+ self.stats["writes"] += 1
845
+ self.stats["written_bytes"] += len(view)
846
+ return report
847
+ finally:
848
+ try:
849
+ os.close(fd)
850
+ except OSError:
851
+ pass
852
+
853
+ def write_file(self, store_path, payload):
854
+ """Publish one small file at an existing store path (metadata blobs)."""
855
+ if not store_path.startswith("/"):
856
+ raise ValueError("store path must be absolute")
857
+ view = memoryview(payload)
858
+ if len(view) > MAX_FILE_BYTES:
859
+ raise ServiceError(
860
+ "LimitExceeded",
861
+ "write_file payload is %d bytes, above the %d bound"
862
+ % (len(view), MAX_FILE_BYTES),
863
+ )
864
+ fd = self._allocate_memfd(len(view), "weft-file")
865
+ try:
866
+ offset = 0
867
+ while offset < len(view):
868
+ offset += os.write(fd, view[offset:])
869
+ body = _string(store_path)
870
+ response = self._call(OP_WRITE_FILE, body, fd=fd)
871
+ reader = _Reader(response)
872
+ return {"bytes": reader.u64(), "generation": reader.u64()}
873
+ finally:
874
+ try:
875
+ os.close(fd)
876
+ except OSError:
877
+ pass
878
+
879
+ def stat_file(self, store_path):
880
+ """Return ``(exists, size)`` for a store path, in process."""
881
+ body = self._call(OP_STAT_FILE, _string(store_path))
882
+ reader = _Reader(body)
883
+ exists = bool(reader.u8())
884
+ size = reader.u64()
885
+ return exists, size
886
+
887
+ def read_file(self, store_path):
888
+ """Read one small store file into a caller-owned memfd.
889
+
890
+ Returns a :class:`TensorBuffer`-shaped object whose ``bytes`` is the
891
+ file content. The store is never mutated.
892
+ """
893
+ exists, size = self.stat_file(store_path)
894
+ if not exists:
895
+ raise ServiceError("NotFound", "no store file at %s" % (store_path,))
896
+ if size > MAX_FILE_BYTES:
897
+ raise ServiceError(
898
+ "LimitExceeded",
899
+ "%s is %d bytes, above the %d read bound"
900
+ % (store_path, size, MAX_FILE_BYTES),
901
+ )
902
+ fd = self._allocate_memfd(size, "weft-read")
903
+ try:
904
+ response = self._call(OP_READ_FILE, _string(store_path), fd=fd)
905
+ (actual,) = struct.unpack("<Q", response)
906
+ if actual != size:
907
+ raise ServiceProtocolError(
908
+ "read_file returned %d bytes, stat said %d" % (actual, size)
909
+ )
910
+ if size:
911
+ mapping = mmap.mmap(fd, size, access=mmap.ACCESS_READ)
912
+ else:
913
+ mapping = mmap.mmap(-1, 1)
914
+ return _FileView(mapping, size)
915
+ finally:
916
+ try:
917
+ os.close(fd)
918
+ except OSError:
919
+ pass
920
+
921
+
922
+ class _FileView:
923
+ """A memfd-backed, read-only snapshot of one small store file."""
924
+
925
+ __slots__ = ("_mm", "size")
926
+
927
+ def __init__(self, mapping, size):
928
+ self._mm = mapping
929
+ self.size = size
930
+
931
+ @property
932
+ def view(self):
933
+ return memoryview(self._mm)[: self.size]
934
+
935
+ @property
936
+ def bytes(self):
937
+ return bytes(self.view)
938
+
939
+ def close(self):
940
+ if self._mm is not None:
941
+ mapping = self._mm
942
+ try:
943
+ mapping.close()
944
+ self._mm = None
945
+ except (BufferError, ValueError):
946
+ self._mm = mapping
947
+
948
+ def __enter__(self):
949
+ return self
950
+
951
+ def __exit__(self, *exc):
952
+ self.close()
953
+ return False
954
+
955
+ def __del__(self):
956
+ try:
957
+ self.close()
958
+ except Exception:
959
+ pass