shared-tensor 0.2.13__tar.gz → 0.2.16__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shared-tensor
3
- Version: 0.2.13
3
+ Version: 0.2.16
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+` and a CUDA-enabled PyTorch build.
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,31 @@ info = provider.get_runtime_info()
371
403
  # execution_mode, server_socket_path, server_running, server_ready, server_info...
372
404
  ```
373
405
 
406
+ ## Logging
407
+
408
+ `shared_tensor` now installs a default package logger on import and enables detailed logs by default.
409
+
410
+ - default level: `INFO`
411
+ - logger name: `shared_tensor`
412
+ - override level with `SHARED_TENSOR_LOG_LEVEL`, for example `INFO`, `WARNING`, or `ERROR`
413
+ - pass `verbose_debug=False` to `SharedTensorProvider`, `SharedTensorClient`, `AsyncSharedTensorClient`, or `SharedTensorServer` if you want to suppress detailed request-level logs
414
+
415
+ ## Client Retry And Timeout Defaults
416
+
417
+ 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.
418
+
419
+ Default request timeout is now `600s` for:
420
+ - `SharedTensorClient`
421
+ - `AsyncSharedTensorClient`
422
+ - `SharedTensorProvider`
423
+
424
+ You can still override these per instance:
425
+
426
+ ```python
427
+ client = SharedTensorClient(timeout=120.0)
428
+ provider = SharedTensorProvider(timeout=120.0)
429
+ ```
430
+
374
431
  ## Testing
375
432
 
376
433
  Default suite:
@@ -25,9 +25,10 @@ Not supported:
25
25
 
26
26
  ## Install
27
27
 
28
- Use Python `3.9+` and a CUDA-enabled PyTorch build.
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,31 @@ info = provider.get_runtime_info()
318
351
  # execution_mode, server_socket_path, server_running, server_ready, server_info...
