callserver 0.1.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.
- callserver/__init__.py +54 -0
- callserver/__main__.py +4 -0
- callserver/allocationmanager.py +176 -0
- callserver/allocationset.py +381 -0
- callserver/callserver.md +315 -0
- callserver/callserver.py +511 -0
- callserver-0.1.0.dist-info/METADATA +134 -0
- callserver-0.1.0.dist-info/RECORD +12 -0
- callserver-0.1.0.dist-info/WHEEL +5 -0
- callserver-0.1.0.dist-info/entry_points.txt +2 -0
- callserver-0.1.0.dist-info/licenses/LICENSE +21 -0
- callserver-0.1.0.dist-info/top_level.txt +1 -0
callserver/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared-memory RPC module for high-performance inter-process communication.
|
|
3
|
+
|
|
4
|
+
This module provides an efficient Remote Procedure Call (RPC) mechanism that uses
|
|
5
|
+
shared memory for transferring large numpy arrays between processes on the same machine.
|
|
6
|
+
It is designed for use cases where low-latency, high-throughput data exchange is required,
|
|
7
|
+
such as real-time robot control, vision processing pipelines, or any compute-intensive
|
|
8
|
+
workload that benefits from zero-copy data sharing.
|
|
9
|
+
|
|
10
|
+
Key Features:
|
|
11
|
+
- Automatic promotion of large numpy arrays to shared memory for zero-copy transfer
|
|
12
|
+
- Support for both synchronous and asynchronous calls
|
|
13
|
+
- Front/back buffer pattern for overlapping computation and communication
|
|
14
|
+
- Connection to child processes or existing server processes via TCP
|
|
15
|
+
|
|
16
|
+
Limitations:
|
|
17
|
+
- Only supports inter-process communication on the same machine (shared memory is local)
|
|
18
|
+
- Cannot be used for distributed or remote execution across machines
|
|
19
|
+
|
|
20
|
+
Classes:
|
|
21
|
+
CallServer: Main client interface for making RPC calls to a server process.
|
|
22
|
+
AllocationSet: Manages a set of shared memory allocations for a single function.
|
|
23
|
+
AllocationDescriptor: Descriptor for a shared memory buffer (name, shape, dtype).
|
|
24
|
+
AllocationSetManager: Manages multiple AllocationSet instances for resource allocation.
|
|
25
|
+
|
|
26
|
+
Functions:
|
|
27
|
+
run: Run the server loop, either on a child-process connection or as a
|
|
28
|
+
standalone TCP server on a given (address, port, authkey) endpoint.
|
|
29
|
+
run_console: Command-line entry point for running a standalone server.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> import numpy as np
|
|
33
|
+
>>> from callserver import CallServer
|
|
34
|
+
>>> with CallServer('adder') as server:
|
|
35
|
+
... result = server.call(np.add, np.ones((1024, 1024)), np.ones((1024, 1024)))
|
|
36
|
+
>>> print(result[0, 0])
|
|
37
|
+
2.0
|
|
38
|
+
|
|
39
|
+
For more details, see the callserver.md documentation file.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from .allocationmanager import AllocationSetManager
|
|
43
|
+
from .allocationset import AllocationDescriptor, AllocationSet
|
|
44
|
+
from .callserver import DEFAULT_AUTHKEY, CallServer, run, run_console
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"CallServer",
|
|
48
|
+
"run",
|
|
49
|
+
"run_console",
|
|
50
|
+
"DEFAULT_AUTHKEY",
|
|
51
|
+
"AllocationSet",
|
|
52
|
+
"AllocationDescriptor",
|
|
53
|
+
"AllocationSetManager",
|
|
54
|
+
]
|
callserver/__main__.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AllocationSetManager for managing multiple function allocation sets.
|
|
3
|
+
|
|
4
|
+
This module provides the AllocationSetManager class which coordinates multiple
|
|
5
|
+
AllocationSet instances for different functions in an RPC workflow.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Callable, Dict, List, Optional, Union
|
|
11
|
+
|
|
12
|
+
from .allocationset import AllocationSet
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AllocationSetManager:
|
|
16
|
+
"""
|
|
17
|
+
Manages multiple AllocationSet instances for resource allocation in RPC workflows.
|
|
18
|
+
|
|
19
|
+
This class implements a singleton pattern per name, ensuring that all references
|
|
20
|
+
to the same named manager share state. It supports front- and back-buffering
|
|
21
|
+
for function arguments, tracks active sets, and handles cleanup.
|
|
22
|
+
|
|
23
|
+
The manager provides multiple allocation sets per function to support asynchronous
|
|
24
|
+
call patterns where one set is being used for a call in progress while another
|
|
25
|
+
is being prepared for the next call.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
_managers: Class-level dict of all manager instances by name.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
>>> manager = AllocationSetManager("my_server")
|
|
32
|
+
>>> alloc_set = manager.add_allocation_set("my_function")
|
|
33
|
+
>>> # Add a second set for async double-buffering
|
|
34
|
+
>>> alloc_set2 = manager.add_allocation_set("my_function")
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
_managers: Dict[str, "AllocationSetManager"] = {}
|
|
38
|
+
|
|
39
|
+
def __new__(cls, name: Optional[str] = None) -> "AllocationSetManager":
|
|
40
|
+
"""
|
|
41
|
+
Return existing manager for the given name, or create a new one.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
name: The unique name for this manager.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
An AllocationSetManager instance (existing or new).
|
|
48
|
+
"""
|
|
49
|
+
if name is not None and name in cls._managers:
|
|
50
|
+
return cls._managers[name]
|
|
51
|
+
return super().__new__(cls)
|
|
52
|
+
|
|
53
|
+
def __init__(self, name: Optional[str] = None) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Initialize the AllocationSetManager.
|
|
56
|
+
|
|
57
|
+
If a manager with the given name already exists, this is a no-op.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
name: The unique name for this manager.
|
|
61
|
+
"""
|
|
62
|
+
if name is not None and name in AllocationSetManager._managers:
|
|
63
|
+
return
|
|
64
|
+
self._name = name
|
|
65
|
+
self._arg_sets: Dict[str, List[AllocationSet]] = {}
|
|
66
|
+
self._active_set_ids: Dict[str, int] = {}
|
|
67
|
+
if self._name is not None:
|
|
68
|
+
assert (
|
|
69
|
+
self._name not in AllocationSetManager._managers
|
|
70
|
+
), f"An AllocationSetManager with name {self._name} already exists."
|
|
71
|
+
AllocationSetManager._managers[self._name] = self
|
|
72
|
+
|
|
73
|
+
def _make_name(self, fn: Union[Callable, str]) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Convert a function or string to a string name.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
fn: A callable or string identifier.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
The function's __name__ attribute, or the string itself.
|
|
82
|
+
"""
|
|
83
|
+
return fn.__name__ if callable(fn) else fn
|
|
84
|
+
|
|
85
|
+
def add_allocation_set(self, fn: Union[Callable, str]) -> AllocationSet:
|
|
86
|
+
"""
|
|
87
|
+
Add a new AllocationSet for the given function.
|
|
88
|
+
|
|
89
|
+
Creates a new allocation set and adds it to the list of sets for the
|
|
90
|
+
specified function. Multiple sets can be created for double-buffering.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
fn: A callable or string identifier for the function.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
The newly created AllocationSet.
|
|
97
|
+
"""
|
|
98
|
+
fn_name = self._make_name(fn)
|
|
99
|
+
if fn_name not in self._arg_sets:
|
|
100
|
+
self._arg_sets[fn_name] = []
|
|
101
|
+
self._active_set_ids[fn_name] = -1
|
|
102
|
+
|
|
103
|
+
arg_set = AllocationSet(
|
|
104
|
+
len(self._arg_sets[fn_name]), f"{self._name}.{fn_name}"
|
|
105
|
+
)
|
|
106
|
+
self._arg_sets[fn_name].append(arg_set)
|
|
107
|
+
return arg_set
|
|
108
|
+
|
|
109
|
+
def get_allocation_set(
|
|
110
|
+
self, fn: Union[Callable, str], idx: Optional[int] = None
|
|
111
|
+
) -> AllocationSet:
|
|
112
|
+
"""
|
|
113
|
+
Get an AllocationSet for the given function.
|
|
114
|
+
|
|
115
|
+
If idx is None, rotates to the next allocation set (for double-buffering).
|
|
116
|
+
If idx is specified, returns or creates the set at that index.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
fn: A callable or string identifier for the function.
|
|
120
|
+
idx: Optional specific index to retrieve.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
The requested AllocationSet.
|
|
124
|
+
"""
|
|
125
|
+
fn_name = self._make_name(fn)
|
|
126
|
+
if fn_name not in self._arg_sets:
|
|
127
|
+
self.add_allocation_set(fn_name)
|
|
128
|
+
if idx is None:
|
|
129
|
+
idx = self._active_set_ids[fn_name]
|
|
130
|
+
idx = (1 + idx) % len(self._arg_sets[fn_name])
|
|
131
|
+
self._active_set_ids[fn_name] = idx
|
|
132
|
+
else:
|
|
133
|
+
for i in range(len(self._arg_sets[fn_name]), idx + 1):
|
|
134
|
+
self.add_allocation_set(fn_name)
|
|
135
|
+
active_set = self._arg_sets[fn_name][idx]
|
|
136
|
+
return active_set
|
|
137
|
+
|
|
138
|
+
def get_active_id(self, fn: Union[Callable, str]) -> int:
|
|
139
|
+
"""
|
|
140
|
+
Get the currently active allocation set index for a function.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
fn: A callable or string identifier for the function.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
The index of the currently active AllocationSet.
|
|
147
|
+
"""
|
|
148
|
+
fn_name = self._make_name(fn)
|
|
149
|
+
return self._active_set_ids[fn_name]
|
|
150
|
+
|
|
151
|
+
def close(self) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Release all resources held by all allocation sets.
|
|
154
|
+
|
|
155
|
+
Closes all allocation sets for all functions managed by this manager.
|
|
156
|
+
After calling this method, the manager should not be used.
|
|
157
|
+
"""
|
|
158
|
+
for arg_set_group in self._arg_sets.values():
|
|
159
|
+
for arg_set in arg_set_group:
|
|
160
|
+
arg_set.close()
|
|
161
|
+
self._arg_sets = {}
|
|
162
|
+
|
|
163
|
+
def __del__(self) -> None:
|
|
164
|
+
"""Clean up resources when the manager is garbage collected."""
|
|
165
|
+
self.close()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
manager = AllocationSetManager("test")
|
|
170
|
+
manager.add_allocation_set("func1")
|
|
171
|
+
manager2 = AllocationSetManager("test")
|
|
172
|
+
assert manager is manager2
|
|
173
|
+
assert manager2.get_allocation_set("func1") != None
|
|
174
|
+
|
|
175
|
+
print("Test passed.")
|
|
176
|
+
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared memory allocation management for inter-process numpy array transfer.
|
|
3
|
+
|
|
4
|
+
This module provides classes for managing shared memory buffers used in RPC
|
|
5
|
+
communication. It handles the low-level details of creating, mapping, and
|
|
6
|
+
converting numpy arrays to/from shared memory segments.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from multiprocessing import shared_memory
|
|
13
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AllocationDescriptor:
|
|
21
|
+
"""
|
|
22
|
+
Descriptor for a shared memory allocation.
|
|
23
|
+
|
|
24
|
+
This lightweight class stores metadata about a shared memory buffer,
|
|
25
|
+
allowing it to be reconstructed in another process. It is serializable
|
|
26
|
+
and can be sent through pipes or sockets.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
name: The system-level name of the shared memory segment.
|
|
30
|
+
shape: The shape of the numpy array stored in the buffer.
|
|
31
|
+
dtype: The data type of the numpy array.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
__slots__ = ("name", "shape", "dtype")
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
name: str,
|
|
39
|
+
shape: Tuple[int, ...],
|
|
40
|
+
dtype: np.dtype,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Initialize an AllocationDescriptor.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
name: The system-level name of the shared memory segment.
|
|
47
|
+
shape: The shape of the numpy array.
|
|
48
|
+
dtype: The data type of the numpy array.
|
|
49
|
+
"""
|
|
50
|
+
self.name = name
|
|
51
|
+
self.shape = shape
|
|
52
|
+
self.dtype = dtype
|
|
53
|
+
|
|
54
|
+
def __repr__(self) -> str:
|
|
55
|
+
return f"AllocationDescriptor(name={self.name!r}, shape={self.shape}, dtype={self.dtype})"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AllocationSet:
|
|
59
|
+
"""
|
|
60
|
+
Manages a set of shared memory allocations for inter-process communication.
|
|
61
|
+
|
|
62
|
+
An AllocationSet represents a collection of shared memory buffers that can be
|
|
63
|
+
used to transfer numpy arrays between processes. It handles the lifecycle of
|
|
64
|
+
shared memory segments, including creation, mapping, and cleanup.
|
|
65
|
+
|
|
66
|
+
The class supports two types of memory ownership:
|
|
67
|
+
- Linked buffers: Created by this process and owned (will be unlinked on close)
|
|
68
|
+
- Mapped buffers: Created by another process, only mapped for access
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
MIN_SIZE: Minimum array size (in bytes) to use shared memory instead of pipes.
|
|
72
|
+
ix: Index identifier for this allocation set.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
>>> alloc_set = AllocationSet(0, "my_function")
|
|
76
|
+
>>> desc = alloc_set.alloc("buffer", (1024, 1024), np.float64)
|
|
77
|
+
>>> array = alloc_set.from_sharedmemory(desc)
|
|
78
|
+
>>> array[:] = 42.0
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
MIN_SIZE = 1024
|
|
82
|
+
|
|
83
|
+
def __init__(self, ix: int, name: Optional[str] = None) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Initialize an AllocationSet.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
ix: Index identifier for this allocation set.
|
|
89
|
+
name: Optional name for logging and debugging purposes.
|
|
90
|
+
"""
|
|
91
|
+
self.ix = ix
|
|
92
|
+
self._name = name
|
|
93
|
+
self._linked_buffers: List[shared_memory.SharedMemory] = []
|
|
94
|
+
self._mapped_buffers: List[shared_memory.SharedMemory] = []
|
|
95
|
+
self._arrays: Dict[str, np.ndarray] = {}
|
|
96
|
+
self._descriptors: Dict[str, AllocationDescriptor] = {}
|
|
97
|
+
|
|
98
|
+
def __repr__(self) -> str:
|
|
99
|
+
"""Return a string representation of the AllocationSet."""
|
|
100
|
+
return (
|
|
101
|
+
f"AllocationSet(ix={self.ix}, name={self._name!r}, "
|
|
102
|
+
f"buffers={len(self._linked_buffers)}, mapped={len(self._mapped_buffers)})"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def alloc(
|
|
106
|
+
self,
|
|
107
|
+
name: str,
|
|
108
|
+
shape: Tuple[int, ...],
|
|
109
|
+
dtype: np.dtype,
|
|
110
|
+
init: Optional[np.ndarray] = None,
|
|
111
|
+
) -> AllocationDescriptor:
|
|
112
|
+
"""
|
|
113
|
+
Allocate a new shared memory buffer.
|
|
114
|
+
|
|
115
|
+
Creates a new shared memory segment and returns a descriptor that can be
|
|
116
|
+
used to access the buffer from another process.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
name: Logical name for the allocation (used for lookup).
|
|
120
|
+
shape: Shape of the numpy array to allocate.
|
|
121
|
+
dtype: Data type of the numpy array.
|
|
122
|
+
init: Optional array to copy into the allocated buffer.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
An AllocationDescriptor containing the buffer metadata.
|
|
126
|
+
"""
|
|
127
|
+
logger.info(f"alloc {self._name}({self.ix}): {name}")
|
|
128
|
+
shm = shared_memory.SharedMemory(
|
|
129
|
+
create=True, size=int(np.prod(shape) * np.dtype(dtype).itemsize)
|
|
130
|
+
)
|
|
131
|
+
array = np.ndarray(shape, dtype=dtype, buffer=shm.buf)
|
|
132
|
+
if init is not None:
|
|
133
|
+
array[:] = init
|
|
134
|
+
self._linked_buffers.append(shm)
|
|
135
|
+
self._arrays[shm.name] = array
|
|
136
|
+
desc = AllocationDescriptor(shm.name, shape, dtype)
|
|
137
|
+
self._descriptors[name] = desc
|
|
138
|
+
return desc
|
|
139
|
+
|
|
140
|
+
def get(
|
|
141
|
+
self, name: str, init: Optional[np.ndarray] = None
|
|
142
|
+
) -> AllocationDescriptor:
|
|
143
|
+
"""
|
|
144
|
+
Get an existing allocation descriptor by logical name.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
name: Logical name of the allocation.
|
|
148
|
+
init: Optional array to copy into the buffer.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
The AllocationDescriptor for the named buffer.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
KeyError: If no allocation with the given name exists.
|
|
155
|
+
"""
|
|
156
|
+
desc = self._descriptors[name]
|
|
157
|
+
if init is not None:
|
|
158
|
+
array = self._arrays[desc.name]
|
|
159
|
+
array[:] = init
|
|
160
|
+
return desc
|
|
161
|
+
|
|
162
|
+
def is_mappable(self, arg: Any) -> bool:
|
|
163
|
+
"""
|
|
164
|
+
Check if an argument should be transferred via shared memory.
|
|
165
|
+
|
|
166
|
+
Arguments are mapped to shared memory if they are numpy arrays larger
|
|
167
|
+
than MIN_SIZE bytes. None values are also considered mappable if a
|
|
168
|
+
corresponding allocation already exists.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
arg: The argument to check.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
True if the argument should use shared memory, False otherwise.
|
|
175
|
+
"""
|
|
176
|
+
return arg is None or (
|
|
177
|
+
isinstance(arg, np.ndarray) and arg.nbytes > AllocationSet.MIN_SIZE
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def to_sharedmemory(
|
|
181
|
+
self, name: str, array: Optional[np.ndarray]
|
|
182
|
+
) -> Optional[AllocationDescriptor]:
|
|
183
|
+
"""
|
|
184
|
+
Convert a numpy array to a shared memory allocation.
|
|
185
|
+
|
|
186
|
+
If the array is already in shared memory (managed by this set), returns
|
|
187
|
+
its existing descriptor. Otherwise, either reuses an existing allocation
|
|
188
|
+
(if shape/dtype match) or creates a new one.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
name: Logical name for the allocation.
|
|
192
|
+
array: The numpy array to convert, or None.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
An AllocationDescriptor for the shared memory buffer, or None if
|
|
196
|
+
array is None and no existing allocation exists.
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
AssertionError: If array is not a numpy array (when not None).
|
|
200
|
+
AssertionError: If shape or dtype changed for an existing allocation.
|
|
201
|
+
"""
|
|
202
|
+
if array is None:
|
|
203
|
+
return self.get(name) if name in self._descriptors else None
|
|
204
|
+
assert isinstance(array, np.ndarray)
|
|
205
|
+
for id, existing in self._arrays.items():
|
|
206
|
+
if array is existing:
|
|
207
|
+
return AllocationDescriptor(id, array.shape, array.dtype)
|
|
208
|
+
if name in self._descriptors:
|
|
209
|
+
assert (
|
|
210
|
+
array.shape == self._descriptors[name].shape
|
|
211
|
+
and array.dtype == self._descriptors[name].dtype
|
|
212
|
+
), f"Unexpected shape or dtype for {name}. Changing argument shape or dtype between calls is not supported."
|
|
213
|
+
return self.get(name, array)
|
|
214
|
+
|
|
215
|
+
return self.alloc(name, array.shape, array.dtype, array)
|
|
216
|
+
|
|
217
|
+
def from_sharedmemory(self, descriptor: AllocationDescriptor) -> np.ndarray:
|
|
218
|
+
"""
|
|
219
|
+
Map a shared memory buffer to a numpy array.
|
|
220
|
+
|
|
221
|
+
If the buffer is already mapped in this set, returns the existing array.
|
|
222
|
+
Otherwise, maps the shared memory segment and creates a new numpy array
|
|
223
|
+
view of it.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
descriptor: The AllocationDescriptor identifying the buffer.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
A numpy array backed by the shared memory buffer.
|
|
230
|
+
|
|
231
|
+
Raises:
|
|
232
|
+
AssertionError: If descriptor is not an AllocationDescriptor.
|
|
233
|
+
"""
|
|
234
|
+
assert isinstance(descriptor, AllocationDescriptor)
|
|
235
|
+
if descriptor.name in self._arrays:
|
|
236
|
+
return self._arrays[descriptor.name]
|
|
237
|
+
logger.info(f"from_sharedmemory {self._name}({self.ix}): {descriptor.name}")
|
|
238
|
+
shm = shared_memory.SharedMemory(name=descriptor.name)
|
|
239
|
+
self._mapped_buffers.append(shm)
|
|
240
|
+
array = np.ndarray(descriptor.shape, dtype=descriptor.dtype, buffer=shm.buf)
|
|
241
|
+
self._arrays[descriptor.name] = array
|
|
242
|
+
return array
|
|
243
|
+
|
|
244
|
+
def args_to_sharedmemory(
|
|
245
|
+
self, *args: Any, **kwargs: Any
|
|
246
|
+
) -> Tuple[List[Any], Dict[str, Any]]:
|
|
247
|
+
"""
|
|
248
|
+
Convert function arguments to shared memory representations.
|
|
249
|
+
|
|
250
|
+
Large numpy arrays in the arguments are converted to AllocationDescriptors,
|
|
251
|
+
while other arguments are passed through unchanged.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
*args: Positional arguments to convert.
|
|
255
|
+
**kwargs: Keyword arguments to convert.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
A tuple of (positional_args, keyword_args) with arrays replaced by
|
|
259
|
+
their AllocationDescriptors.
|
|
260
|
+
"""
|
|
261
|
+
positional_args = [
|
|
262
|
+
self.to_sharedmemory(f"__arg{idx}", arg) if self.is_mappable(arg) else arg
|
|
263
|
+
for idx, arg in enumerate(args)
|
|
264
|
+
]
|
|
265
|
+
keyword_args = {
|
|
266
|
+
k: self.to_sharedmemory(k, v) if self.is_mappable(v) else v
|
|
267
|
+
for k, v in kwargs.items()
|
|
268
|
+
}
|
|
269
|
+
return positional_args, keyword_args
|
|
270
|
+
|
|
271
|
+
def args_from_sharedmemory(
|
|
272
|
+
self, *args: Any, **kwargs: Any
|
|
273
|
+
) -> Tuple[List[Any], Dict[str, Any]]:
|
|
274
|
+
"""
|
|
275
|
+
Convert function arguments from shared memory representations.
|
|
276
|
+
|
|
277
|
+
AllocationDescriptors in the arguments are mapped to numpy arrays,
|
|
278
|
+
while other arguments are passed through unchanged.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
*args: Positional arguments to convert.
|
|
282
|
+
**kwargs: Keyword arguments to convert.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
A tuple of (positional_args, keyword_args) with descriptors replaced
|
|
286
|
+
by their numpy array views.
|
|
287
|
+
"""
|
|
288
|
+
positional_args = [
|
|
289
|
+
self.from_sharedmemory(arg) if isinstance(arg, AllocationDescriptor) else arg
|
|
290
|
+
for arg in args
|
|
291
|
+
]
|
|
292
|
+
keyword_args = {
|
|
293
|
+
k: self.from_sharedmemory(v) if isinstance(v, AllocationDescriptor) else v
|
|
294
|
+
for k, v in kwargs.items()
|
|
295
|
+
}
|
|
296
|
+
return positional_args, keyword_args
|
|
297
|
+
|
|
298
|
+
def retval_to_sharedmemory(self, retval: Any) -> Any:
|
|
299
|
+
"""
|
|
300
|
+
Convert a function's return value to shared memory representation.
|
|
301
|
+
|
|
302
|
+
Supports return values that are tuples, lists, dicts, or single values.
|
|
303
|
+
Large numpy arrays are converted to AllocationDescriptors.
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
retval: The return value to convert.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
The return value with large arrays replaced by AllocationDescriptors.
|
|
310
|
+
"""
|
|
311
|
+
if isinstance(retval, tuple):
|
|
312
|
+
return tuple(
|
|
313
|
+
self.to_sharedmemory(f"__retval{idx}", v) if self.is_mappable(v) else v
|
|
314
|
+
for idx, v in enumerate(retval)
|
|
315
|
+
)
|
|
316
|
+
if isinstance(retval, list):
|
|
317
|
+
return [
|
|
318
|
+
self.to_sharedmemory(f"__retval{idx}", v) if self.is_mappable(v) else v
|
|
319
|
+
for idx, v in enumerate(retval)
|
|
320
|
+
]
|
|
321
|
+
if isinstance(retval, dict):
|
|
322
|
+
return {
|
|
323
|
+
k: self.to_sharedmemory(k, v) if self.is_mappable(v) else v
|
|
324
|
+
for k, v in retval.items()
|
|
325
|
+
}
|
|
326
|
+
return (
|
|
327
|
+
self.to_sharedmemory("__retval0", retval)
|
|
328
|
+
if self.is_mappable(retval)
|
|
329
|
+
else retval
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
def retval_from_sharedmemory(self, retval: Any) -> Any:
|
|
333
|
+
"""
|
|
334
|
+
Convert a function's return value from shared memory representation.
|
|
335
|
+
|
|
336
|
+
Supports return values that are tuples, lists, dicts, or single values.
|
|
337
|
+
AllocationDescriptors are mapped to numpy arrays.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
retval: The return value to convert.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
The return value with descriptors replaced by numpy array views.
|
|
344
|
+
"""
|
|
345
|
+
if isinstance(retval, tuple):
|
|
346
|
+
return tuple(
|
|
347
|
+
self.from_sharedmemory(v) if isinstance(v, AllocationDescriptor) else v
|
|
348
|
+
for v in retval
|
|
349
|
+
)
|
|
350
|
+
if isinstance(retval, list):
|
|
351
|
+
return [
|
|
352
|
+
self.from_sharedmemory(v) if isinstance(v, AllocationDescriptor) else v
|
|
353
|
+
for v in retval
|
|
354
|
+
]
|
|
355
|
+
if isinstance(retval, dict):
|
|
356
|
+
return {
|
|
357
|
+
k: self.from_sharedmemory(v) if isinstance(v, AllocationDescriptor) else v
|
|
358
|
+
for k, v in retval.items()
|
|
359
|
+
}
|
|
360
|
+
return (
|
|
361
|
+
self.from_sharedmemory(retval)
|
|
362
|
+
if isinstance(retval, AllocationDescriptor)
|
|
363
|
+
else retval
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def close(self) -> None:
|
|
367
|
+
"""
|
|
368
|
+
Release all shared memory resources.
|
|
369
|
+
|
|
370
|
+
Closes and unlinks all linked buffers (owned by this process), and
|
|
371
|
+
closes all mapped buffers (owned by other processes). After calling
|
|
372
|
+
this method, the AllocationSet should not be used.
|
|
373
|
+
"""
|
|
374
|
+
for buf in self._linked_buffers:
|
|
375
|
+
buf.close()
|
|
376
|
+
buf.unlink()
|
|
377
|
+
for buf in self._mapped_buffers:
|
|
378
|
+
buf.close()
|
|
379
|
+
self._linked_buffers = []
|
|
380
|
+
self._mapped_buffers = []
|
|
381
|
+
self._arrays = {}
|