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/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
|