319
352
  ```
320
353
 
354
+ ## Logging
355
+
356
+ `shared_tensor` now installs a default package logger on import and enables detailed logs by default.
357
+
358
+ - default level: `INFO`
359
+ - logger name: `shared_tensor`
360
+ - override level with `SHARED_TENSOR_LOG_LEVEL`, for example `INFO`, `WARNING`, or `ERROR`
361
+ - pass `verbose_debug=False` to `SharedTensorProvider`, `SharedTensorClient`, `AsyncSharedTensorClient`, or `SharedTensorServer` if you want to suppress detailed request-level logs
362
+
363
+ ## Client Retry And Timeout Defaults
364
+
365
+ 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.
366
+
367
+ Default request timeout is now `600s` for:
368
+ - `SharedTensorClient`
369
+ - `AsyncSharedTensorClient`
370
+ - `SharedTensorProvider`
371
+
372
+ You can still override these per instance:
373
+
374
+ ```python
375
+ client = SharedTensorClient(timeout=120.0)
376
+ provider = SharedTensorProvider(timeout=120.0)
377
+ ```
378
+
321
379
  ## Testing
322
380
 
323
381
  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.13"
7
+ version = "0.2.16"
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]
@@ -0,0 +1,51 @@
1
+ """shared_tensor: same-host same-GPU PyTorch CUDA IPC over local UDS RPC."""
2
+
3
+ import logging
4
+ import os
5
+
6
+
7
+ class _SafeStreamHandler(logging.StreamHandler):
8
+ def emit(self, record: logging.LogRecord) -> None:
9
+ stream = self.stream
10
+ if stream is None or getattr(stream, "closed", False):
11
+ return
12
+ try:
13
+ super().emit(record)
14
+ except ValueError:
15
+ return
16
+
17
+
18
+ def _configure_default_logging() -> None:
19
+ logger = logging.getLogger("shared_tensor")
20
+ if logger.handlers:
21
+ return
22
+ level_name = os.getenv("SHARED_TENSOR_LOG_LEVEL", "INFO").strip().upper() or "INFO"
23
+ level = getattr(logging, level_name, logging.INFO)
24
+ handler = _SafeStreamHandler()
25
+ handler.setFormatter(logging.Formatter("[shared_tensor] %(levelname)s %(name)s: %(message)s"))
26
+ logger.addHandler(handler)
27
+ logger.setLevel(level)
28
+ logger.propagate = False
29
+
30
+ _configure_default_logging()
31
+
32
+ from shared_tensor.async_client import AsyncSharedTensorClient
33
+ from shared_tensor.async_task import TaskInfo, TaskStatus
34
+ from shared_tensor.client import SharedTensorClient
35
+ from shared_tensor.errors import SharedTensorStaleHandleError
36
+ from shared_tensor.managed_object import SharedObjectHandle
37
+ from shared_tensor.provider import SharedTensorProvider
38
+ from shared_tensor.server import SharedTensorServer
39
+
40
+ __all__ = [
41
+ "AsyncSharedTensorClient",
42
+ "SharedTensorClient",
43
+ "SharedObjectHandle",
44
+ "SharedTensorStaleHandleError",
45
+ "SharedTensorProvider",
46
+ "SharedTensorServer",
47
+ "TaskInfo",
48
+ "TaskStatus",
49
+ ]
50
+
51
+ __version__ = "0.2.16"
@@ -20,7 +20,7 @@ class AsyncSharedTensorClient:
20
20
  def __init__(
21
21
  self,
22
22
  base_path: str = "/tmp/shared-tensor",
23
- verbose_debug: bool = False,
23
+ verbose_debug: bool = True,
24
24
  poll_interval: float = 1.0,
25
25
  *,
26
26
  device_index: int | None = None,
@@ -59,7 +59,7 @@ class SharedTensorClient:
59
59
  *,
60
60
  device_index: int | None = None,
61
61
  timeout: float = 600.0,
62
- verbose_debug: bool = False,
62
+ verbose_debug: bool = True,
63
63
  ) -> None:
64
64
  self.base_path = base_path
65
65
  self.device_index = device_index
@@ -94,7 +94,7 @@ class SharedTensorProvider:
94
94
  timeout: float = 600.0,
95
95
  execution_mode: str = "auto",
96
96
  server_startup_timeout: float = 30.0,
97
- verbose_debug: bool = False,
97
+ verbose_debug: bool = True,
98
98
  ) -> None:
99
99
  resolved_mode, auto_mode = _resolve_execution_mode(
100
100
  execution_mode,
@@ -97,7 +97,7 @@ class SharedTensorServer:
97
97
  result_ttl: float = 3600.0,
98
98
  process_start_method: str | None = None,
99
99
  startup_timeout: float = 30.0,
100
- verbose_debug: bool = False,
100
+ verbose_debug: bool = True,
101
101
  ) -> None:
102
102
  self.provider = provider or SharedTensorProvider(execution_mode="server")
103
103
  self.socket_path = socket_path or resolve_runtime_socket_path(
@@ -34,6 +34,7 @@ except ImportError: # pragma: no cover - exercised when transformers is not ins
34
34
 
35
35
  TORCH_MODULE: Any | None = _torch
36
36
  TRANSFORMERS_PRETRAINED_MODEL: Any | None = _transformers_pretrained_model
37
+ _TRANSFORMERS_CLASS_CACHE: dict[tuple[str, str], Any] = {}
37
38
 
38
39
 
39
40
  CONTROL_ENCODING = "control_pickle"
@@ -177,9 +178,20 @@ def _serialize_transformers_model(module: Any) -> bytes:
177
178
  if config is None:
178
179
  raise SharedTensorSerializationError("transformers model is missing a config")
179
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))
180
183
  payload = {
181
184
  "module_sources": module_sources,
182
- "module_bytes": _torch_serialize(module),
185
+ "config_bytes": pickle.dumps(config, protocol=pickle.HIGHEST_PROTOCOL),
186
+ "model_module": type(module).__module__,
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)),
183
195
  }
184
196
  return pickle.dumps(payload, protocol=pickle.HIGHEST_PROTOCOL)
185
197
 
@@ -307,17 +319,128 @@ def _import_transformers_module(module_name: str, module_sources: dict[str, dict
307
319
  return importlib.import_module(module_name)
308
320
 
309
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:
342
+ raise SharedTensorSerializationError(
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"
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
414
+
415
+
310
416
  def _deserialize_transformers_model(payload: bytes) -> Any:
311
417
  encoded = pickle.loads(payload)
312
418
  module_sources = cast(dict[str, dict[str, str]], encoded.get("module_sources", {}))
313
419
  for module_name in module_sources:
314
420
  _import_transformers_module(module_name, module_sources)
315
- module_bytes = encoded.get("module_bytes")
316
- if not isinstance(module_bytes, (bytes, bytearray)):
317
- raise SharedTensorSerializationError("transformers module payload is invalid")
318
- model = pickle.loads(module_bytes)
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)
319
442
  eval_fn = getattr(model, "eval", None)
320
- if callable(eval_fn):
443
+ if callable(eval_fn) and not training:
321
444
  eval_fn()
322
445
  _validate_module_device(model)
323
446
  return model
@@ -1,22 +0,0 @@
1
- """shared_tensor: same-host same-GPU PyTorch CUDA IPC over local UDS RPC."""
2
-
3
- from shared_tensor.async_client import AsyncSharedTensorClient
4
- from shared_tensor.async_task import TaskInfo, TaskStatus
5
- from shared_tensor.client import SharedTensorClient
6
- from shared_tensor.errors import SharedTensorStaleHandleError
7
- from shared_tensor.managed_object import SharedObjectHandle
8
- from shared_tensor.provider import SharedTensorProvider
9
- from shared_tensor.server import SharedTensorServer
10
-
11
- __all__ = [
12
- "AsyncSharedTensorClient",
13
- "SharedTensorClient",
14
- "SharedObjectHandle",
15
- "SharedTensorStaleHandleError",
16
- "SharedTensorProvider",
17
- "SharedTensorServer",
18
- "TaskInfo",
19
- "TaskStatus",
20
- ]
21
-
22
- __version__ = "0.2.13"
File without changes
File without changes