shared-tensor 0.2.12__tar.gz → 0.2.15__tar.gz
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.
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/PKG-INFO +51 -3
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/README.md +50 -1
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/pyproject.toml +1 -2
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/shared_tensor/__init__.py +1 -1
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/shared_tensor/async_client.py +1 -1
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/shared_tensor/client.py +34 -6
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/shared_tensor/provider.py +1 -1
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/shared_tensor/utils.py +204 -33
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/LICENSE +0 -0
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/MANIFEST.in +0 -0
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/setup.cfg +0 -0
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/shared_tensor/async_task.py +0 -0
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/shared_tensor/errors.py +0 -0
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/shared_tensor/managed_object.py +0 -0
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/shared_tensor/runtime.py +0 -0
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/shared_tensor/server.py +0 -0
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/shared_tensor/transport.py +0 -0
- {shared_tensor-0.2.12 → shared_tensor-0.2.15}/shared_tensor.egg-info/SOURCES.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shared-tensor
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.15
|
|
4
4
|
Summary: Native PyTorch CUDA IPC over Unix Domain Socket for same-host process separation
|
|
5
5
|
Author-email: Athena Team <contact@world-sim-dev.org>
|
|
6
6
|
Maintainer-email: Athena Team <contact@world-sim-dev.org>
|
|
@@ -30,7 +30,6 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
License-File: LICENSE
|
|
31
31
|
Requires-Dist: cloudpickle>=3.0.0
|
|
32
32
|
Requires-Dist: numpy<2
|
|
33
|
-
Requires-Dist: torch>=2.1
|
|
34
33
|
Provides-Extra: dev
|
|
35
34
|
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
36
35
|
Requires-Dist: pytest-cov>=2.0; extra == "dev"
|
|
@@ -78,9 +77,10 @@ Not supported:
|
|
|
78
77
|
|
|
79
78
|
## Install
|
|
80
79
|
|
|
81
|
-
Use Python `3.9
|
|
80
|
+
Use Python `3.9+`. Install a compatible PyTorch build first, then install `shared-tensor`.
|
|
82
81
|
|
|
83
82
|
```bash
|
|
83
|
+
pip install torch
|
|
84
84
|
pip install shared-tensor
|
|
85
85
|
```
|
|
86
86
|
|
|
@@ -92,6 +92,8 @@ conda activate shared-tensor-dev
|
|
|
92
92
|
pip install -e ".[dev,test]"
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
If you want to share Hugging Face `transformers` models, install both `torch` and `transformers` in the server and client environments. `shared-tensor` no longer installs `torch` for you.
|
|
96
|
+
|
|
95
97
|
## Docs
|
|
96
98
|
|
|
97
99
|
Read the examples first, then the design notes:
|
|
@@ -150,6 +152,34 @@ executes endpoint functions reopens CUDA objects via torch IPC
|
|
|
150
152
|
manages cache and refcounts releases managed handles explicitly
|
|
151
153
|
```
|
|
152
154
|
|
|
155
|
+
## Example: Transformers Models
|
|
156
|
+
|
|
157
|
+
`shared_tensor` also supports CUDA `transformers.PreTrainedModel` instances.
|
|
158
|
+
|
|
159
|
+
See:
|
|
160
|
+
- `examples/transformers_two_proc_demo.py`: minimal same-code two-process demo using `AutoModel`-style loading
|
|
161
|
+
- `examples/transformers_mutation_check.py`: proves client-side in-place parameter mutation is visible on the server
|
|
162
|
+
- `examples/transformers_ipc_benchmark.py`: measures reopen latency and client GPU memory delta
|
|
163
|
+
|
|
164
|
+
Usage:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
TRANSFORMERS_MODEL_ROOT=/path/to/model-or-hf-cache/models--bert-base-uncased \
|
|
168
|
+
SHARED_TENSOR_ENABLED=1 SHARED_TENSOR_ROLE=server \
|
|
169
|
+
python examples/transformers_two_proc_demo.py
|
|
170
|
+
|
|
171
|
+
TRANSFORMERS_MODEL_ROOT=/path/to/model-or-hf-cache/models--bert-base-uncased \
|
|
172
|
+
SHARED_TENSOR_ENABLED=1 \
|
|
173
|
+
python examples/transformers_two_proc_demo.py
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Notes:
|
|
177
|
+
- `TRANSFORMERS_MODEL_ROOT` may point either to a resolved local model directory or to a Hugging Face cache root like `models--...`; the example resolves the newest snapshot automatically
|
|
178
|
+
- `TRANSFORMERS_AUTO_CLASS` defaults to `AutoModel` and can be overridden to another `Auto*` class that exposes `from_pretrained`
|
|
179
|
+
- for custom `transformers` code paths, the library stages the required module source files before reopening the shared module on the client
|
|
180
|
+
- transport remains same-host same-GPU torch CUDA IPC; the client should not allocate a second full model copy just to reconstruct parameters
|
|
181
|
+
- in a fresh client Python process, the first reopen may still look slow because `transformers` import/module resolution is often much slower than the shared-tensor IPC restore path itself; a second reopen in the same process should be much faster
|
|
182
|
+
|
|
153
183
|
## Lifetime And Failure Contract
|
|
154
184
|
|
|
155
185
|
`shared_tensor` follows native PyTorch CUDA IPC semantics. It does not virtualize or harden producer lifetime.
|
|
@@ -346,6 +376,8 @@ Use call-level invalidation when you want to evict one cache key.
|
|
|
346
376
|
Use endpoint-level invalidation when you want to drop all cached variants for the endpoint.
|
|
347
377
|
Invalidation removes cache lookup entries; it does not guarantee that already-issued client handles remain valid after producer death.
|
|
348
378
|
|
|
379
|
+
For cached `transformers` model endpoints, keep `cache=True` unless you explicitly want every request to rebuild and re-share the model.
|
|
380
|
+
|
|
349
381
|
|
|
350
382
|
## Handle Health Checks
|
|
351
383
|
|
|
@@ -371,6 +403,22 @@ info = provider.get_runtime_info()
|
|
|
371
403
|
# execution_mode, server_socket_path, server_running, server_ready, server_info...
|
|
372
404
|
```
|
|
373
405
|
|
|
406
|
+
## Client Retry And Timeout Defaults
|
|
407
|
+
|
|
408
|
+
The client now retries initial connection setup for up to `60s` when the server socket is not ready yet, covering the common server-startup race where the client starts slightly earlier.
|
|
409
|
+
|
|
410
|
+
Default request timeout is now `600s` for:
|
|
411
|
+
- `SharedTensorClient`
|
|
412
|
+
- `AsyncSharedTensorClient`
|
|
413
|
+
- `SharedTensorProvider`
|
|
414
|
+
|
|
415
|
+
You can still override these per instance:
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
client = SharedTensorClient(timeout=120.0)
|
|
419
|
+
provider = SharedTensorProvider(timeout=120.0)
|
|
420
|
+
```
|
|
421
|
+
|
|
374
422
|
## Testing
|
|
375
423
|
|
|
376
424
|
Default suite:
|
|
@@ -25,9 +25,10 @@ Not supported:
|
|
|
25
25
|
|
|
26
26
|
## Install
|
|
27
27
|
|
|
28
|
-
Use Python `3.9
|
|
28
|
+
Use Python `3.9+`. Install a compatible PyTorch build first, then install `shared-tensor`.
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
+
pip install torch
|
|
31
32
|
pip install shared-tensor
|
|
32
33
|
```
|
|
33
34
|
|
|
@@ -39,6 +40,8 @@ conda activate shared-tensor-dev
|
|
|
39
40
|
pip install -e ".[dev,test]"
|
|
40
41
|
```
|
|
41
42
|
|
|
43
|
+
If you want to share Hugging Face `transformers` models, install both `torch` and `transformers` in the server and client environments. `shared-tensor` no longer installs `torch` for you.
|
|
44
|
+
|
|
42
45
|
## Docs
|
|
43
46
|
|
|
44
47
|
Read the examples first, then the design notes:
|
|
@@ -97,6 +100,34 @@ executes endpoint functions reopens CUDA objects via torch IPC
|
|
|
97
100
|
manages cache and refcounts releases managed handles explicitly
|
|
98
101
|
```
|
|
99
102
|
|
|
103
|
+
## Example: Transformers Models
|
|
104
|
+
|
|
105
|
+
`shared_tensor` also supports CUDA `transformers.PreTrainedModel` instances.
|
|
106
|
+
|
|
107
|
+
See:
|
|
108
|
+
- `examples/transformers_two_proc_demo.py`: minimal same-code two-process demo using `AutoModel`-style loading
|
|
109
|
+
- `examples/transformers_mutation_check.py`: proves client-side in-place parameter mutation is visible on the server
|
|
110
|
+
- `examples/transformers_ipc_benchmark.py`: measures reopen latency and client GPU memory delta
|
|
111
|
+
|
|
112
|
+
Usage:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
TRANSFORMERS_MODEL_ROOT=/path/to/model-or-hf-cache/models--bert-base-uncased \
|
|
116
|
+
SHARED_TENSOR_ENABLED=1 SHARED_TENSOR_ROLE=server \
|
|
117
|
+
python examples/transformers_two_proc_demo.py
|
|
118
|
+
|
|
119
|
+
TRANSFORMERS_MODEL_ROOT=/path/to/model-or-hf-cache/models--bert-base-uncased \
|
|
120
|
+
SHARED_TENSOR_ENABLED=1 \
|
|
121
|
+
python examples/transformers_two_proc_demo.py
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Notes:
|
|
125
|
+
- `TRANSFORMERS_MODEL_ROOT` may point either to a resolved local model directory or to a Hugging Face cache root like `models--...`; the example resolves the newest snapshot automatically
|
|
126
|
+
- `TRANSFORMERS_AUTO_CLASS` defaults to `AutoModel` and can be overridden to another `Auto*` class that exposes `from_pretrained`
|
|
127
|
+
- for custom `transformers` code paths, the library stages the required module source files before reopening the shared module on the client
|
|
128
|
+
- transport remains same-host same-GPU torch CUDA IPC; the client should not allocate a second full model copy just to reconstruct parameters
|
|
129
|
+
- in a fresh client Python process, the first reopen may still look slow because `transformers` import/module resolution is often much slower than the shared-tensor IPC restore path itself; a second reopen in the same process should be much faster
|
|
130
|
+
|
|
100
131
|
## Lifetime And Failure Contract
|
|
101
132
|
|
|
102
133
|
`shared_tensor` follows native PyTorch CUDA IPC semantics. It does not virtualize or harden producer lifetime.
|
|
@@ -293,6 +324,8 @@ Use call-level invalidation when you want to evict one cache key.
|
|
|
293
324
|
Use endpoint-level invalidation when you want to drop all cached variants for the endpoint.
|
|
294
325
|
Invalidation removes cache lookup entries; it does not guarantee that already-issued client handles remain valid after producer death.
|
|
295
326
|
|
|
327
|
+
For cached `transformers` model endpoints, keep `cache=True` unless you explicitly want every request to rebuild and re-share the model.
|
|
328
|
+
|
|
296
329
|
|
|
297
330
|
## Handle Health Checks
|
|
298
331
|
|
|
@@ -318,6 +351,22 @@ info = provider.get_runtime_info()
|
|
|
318
351
|
# execution_mode, server_socket_path, server_running, server_ready, server_info...
|
|
319
352
|
```
|
|
320
353
|
|
|
354
|
+
## Client Retry And Timeout Defaults
|
|
355
|
+
|
|
356
|
+
The client now retries initial connection setup for up to `60s` when the server socket is not ready yet, covering the common server-startup race where the client starts slightly earlier.
|
|
357
|
+
|
|
358
|
+
Default request timeout is now `600s` for:
|
|
359
|
+
- `SharedTensorClient`
|
|
360
|
+
- `AsyncSharedTensorClient`
|
|
361
|
+
- `SharedTensorProvider`
|
|
362
|
+
|
|
363
|
+
You can still override these per instance:
|
|
364
|
+
|
|
365
|
+
```python
|
|
366
|
+
client = SharedTensorClient(timeout=120.0)
|
|
367
|
+
provider = SharedTensorProvider(timeout=120.0)
|
|
368
|
+
```
|
|
369
|
+
|
|
321
370
|
## Testing
|
|
322
371
|
|
|
323
372
|
Default suite:
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "shared-tensor"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.15"
|
|
8
8
|
description = "Native PyTorch CUDA IPC over Unix Domain Socket for same-host process separation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -47,7 +47,6 @@ requires-python = ">=3.9"
|
|
|
47
47
|
dependencies = [
|
|
48
48
|
"cloudpickle>=3.0.0",
|
|
49
49
|
"numpy<2",
|
|
50
|
-
"torch>=2.1",
|
|
51
50
|
]
|
|
52
51
|
|
|
53
52
|
[project.optional-dependencies]
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
import errno
|
|
6
7
|
import socket
|
|
8
|
+
import time
|
|
7
9
|
from dataclasses import dataclass
|
|
8
10
|
from typing import Any, cast
|
|
9
11
|
|
|
@@ -32,6 +34,9 @@ from shared_tensor.utils import (
|
|
|
32
34
|
|
|
33
35
|
logger = logging.getLogger(__name__)
|
|
34
36
|
|
|
37
|
+
CONNECT_RETRY_TIMEOUT_SECONDS = 60.0
|
|
38
|
+
CONNECT_RETRY_INTERVAL_SECONDS = 0.5
|
|
39
|
+
|
|
35
40
|
|
|
36
41
|
@dataclass(slots=True)
|
|
37
42
|
class _ClientReleaser(ReleaseHandle):
|
|
@@ -53,7 +58,7 @@ class SharedTensorClient:
|
|
|
53
58
|
base_path: str = "/tmp/shared-tensor",
|
|
54
59
|
*,
|
|
55
60
|
device_index: int | None = None,
|
|
56
|
-
timeout: float =
|
|
61
|
+
timeout: float = 600.0,
|
|
57
62
|
verbose_debug: bool = False,
|
|
58
63
|
) -> None:
|
|
59
64
|
self.base_path = base_path
|
|
@@ -117,12 +122,35 @@ class SharedTensorClient:
|
|
|
117
122
|
method = request.get("method", "<unknown>")
|
|
118
123
|
if self.verbose_debug:
|
|
119
124
|
logger.debug("Client sending request", extra={"method": method, "socket_path": self.socket_path})
|
|
125
|
+
started_at = time.monotonic()
|
|
120
126
|
try:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
127
|
+
while True:
|
|
128
|
+
try:
|
|
129
|
+
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
|
|
130
|
+
sock.settimeout(self.timeout)
|
|
131
|
+
sock.connect(self.socket_path)
|
|
132
|
+
send_message(sock, request)
|
|
133
|
+
response = recv_message(sock)
|
|
134
|
+
break
|
|
135
|
+
except OSError as exc:
|
|
136
|
+
retryable = exc.errno in {
|
|
137
|
+
errno.ENOENT,
|
|
138
|
+
errno.ECONNREFUSED,
|
|
139
|
+
}
|
|
140
|
+
elapsed = time.monotonic() - started_at
|
|
141
|
+
if not retryable or elapsed >= CONNECT_RETRY_TIMEOUT_SECONDS:
|
|
142
|
+
raise
|
|
143
|
+
if self.verbose_debug:
|
|
144
|
+
logger.info(
|
|
145
|
+
"Client retrying request connection",
|
|
146
|
+
extra={
|
|
147
|
+
"method": method,
|
|
148
|
+
"socket_path": self.socket_path,
|
|
149
|
+
"elapsed": round(elapsed, 3),
|
|
150
|
+
"error": str(exc),
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
time.sleep(CONNECT_RETRY_INTERVAL_SECONDS)
|
|
126
154
|
except TimeoutError as exc:
|
|
127
155
|
if self.verbose_debug:
|
|
128
156
|
logger.warning("Client request timed out", extra={"method": method, "socket_path": self.socket_path})
|
|
@@ -91,7 +91,7 @@ class SharedTensorProvider:
|
|
|
91
91
|
*,
|
|
92
92
|
enabled: bool | None = None,
|
|
93
93
|
device_index: int | None = None,
|
|
94
|
-
timeout: float =
|
|
94
|
+
timeout: float = 600.0,
|
|
95
95
|
execution_mode: str = "auto",
|
|
96
96
|
server_startup_timeout: float = 30.0,
|
|
97
97
|
verbose_debug: bool = False,
|
|
@@ -9,6 +9,8 @@ import io
|
|
|
9
9
|
import multiprocessing.reduction as mp_reduction
|
|
10
10
|
import os
|
|
11
11
|
import pickle
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
12
14
|
from pathlib import Path
|
|
13
15
|
from collections.abc import Callable
|
|
14
16
|
from typing import Any, cast
|
|
@@ -32,6 +34,7 @@ except ImportError: # pragma: no cover - exercised when transformers is not ins
|
|
|
32
34
|
|
|
33
35
|
TORCH_MODULE: Any | None = _torch
|
|
34
36
|
TRANSFORMERS_PRETRAINED_MODEL: Any | None = _transformers_pretrained_model
|
|
37
|
+
_TRANSFORMERS_CLASS_CACHE: dict[tuple[str, str], Any] = {}
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
CONTROL_ENCODING = "control_pickle"
|
|
@@ -174,16 +177,21 @@ def _serialize_transformers_model(module: Any) -> bytes:
|
|
|
174
177
|
config = getattr(module, "config", None)
|
|
175
178
|
if config is None:
|
|
176
179
|
raise SharedTensorSerializationError("transformers model is missing a config")
|
|
177
|
-
|
|
178
|
-
|
|
180
|
+
module_sources = _collect_transformers_module_sources(type(module).__module__, type(config).__module__)
|
|
181
|
+
parameters = dict(module.named_parameters(remove_duplicate=False))
|
|
182
|
+
buffers = dict(module.named_buffers(remove_duplicate=False))
|
|
179
183
|
payload = {
|
|
184
|
+
"module_sources": module_sources,
|
|
185
|
+
"config_bytes": pickle.dumps(config, protocol=pickle.HIGHEST_PROTOCOL),
|
|
180
186
|
"model_module": type(module).__module__,
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
+
"model_qualname": type(module).__qualname__,
|
|
188
|
+
"state_bytes": _torch_serialize(
|
|
189
|
+
{
|
|
190
|
+
"parameters": parameters,
|
|
191
|
+
"buffers": buffers,
|
|
192
|
+
}
|
|
193
|
+
),
|
|
194
|
+
"training": bool(getattr(module, "training", False)),
|
|
187
195
|
}
|
|
188
196
|
return pickle.dumps(payload, protocol=pickle.HIGHEST_PROTOCOL)
|
|
189
197
|
|
|
@@ -240,37 +248,200 @@ def serialize_call_payloads(
|
|
|
240
248
|
raise SharedTensorSerializationError(f"Failed to serialize call payloads: {exc}") from exc
|
|
241
249
|
|
|
242
250
|
|
|
243
|
-
def
|
|
244
|
-
|
|
245
|
-
if
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if
|
|
251
|
+
def _bundle_module_files(module_name: str) -> dict[str, str] | None:
|
|
252
|
+
module = sys.modules.get(module_name)
|
|
253
|
+
if module is None:
|
|
254
|
+
module = importlib.import_module(module_name)
|
|
255
|
+
module_file = getattr(module, "__file__", None)
|
|
256
|
+
if not module_file:
|
|
257
|
+
return None
|
|
258
|
+
module_path = Path(module_file).resolve()
|
|
259
|
+
if module_path.suffix != ".py":
|
|
260
|
+
return None
|
|
261
|
+
transformers_root = None
|
|
262
|
+
if TRANSFORMERS_PRETRAINED_MODEL is not None:
|
|
263
|
+
transformers_file = getattr(sys.modules.get("transformers"), "__file__", None)
|
|
264
|
+
if transformers_file is not None:
|
|
265
|
+
transformers_root = Path(transformers_file).resolve().parent
|
|
266
|
+
if transformers_root is not None:
|
|
267
|
+
try:
|
|
268
|
+
module_path.relative_to(transformers_root)
|
|
269
|
+
if not module_name.startswith("transformers_modules."):
|
|
270
|
+
return None
|
|
271
|
+
except ValueError:
|
|
272
|
+
pass
|
|
273
|
+
files = {module_path.name: module_path.read_text(encoding="utf-8")}
|
|
274
|
+
get_relative_import_files = getattr(importlib.import_module("transformers.dynamic_module_utils"), "get_relative_import_files", None)
|
|
275
|
+
if callable(get_relative_import_files):
|
|
276
|
+
for needed in get_relative_import_files(str(module_path)):
|
|
277
|
+
needed_path = Path(needed).resolve()
|
|
278
|
+
files[needed_path.name] = needed_path.read_text(encoding="utf-8")
|
|
279
|
+
return files
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _collect_transformers_module_sources(*module_names: str) -> dict[str, dict[str, str]]:
|
|
283
|
+
bundles: dict[str, dict[str, str]] = {}
|
|
284
|
+
for module_name in module_names:
|
|
285
|
+
bundle = _bundle_module_files(module_name)
|
|
286
|
+
if bundle:
|
|
287
|
+
bundles[module_name] = bundle
|
|
288
|
+
return bundles
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _stage_transformers_module_sources(module_sources: dict[str, dict[str, str]]) -> None:
|
|
292
|
+
if not module_sources:
|
|
293
|
+
return
|
|
294
|
+
staging_root = Path(tempfile.gettempdir()) / "shared-tensor-transformers-modules"
|
|
295
|
+
staging_root.mkdir(parents=True, exist_ok=True)
|
|
296
|
+
root_str = str(staging_root)
|
|
297
|
+
if root_str not in sys.path:
|
|
298
|
+
sys.path.insert(0, root_str)
|
|
299
|
+
for module_name, files in module_sources.items():
|
|
300
|
+
parts = module_name.split(".")
|
|
301
|
+
package_dir = staging_root.joinpath(*parts[:-1])
|
|
302
|
+
package_dir.mkdir(parents=True, exist_ok=True)
|
|
303
|
+
current = staging_root
|
|
304
|
+
for part in parts[:-1]:
|
|
305
|
+
current = current / part
|
|
306
|
+
init_file = current / "__init__.py"
|
|
307
|
+
if not init_file.exists():
|
|
308
|
+
init_file.write_text("", encoding="utf-8")
|
|
309
|
+
for filename, content in files.items():
|
|
310
|
+
(package_dir / filename).write_text(content, encoding="utf-8")
|
|
311
|
+
importlib.invalidate_caches()
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _import_transformers_module(module_name: str, module_sources: dict[str, dict[str, str]]) -> Any:
|
|
315
|
+
try:
|
|
316
|
+
return importlib.import_module(module_name)
|
|
317
|
+
except ModuleNotFoundError:
|
|
318
|
+
_stage_transformers_module_sources(module_sources)
|
|
319
|
+
return importlib.import_module(module_name)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _resolve_module_qualname(module_name: str, qualname: str) -> Any:
|
|
323
|
+
cache_key = (module_name, qualname)
|
|
324
|
+
cached = _TRANSFORMERS_CLASS_CACHE.get(cache_key)
|
|
325
|
+
if cached is not None:
|
|
326
|
+
return cached
|
|
327
|
+
module = importlib.import_module(module_name)
|
|
328
|
+
current: Any = module
|
|
329
|
+
for part in qualname.split("."):
|
|
330
|
+
current = getattr(current, part)
|
|
331
|
+
_TRANSFORMERS_CLASS_CACHE[cache_key] = current
|
|
332
|
+
return current
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _build_transformers_meta_shell(model_cls: Any, config: Any) -> Any:
|
|
336
|
+
if TORCH_MODULE is None:
|
|
337
|
+
raise SharedTensorSerializationError("PyTorch is required for transformers deserialization")
|
|
338
|
+
try:
|
|
339
|
+
with TORCH_MODULE.device("meta"):
|
|
340
|
+
model = model_cls(config)
|
|
341
|
+
except Exception as exc:
|
|
252
342
|
raise SharedTensorSerializationError(
|
|
253
|
-
f"Failed to
|
|
343
|
+
f"Failed to construct transformers model shell on meta device: {exc}"
|
|
344
|
+
) from exc
|
|
345
|
+
return model
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _resolve_parent_module(root: Any, parent_name: str) -> Any:
|
|
349
|
+
if not parent_name:
|
|
350
|
+
return root
|
|
351
|
+
get_submodule = getattr(root, "get_submodule", None)
|
|
352
|
+
if callable(get_submodule):
|
|
353
|
+
return get_submodule(parent_name)
|
|
354
|
+
current = root
|
|
355
|
+
for part in parent_name.split("."):
|
|
356
|
+
current = getattr(current, part)
|
|
357
|
+
return current
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _rebind_named_parameter(module: Any, name: str, value: Any) -> None:
|
|
361
|
+
parent_name, _, local_name = name.rpartition(".")
|
|
362
|
+
parent = _resolve_parent_module(module, parent_name)
|
|
363
|
+
parent._parameters[local_name] = value
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _rebind_named_buffer(module: Any, name: str, value: Any) -> None:
|
|
367
|
+
parent_name, _, local_name = name.rpartition(".")
|
|
368
|
+
parent = _resolve_parent_module(module, parent_name)
|
|
369
|
+
parent._buffers[local_name] = value
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _validate_transformers_state_layout(module: Any, parameters: dict[str, Any], buffers: dict[str, Any]) -> None:
|
|
373
|
+
expected_parameters = dict(module.named_parameters(remove_duplicate=False))
|
|
374
|
+
expected_buffers = dict(module.named_buffers(remove_duplicate=False))
|
|
375
|
+
if set(expected_parameters) != set(parameters):
|
|
376
|
+
raise SharedTensorSerializationError(
|
|
377
|
+
"Transformers parameter layout mismatch between serialized model and client shell"
|
|
378
|
+
)
|
|
379
|
+
if set(expected_buffers) != set(buffers):
|
|
380
|
+
raise SharedTensorSerializationError(
|
|
381
|
+
"Transformers buffer layout mismatch between serialized model and client shell"
|
|
254
382
|
)
|
|
383
|
+
for name, expected in expected_parameters.items():
|
|
384
|
+
actual = parameters[name]
|
|
385
|
+
if not isinstance(actual, TORCH_MODULE.nn.Parameter):
|
|
386
|
+
raise SharedTensorSerializationError(
|
|
387
|
+
f"Transformers parameter '{name}' payload is not a torch.nn.Parameter"
|
|
388
|
+
)
|
|
389
|
+
if actual.shape != expected.shape or actual.dtype != expected.dtype:
|
|
390
|
+
raise SharedTensorSerializationError(
|
|
391
|
+
f"Transformers parameter '{name}' shape or dtype does not match the client shell"
|
|
392
|
+
)
|
|
393
|
+
for name, expected in expected_buffers.items():
|
|
394
|
+
actual = buffers[name]
|
|
395
|
+
if not isinstance(actual, TORCH_MODULE.Tensor):
|
|
396
|
+
raise SharedTensorSerializationError(
|
|
397
|
+
f"Transformers buffer '{name}' payload is not a torch.Tensor"
|
|
398
|
+
)
|
|
399
|
+
if actual.shape != expected.shape or actual.dtype != expected.dtype:
|
|
400
|
+
raise SharedTensorSerializationError(
|
|
401
|
+
f"Transformers buffer '{name}' shape or dtype does not match the client shell"
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _restore_transformers_state(module: Any, state: dict[str, Any]) -> Any:
|
|
406
|
+
parameters = cast(dict[str, Any], state.get("parameters", {}))
|
|
407
|
+
buffers = cast(dict[str, Any], state.get("buffers", {}))
|
|
408
|
+
_validate_transformers_state_layout(module, parameters, buffers)
|
|
409
|
+
for name, value in parameters.items():
|
|
410
|
+
_rebind_named_parameter(module, name, value)
|
|
411
|
+
for name, value in buffers.items():
|
|
412
|
+
_rebind_named_buffer(module, name, value)
|
|
413
|
+
return module
|
|
255
414
|
|
|
256
415
|
|
|
257
416
|
def _deserialize_transformers_model(payload: bytes) -> Any:
|
|
258
417
|
encoded = pickle.loads(payload)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if
|
|
271
|
-
model
|
|
272
|
-
|
|
273
|
-
|
|
418
|
+
module_sources = cast(dict[str, dict[str, str]], encoded.get("module_sources", {}))
|
|
419
|
+
for module_name in module_sources:
|
|
420
|
+
_import_transformers_module(module_name, module_sources)
|
|
421
|
+
config_bytes = encoded.get("config_bytes")
|
|
422
|
+
state_bytes = encoded.get("state_bytes")
|
|
423
|
+
model_module = encoded.get("model_module")
|
|
424
|
+
model_qualname = encoded.get("model_qualname")
|
|
425
|
+
if not isinstance(config_bytes, (bytes, bytearray)):
|
|
426
|
+
raise SharedTensorSerializationError("transformers config payload is invalid")
|
|
427
|
+
if not isinstance(state_bytes, (bytes, bytearray)):
|
|
428
|
+
raise SharedTensorSerializationError("transformers state payload is invalid")
|
|
429
|
+
if not isinstance(model_module, str) or not model_module:
|
|
430
|
+
raise SharedTensorSerializationError("transformers model module is invalid")
|
|
431
|
+
if not isinstance(model_qualname, str) or not model_qualname:
|
|
432
|
+
raise SharedTensorSerializationError("transformers model qualname is invalid")
|
|
433
|
+
config = pickle.loads(config_bytes)
|
|
434
|
+
model_cls = _resolve_module_qualname(model_module, model_qualname)
|
|
435
|
+
model = _build_transformers_meta_shell(model_cls, config)
|
|
436
|
+
state = pickle.loads(state_bytes)
|
|
437
|
+
model = _restore_transformers_state(model, cast(dict[str, Any], state))
|
|
438
|
+
train_fn = getattr(model, "train", None)
|
|
439
|
+
training = bool(encoded.get("training", False))
|
|
440
|
+
if callable(train_fn):
|
|
441
|
+
train_fn(training)
|
|
442
|
+
eval_fn = getattr(model, "eval", None)
|
|
443
|
+
if callable(eval_fn) and not training:
|
|
444
|
+
eval_fn()
|
|
274
445
|
_validate_module_device(model)
|
|
275
446
|
return model
|
|
276
447
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|