py-sharedmemory 1.0.0__tar.gz → 2.0.0__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.
- {py_sharedmemory-1.0.0 → py_sharedmemory-2.0.0}/LICENSE +1 -1
- py_sharedmemory-2.0.0/PKG-INFO +243 -0
- py_sharedmemory-2.0.0/README.md +226 -0
- py_sharedmemory-2.0.0/memory/__init__.py +3 -0
- py_sharedmemory-2.0.0/memory/convert.py +53 -0
- py_sharedmemory-2.0.0/memory/factory.py +13 -0
- py_sharedmemory-2.0.0/memory/receiver.py +23 -0
- py_sharedmemory-2.0.0/memory/sender.py +111 -0
- py_sharedmemory-2.0.0/py_sharedmemory.egg-info/PKG-INFO +243 -0
- {py_sharedmemory-1.0.0 → py_sharedmemory-2.0.0}/py_sharedmemory.egg-info/SOURCES.txt +2 -3
- {py_sharedmemory-1.0.0 → py_sharedmemory-2.0.0}/pyproject.toml +4 -5
- py_sharedmemory-1.0.0/PKG-INFO +0 -179
- py_sharedmemory-1.0.0/README.md +0 -141
- py_sharedmemory-1.0.0/memory/__init__.py +0 -1
- py_sharedmemory-1.0.0/memory/converter.py +0 -106
- py_sharedmemory-1.0.0/memory/factory.py +0 -12
- py_sharedmemory-1.0.0/memory/receiver.py +0 -75
- py_sharedmemory-1.0.0/memory/sender.py +0 -149
- py_sharedmemory-1.0.0/py_sharedmemory.egg-info/PKG-INFO +0 -179
- py_sharedmemory-1.0.0/tests/test_shared_memory.py +0 -339
- {py_sharedmemory-1.0.0 → py_sharedmemory-2.0.0}/py_sharedmemory.egg-info/dependency_links.txt +0 -0
- {py_sharedmemory-1.0.0 → py_sharedmemory-2.0.0}/py_sharedmemory.egg-info/requires.txt +0 -0
- {py_sharedmemory-1.0.0 → py_sharedmemory-2.0.0}/py_sharedmemory.egg-info/top_level.txt +0 -0
- {py_sharedmemory-1.0.0 → py_sharedmemory-2.0.0}/setup.cfg +0 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-sharedmemory
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: Optimized Python IPC: Uses shared memory to bypass multiprocessing queue I/O bottlenecks, ideal for large data (1MB+) in scientific computing, RL, etc. Reduces system load and improves latency consistency
|
|
5
|
+
Author: Tobias Würth
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/tobiaswuerth/python_shared_memory_queue
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/tobiaswuerth/python_shared_memory_queue/issues
|
|
9
|
+
Keywords: python,performance,queue,multiprocessing,numpy,zero-copy,data-transfer,low-latency,inter-process-communication,shared-memory,high-throughput,parallel-processing,large-data,memoryview
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: numpy>=1.26
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# py-sharedmemory: High-Performance Multiprocessing Queue Alternative for Large Data
|
|
19
|
+
|
|
20
|
+
[](https://badge.fury.io/py/py-sharedmemory)
|
|
21
|
+
|
|
22
|
+
Install using:
|
|
23
|
+
```
|
|
24
|
+
pip install py-sharedmemory
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Python's standard `multiprocessing.Queue` pickles data for inter-process communication. This often creates performance bottlenecks in demanding applications like Deep Reinforcement Learning (DeepRL) training, scientific computing, or distributed systems that handle large data objects (e.g., NumPy arrays, tensors >1MB) or involve high I/O volumes between many processes (like actors, replay buffers, and trainers).
|
|
28
|
+
|
|
29
|
+
`py-sharedmemory` offers a high-performance replacement. It bypasses pickling by converting data into bytes and moving them into shared memory. Only minimal metadata is sent via the standard queue mechanism. This significantly reduces serialization overhead and inter-process I/O, drastically improving data transfer throughput for data-intensive Python multiprocessing tasks. If your application struggles with slow `multiprocessing.Queue` performance due to large object transfers, `py-sharedmemory` provides a faster alternative.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
# Usage example
|
|
34
|
+
```python
|
|
35
|
+
import multiprocessing as mp
|
|
36
|
+
|
|
37
|
+
from memory import create_shared_memory_pair, SharedMemorySender, SharedMemoryReceiver
|
|
38
|
+
|
|
39
|
+
def producer_sm(sender:SharedMemorySender):
|
|
40
|
+
your_data = "your data"
|
|
41
|
+
|
|
42
|
+
sender.put(your_data) # blocks until there is space
|
|
43
|
+
# ...
|
|
44
|
+
|
|
45
|
+
# or
|
|
46
|
+
if sender.has_space():
|
|
47
|
+
sender.put(your_data)
|
|
48
|
+
# ...
|
|
49
|
+
|
|
50
|
+
sender.wait_for_all_ack() # wait for all data to be received before closing the sender
|
|
51
|
+
# ...
|
|
52
|
+
|
|
53
|
+
def consumer_sm(receiver:SharedMemoryReceiver, loops, size):
|
|
54
|
+
data = receiver.get() # blocks
|
|
55
|
+
# ...
|
|
56
|
+
|
|
57
|
+
# or
|
|
58
|
+
data = receiver.get(timeout=3) # raises queue.Empty exception after 3s
|
|
59
|
+
# ...
|
|
60
|
+
|
|
61
|
+
# or
|
|
62
|
+
data = receiver.get_nowait()
|
|
63
|
+
if data is None:
|
|
64
|
+
# handle empty case
|
|
65
|
+
# ...
|
|
66
|
+
|
|
67
|
+
if __name__ == '__main__':
|
|
68
|
+
sender, receiver = create_shared_memory_pair(capacity=5)
|
|
69
|
+
|
|
70
|
+
mp.Process(target=producer_sm, args=(sender,)).start()
|
|
71
|
+
mp.Process(target=consumer_sm, args=(receiver,)).start()
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
# Supported Types
|
|
77
|
+
|
|
78
|
+
The following data types are currently supported:
|
|
79
|
+
```
|
|
80
|
+
- bytes
|
|
81
|
+
- int
|
|
82
|
+
- float
|
|
83
|
+
- bool
|
|
84
|
+
- str
|
|
85
|
+
- np.ndarray
|
|
86
|
+
- np.dtype
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
As well as any nested structures of the following types:
|
|
90
|
+
```
|
|
91
|
+
- tuple/NamedTuple
|
|
92
|
+
- list
|
|
93
|
+
- dict
|
|
94
|
+
- set
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
# Explanation
|
|
100
|
+
|
|
101
|
+
Let's assume I have the following data:
|
|
102
|
+
```python
|
|
103
|
+
my_data = {
|
|
104
|
+
"idx": 12345,
|
|
105
|
+
"source": "actor_3",
|
|
106
|
+
"data_batch": np.zeros((64, 80, 4, 84, 84, 3), dtype=np.float32)
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
This object uses about ~1.7GB of RAM. Considering a bigger application with multiple processes generating, storing, providing and processing data, the total amount of inter-process I/O is quickly in the hundreds of GB/s. This is demanding on the system.
|
|
110
|
+
|
|
111
|
+
Using `py-sharedmemory` the data is instead moved into the system shared memory, which is accessible from other processes using a key. The information transferred via standard multiprocessing queues is reduced to this meta-data used for accessing and reconstructing the object:
|
|
112
|
+
```python
|
|
113
|
+
(
|
|
114
|
+
'wnsm_1adcf1aa', # shared memory key
|
|
115
|
+
'd', 1734082569, # total byte size
|
|
116
|
+
<class 'dict'>, [ # data structure information
|
|
117
|
+
('i', 2, False),
|
|
118
|
+
('s', 7),
|
|
119
|
+
('n', 1734082560, dtype('float32'), (64, 80, 4, 84, 84, 3))
|
|
120
|
+
], ['idx', 'source', 'data_batch']
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
<details>
|
|
125
|
+
|
|
126
|
+
<summary>Large example</summary>
|
|
127
|
+
|
|
128
|
+
Assuming this is your data object:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from typing import NamedTuple
|
|
132
|
+
class TestNamedTuple(NamedTuple):
|
|
133
|
+
name: str
|
|
134
|
+
value: int
|
|
135
|
+
|
|
136
|
+
extensive_test_data = {
|
|
137
|
+
"test_id": 98765,
|
|
138
|
+
"description": "This is a comprehensive test data object.",
|
|
139
|
+
"is_active": True,
|
|
140
|
+
"parameters": {
|
|
141
|
+
"learning_rate": 0.001,
|
|
142
|
+
"batch_size": 128,
|
|
143
|
+
"optimizer": "adam",
|
|
144
|
+
"epsilon": 1e-8,
|
|
145
|
+
"byte_config": b'\x01\x02\x03\x04'
|
|
146
|
+
},
|
|
147
|
+
"data_samples": [
|
|
148
|
+
{
|
|
149
|
+
"sample_idx": 0,
|
|
150
|
+
"image_data": np.random.rand(32, 32, 3).astype(np.float32),
|
|
151
|
+
"label": 10,
|
|
152
|
+
"confidence": 0.95,
|
|
153
|
+
"flags": [True, False, True],
|
|
154
|
+
"metadata": ("image", "png", 32, 32)
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"sample_idx": 1,
|
|
158
|
+
"image_data": np.zeros((16, 16, 1), dtype=np.uint8),
|
|
159
|
+
"label": 5,
|
|
160
|
+
"confidence": 0.88,
|
|
161
|
+
"flags": [False, False, True],
|
|
162
|
+
"metadata": ("image", "jpeg", 16, 16)
|
|
163
|
+
}
|
|
164
|
+
],
|
|
165
|
+
"settings_list": [
|
|
166
|
+
100,
|
|
167
|
+
"high",
|
|
168
|
+
False,
|
|
169
|
+
0.5,
|
|
170
|
+
b'setting_bytes',
|
|
171
|
+
(1, 'a', True),
|
|
172
|
+
{'nested_list_dict': [1, 2, {'deep_key': 'deep_value'}]},
|
|
173
|
+
np.array([1.1, 2.2, 3.3])
|
|
174
|
+
],
|
|
175
|
+
"unique_identifiers": {101, 202, 303, 101},
|
|
176
|
+
"status_codes": set([b'success', b'pending', b'failed']),
|
|
177
|
+
"data_types_used": [
|
|
178
|
+
np.dtype('float32'),
|
|
179
|
+
np.dtype('int64'),
|
|
180
|
+
np.dtype('uint8'),
|
|
181
|
+
np.dtype('bool_')
|
|
182
|
+
],
|
|
183
|
+
"complex_nested_structure": {
|
|
184
|
+
"level1_dict": {
|
|
185
|
+
"level2_list": [
|
|
186
|
+
"string_in_list",
|
|
187
|
+
12345,
|
|
188
|
+
True,
|
|
189
|
+
(10, 20, 30),
|
|
190
|
+
{"level3_dict": {"array_in_dict": np.arange(5)}},
|
|
191
|
+
TestNamedTuple(name='example', value=99)
|
|
192
|
+
],
|
|
193
|
+
"level2_set": {9, 8, 7, "set_string"},
|
|
194
|
+
"level2_tuple": (
|
|
195
|
+
"tuple_item_1",
|
|
196
|
+
56.78,
|
|
197
|
+
b'tuple_bytes',
|
|
198
|
+
[1, 2, 3],
|
|
199
|
+
{"nested_in_tuple": "hello"}
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
"empty_structures": {
|
|
204
|
+
"empty_list": [],
|
|
205
|
+
"empty_dict": {},
|
|
206
|
+
"empty_set": set(),
|
|
207
|
+
"empty_tuple": ()
|
|
208
|
+
},
|
|
209
|
+
"large_array_example": np.random.rand(64, 80, 4, 84, 84, 3).astype(np.float32)
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Then the meta-data produced for this object is this:
|
|
214
|
+
|
|
215
|
+
`('wnsm_eb8850c8', 'd', 1734095454, <class 'dict'>, [('i', 3, False), ('s', 41), ('b', 1), ('d', 26, <class 'dict'>, [('f', 8), ('i', 2, False), ('s', 4), ('f', 8), ('r', 4)], ['learning_rate', 'batch_size', 'optimizer', 'epsilon', 'byte_config']), ('l', 12591, <class 'list'>, [('d', 12311, <class 'dict'>, [('i', 1, False), ('n', 12288, dtype('float32'), (32, 32, 3)), ('i', 1, False), ('f', 8), ('l', 3, <class 'list'>, [('b', 1), ('b', 1), ('b', 1)]), ('p', 10, <class 'tuple'>, [('s', 5), ('s', 3), ('i', 1, False), ('i', 1, False)])], ['sample_idx', 'image_data', 'label', 'confidence', 'flags', 'metadata']), ('d', 280, <class 'dict'>, [('i', 1, False), ('n', 256, dtype('uint8'), (16, 16, 1)), ('i', 1, False), ('f', 8), ('l', 3, <class 'list'>, [('b', 1), ('b', 1), ('b', 1)]), ('p', 11, <class 'tuple'>, [('s', 5), ('s', 4), ('i', 1, False), ('i', 1, False)])], ['sample_idx', 'image_data', 'label', 'confidence', 'flags', 'metadata'])]), ('l', 66, <class 'list'>, [('i', 1, False), ('s', 4), ('b', 1), ('f', 8), ('r', 13), ('p', 3, <class 'tuple'>, [('i', 1, False), ('s', 1), ('b', 1)]), ('d', 12, <class 'dict'>, [('l', 12, <class 'list'>, [('i', 1, False), ('i', 1, False), ('d', 10, <class 'dict'>, [('s', 10)], ['deep_key'])])], ['nested_list_dict']), ('n', 24, dtype('float64'), (3,))]), ('u', 5, <class 'set'>, [('i', 2, False), ('i', 1, False), ('i', 2, False)]), ('u', 20, <class 'set'>, [('r', 6), ('r', 7), ('r', 7)]), ('l', 21, <class 'list'>, [('t', 7), ('t', 5), ('t', 5), ('t', 4)]), ('d', 120, <class 'dict'>, [('d', 120, <class 'dict'>, [('l', 68, <class 'list'>, [('s', 14), ('i', 2, False), ('b', 1), ('p', 3, <class 'tuple'>, [('i', 1, False), ('i', 1, False), ('i', 1, False)]), ('d', 40, <class 'dict'>, [('d', 40, <class 'dict'>, [('n', 40, dtype('int64'), (5,))], ['array_in_dict'])], ['level3_dict']), ('p', 8, <class '__main__.TestNamedTuple'>, [('s', 7), ('i', 1, False)])]), ('u', 13, <class 'set'>, [('i', 1, False), ('s', 10), ('i', 1, False), ('i', 1, False)]), ('p', 39, <class 'tuple'>, [('s', 12), ('f', 8), ('r', 11), ('l', 3, <class 'list'>, [('i', 1, False), ('i', 1, False), ('i', 1, False)]), ('d', 5, <class 'dict'>, [('s', 5)], ['nested_in_tuple'])])], ['level2_list', 'level2_set', 'level2_tuple'])], ['level1_dict']), ('d', 0, <class 'dict'>, [('l', 0, <class 'list'>, []), ('d', 0, <class 'dict'>, [], []), ('u', 0, <class 'set'>, []), ('p', 0, <class 'tuple'>, [])], ['empty_list', 'empty_dict', 'empty_set', 'empty_tuple']), ('n', 1734082560, dtype('float32'), (64, 80, 4, 84, 84, 3))], ['test_id', 'description', 'is_active', 'parameters', 'data_samples', 'settings_list', 'unique_identifiers', 'status_codes', 'data_types_used', 'complex_nested_structure', 'empty_structures', 'large_array_example'])`
|
|
216
|
+
|
|
217
|
+
</details>
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
# Considerations
|
|
222
|
+
There is a certain overhead to the conversion process which is especially noticable for smaller objects and more complex data structures.
|
|
223
|
+
Use the following heuristic depending on the size of the data you are handling:
|
|
224
|
+
|
|
225
|
+
||10B|100B|1KB|10KB|100KB|1MB|10MB|100MB|1GB|10GB|
|
|
226
|
+
|---|---|---|---|---|---|---|---|---|---|---|
|
|
227
|
+
|``mp.Queue()``|✅|✅|✅|✅|✅|❌|❌|❌|❌|❌|
|
|
228
|
+
|``py-sharedmemory``|❌|❌|❌|❌|❌|✅|✅|✅|✅|✅|
|
|
229
|
+
|
|
230
|
+
Actual performance heavily relies on your memory speed.
|
|
231
|
+
|
|
232
|
+
# Performance Testing
|
|
233
|
+
|
|
234
|
+
I measured the time it takes to send/receive data comparing the standard queue and this shared memory implementation:
|
|
235
|
+
|
|
236
|
+
> Note: The standard queue implementation uses a sender thread, that is why the dark blue bar for 'standard send' apparently has more throughput, since it took less time for each cycle, but the actual time until the data is transfered is longer behind the scenes, making this comparison only somewhat useful.
|
|
237
|
+
|
|
238
|
+

|
|
239
|
+
|
|
240
|
+
What we can see here is that starting at around 1MB sized packages, my `py-sharedmemory` implementation starts to outperform the standard implementation.
|
|
241
|
+
Furthermore, and this is most importantly, no I/O is produced. Also, the standard implementation crashes for 10GB packages.
|
|
242
|
+
|
|
243
|
+
Generally I noticed that my system runs much more smoothly and stable when using `py-sharedmemory` with consistent put/get durations and no application slowdowns, when comparing to the standard implementation.
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# py-sharedmemory: High-Performance Multiprocessing Queue Alternative for Large Data
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/py/py-sharedmemory)
|
|
4
|
+
|
|
5
|
+
Install using:
|
|
6
|
+
```
|
|
7
|
+
pip install py-sharedmemory
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Python's standard `multiprocessing.Queue` pickles data for inter-process communication. This often creates performance bottlenecks in demanding applications like Deep Reinforcement Learning (DeepRL) training, scientific computing, or distributed systems that handle large data objects (e.g., NumPy arrays, tensors >1MB) or involve high I/O volumes between many processes (like actors, replay buffers, and trainers).
|
|
11
|
+
|
|
12
|
+
`py-sharedmemory` offers a high-performance replacement. It bypasses pickling by converting data into bytes and moving them into shared memory. Only minimal metadata is sent via the standard queue mechanism. This significantly reduces serialization overhead and inter-process I/O, drastically improving data transfer throughput for data-intensive Python multiprocessing tasks. If your application struggles with slow `multiprocessing.Queue` performance due to large object transfers, `py-sharedmemory` provides a faster alternative.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Usage example
|
|
17
|
+
```python
|
|
18
|
+
import multiprocessing as mp
|
|
19
|
+
|
|
20
|
+
from memory import create_shared_memory_pair, SharedMemorySender, SharedMemoryReceiver
|
|
21
|
+
|
|
22
|
+
def producer_sm(sender:SharedMemorySender):
|
|
23
|
+
your_data = "your data"
|
|
24
|
+
|
|
25
|
+
sender.put(your_data) # blocks until there is space
|
|
26
|
+
# ...
|
|
27
|
+
|
|
28
|
+
# or
|
|
29
|
+
if sender.has_space():
|
|
30
|
+
sender.put(your_data)
|
|
31
|
+
# ...
|
|
32
|
+
|
|
33
|
+
sender.wait_for_all_ack() # wait for all data to be received before closing the sender
|
|
34
|
+
# ...
|
|
35
|
+
|
|
36
|
+
def consumer_sm(receiver:SharedMemoryReceiver, loops, size):
|
|
37
|
+
data = receiver.get() # blocks
|
|
38
|
+
# ...
|
|
39
|
+
|
|
40
|
+
# or
|
|
41
|
+
data = receiver.get(timeout=3) # raises queue.Empty exception after 3s
|
|
42
|
+
# ...
|
|
43
|
+
|
|
44
|
+
# or
|
|
45
|
+
data = receiver.get_nowait()
|
|
46
|
+
if data is None:
|
|
47
|
+
# handle empty case
|
|
48
|
+
# ...
|
|
49
|
+
|
|
50
|
+
if __name__ == '__main__':
|
|
51
|
+
sender, receiver = create_shared_memory_pair(capacity=5)
|
|
52
|
+
|
|
53
|
+
mp.Process(target=producer_sm, args=(sender,)).start()
|
|
54
|
+
mp.Process(target=consumer_sm, args=(receiver,)).start()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
# Supported Types
|
|
60
|
+
|
|
61
|
+
The following data types are currently supported:
|
|
62
|
+
```
|
|
63
|
+
- bytes
|
|
64
|
+
- int
|
|
65
|
+
- float
|
|
66
|
+
- bool
|
|
67
|
+
- str
|
|
68
|
+
- np.ndarray
|
|
69
|
+
- np.dtype
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
As well as any nested structures of the following types:
|
|
73
|
+
```
|
|
74
|
+
- tuple/NamedTuple
|
|
75
|
+
- list
|
|
76
|
+
- dict
|
|
77
|
+
- set
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
# Explanation
|
|
83
|
+
|
|
84
|
+
Let's assume I have the following data:
|
|
85
|
+
```python
|
|
86
|
+
my_data = {
|
|
87
|
+
"idx": 12345,
|
|
88
|
+
"source": "actor_3",
|
|
89
|
+
"data_batch": np.zeros((64, 80, 4, 84, 84, 3), dtype=np.float32)
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
This object uses about ~1.7GB of RAM. Considering a bigger application with multiple processes generating, storing, providing and processing data, the total amount of inter-process I/O is quickly in the hundreds of GB/s. This is demanding on the system.
|
|
93
|
+
|
|
94
|
+
Using `py-sharedmemory` the data is instead moved into the system shared memory, which is accessible from other processes using a key. The information transferred via standard multiprocessing queues is reduced to this meta-data used for accessing and reconstructing the object:
|
|
95
|
+
```python
|
|
96
|
+
(
|
|
97
|
+
'wnsm_1adcf1aa', # shared memory key
|
|
98
|
+
'd', 1734082569, # total byte size
|
|
99
|
+
<class 'dict'>, [ # data structure information
|
|
100
|
+
('i', 2, False),
|
|
101
|
+
('s', 7),
|
|
102
|
+
('n', 1734082560, dtype('float32'), (64, 80, 4, 84, 84, 3))
|
|
103
|
+
], ['idx', 'source', 'data_batch']
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
<details>
|
|
108
|
+
|
|
109
|
+
<summary>Large example</summary>
|
|
110
|
+
|
|
111
|
+
Assuming this is your data object:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from typing import NamedTuple
|
|
115
|
+
class TestNamedTuple(NamedTuple):
|
|
116
|
+
name: str
|
|
117
|
+
value: int
|
|
118
|
+
|
|
119
|
+
extensive_test_data = {
|
|
120
|
+
"test_id": 98765,
|
|
121
|
+
"description": "This is a comprehensive test data object.",
|
|
122
|
+
"is_active": True,
|
|
123
|
+
"parameters": {
|
|
124
|
+
"learning_rate": 0.001,
|
|
125
|
+
"batch_size": 128,
|
|
126
|
+
"optimizer": "adam",
|
|
127
|
+
"epsilon": 1e-8,
|
|
128
|
+
"byte_config": b'\x01\x02\x03\x04'
|
|
129
|
+
},
|
|
130
|
+
"data_samples": [
|
|
131
|
+
{
|
|
132
|
+
"sample_idx": 0,
|
|
133
|
+
"image_data": np.random.rand(32, 32, 3).astype(np.float32),
|
|
134
|
+
"label": 10,
|
|
135
|
+
"confidence": 0.95,
|
|
136
|
+
"flags": [True, False, True],
|
|
137
|
+
"metadata": ("image", "png", 32, 32)
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"sample_idx": 1,
|
|
141
|
+
"image_data": np.zeros((16, 16, 1), dtype=np.uint8),
|
|
142
|
+
"label": 5,
|
|
143
|
+
"confidence": 0.88,
|
|
144
|
+
"flags": [False, False, True],
|
|
145
|
+
"metadata": ("image", "jpeg", 16, 16)
|
|
146
|
+
}
|
|
147
|
+
],
|
|
148
|
+
"settings_list": [
|
|
149
|
+
100,
|
|
150
|
+
"high",
|
|
151
|
+
False,
|
|
152
|
+
0.5,
|
|
153
|
+
b'setting_bytes',
|
|
154
|
+
(1, 'a', True),
|
|
155
|
+
{'nested_list_dict': [1, 2, {'deep_key': 'deep_value'}]},
|
|
156
|
+
np.array([1.1, 2.2, 3.3])
|
|
157
|
+
],
|
|
158
|
+
"unique_identifiers": {101, 202, 303, 101},
|
|
159
|
+
"status_codes": set([b'success', b'pending', b'failed']),
|
|
160
|
+
"data_types_used": [
|
|
161
|
+
np.dtype('float32'),
|
|
162
|
+
np.dtype('int64'),
|
|
163
|
+
np.dtype('uint8'),
|
|
164
|
+
np.dtype('bool_')
|
|
165
|
+
],
|
|
166
|
+
"complex_nested_structure": {
|
|
167
|
+
"level1_dict": {
|
|
168
|
+
"level2_list": [
|
|
169
|
+
"string_in_list",
|
|
170
|
+
12345,
|
|
171
|
+
True,
|
|
172
|
+
(10, 20, 30),
|
|
173
|
+
{"level3_dict": {"array_in_dict": np.arange(5)}},
|
|
174
|
+
TestNamedTuple(name='example', value=99)
|
|
175
|
+
],
|
|
176
|
+
"level2_set": {9, 8, 7, "set_string"},
|
|
177
|
+
"level2_tuple": (
|
|
178
|
+
"tuple_item_1",
|
|
179
|
+
56.78,
|
|
180
|
+
b'tuple_bytes',
|
|
181
|
+
[1, 2, 3],
|
|
182
|
+
{"nested_in_tuple": "hello"}
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
"empty_structures": {
|
|
187
|
+
"empty_list": [],
|
|
188
|
+
"empty_dict": {},
|
|
189
|
+
"empty_set": set(),
|
|
190
|
+
"empty_tuple": ()
|
|
191
|
+
},
|
|
192
|
+
"large_array_example": np.random.rand(64, 80, 4, 84, 84, 3).astype(np.float32)
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Then the meta-data produced for this object is this:
|
|
197
|
+
|
|
198
|
+
`('wnsm_eb8850c8', 'd', 1734095454, <class 'dict'>, [('i', 3, False), ('s', 41), ('b', 1), ('d', 26, <class 'dict'>, [('f', 8), ('i', 2, False), ('s', 4), ('f', 8), ('r', 4)], ['learning_rate', 'batch_size', 'optimizer', 'epsilon', 'byte_config']), ('l', 12591, <class 'list'>, [('d', 12311, <class 'dict'>, [('i', 1, False), ('n', 12288, dtype('float32'), (32, 32, 3)), ('i', 1, False), ('f', 8), ('l', 3, <class 'list'>, [('b', 1), ('b', 1), ('b', 1)]), ('p', 10, <class 'tuple'>, [('s', 5), ('s', 3), ('i', 1, False), ('i', 1, False)])], ['sample_idx', 'image_data', 'label', 'confidence', 'flags', 'metadata']), ('d', 280, <class 'dict'>, [('i', 1, False), ('n', 256, dtype('uint8'), (16, 16, 1)), ('i', 1, False), ('f', 8), ('l', 3, <class 'list'>, [('b', 1), ('b', 1), ('b', 1)]), ('p', 11, <class 'tuple'>, [('s', 5), ('s', 4), ('i', 1, False), ('i', 1, False)])], ['sample_idx', 'image_data', 'label', 'confidence', 'flags', 'metadata'])]), ('l', 66, <class 'list'>, [('i', 1, False), ('s', 4), ('b', 1), ('f', 8), ('r', 13), ('p', 3, <class 'tuple'>, [('i', 1, False), ('s', 1), ('b', 1)]), ('d', 12, <class 'dict'>, [('l', 12, <class 'list'>, [('i', 1, False), ('i', 1, False), ('d', 10, <class 'dict'>, [('s', 10)], ['deep_key'])])], ['nested_list_dict']), ('n', 24, dtype('float64'), (3,))]), ('u', 5, <class 'set'>, [('i', 2, False), ('i', 1, False), ('i', 2, False)]), ('u', 20, <class 'set'>, [('r', 6), ('r', 7), ('r', 7)]), ('l', 21, <class 'list'>, [('t', 7), ('t', 5), ('t', 5), ('t', 4)]), ('d', 120, <class 'dict'>, [('d', 120, <class 'dict'>, [('l', 68, <class 'list'>, [('s', 14), ('i', 2, False), ('b', 1), ('p', 3, <class 'tuple'>, [('i', 1, False), ('i', 1, False), ('i', 1, False)]), ('d', 40, <class 'dict'>, [('d', 40, <class 'dict'>, [('n', 40, dtype('int64'), (5,))], ['array_in_dict'])], ['level3_dict']), ('p', 8, <class '__main__.TestNamedTuple'>, [('s', 7), ('i', 1, False)])]), ('u', 13, <class 'set'>, [('i', 1, False), ('s', 10), ('i', 1, False), ('i', 1, False)]), ('p', 39, <class 'tuple'>, [('s', 12), ('f', 8), ('r', 11), ('l', 3, <class 'list'>, [('i', 1, False), ('i', 1, False), ('i', 1, False)]), ('d', 5, <class 'dict'>, [('s', 5)], ['nested_in_tuple'])])], ['level2_list', 'level2_set', 'level2_tuple'])], ['level1_dict']), ('d', 0, <class 'dict'>, [('l', 0, <class 'list'>, []), ('d', 0, <class 'dict'>, [], []), ('u', 0, <class 'set'>, []), ('p', 0, <class 'tuple'>, [])], ['empty_list', 'empty_dict', 'empty_set', 'empty_tuple']), ('n', 1734082560, dtype('float32'), (64, 80, 4, 84, 84, 3))], ['test_id', 'description', 'is_active', 'parameters', 'data_samples', 'settings_list', 'unique_identifiers', 'status_codes', 'data_types_used', 'complex_nested_structure', 'empty_structures', 'large_array_example'])`
|
|
199
|
+
|
|
200
|
+
</details>
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
# Considerations
|
|
205
|
+
There is a certain overhead to the conversion process which is especially noticable for smaller objects and more complex data structures.
|
|
206
|
+
Use the following heuristic depending on the size of the data you are handling:
|
|
207
|
+
|
|
208
|
+
||10B|100B|1KB|10KB|100KB|1MB|10MB|100MB|1GB|10GB|
|
|
209
|
+
|---|---|---|---|---|---|---|---|---|---|---|
|
|
210
|
+
|``mp.Queue()``|✅|✅|✅|✅|✅|❌|❌|❌|❌|❌|
|
|
211
|
+
|``py-sharedmemory``|❌|❌|❌|❌|❌|✅|✅|✅|✅|✅|
|
|
212
|
+
|
|
213
|
+
Actual performance heavily relies on your memory speed.
|
|
214
|
+
|
|
215
|
+
# Performance Testing
|
|
216
|
+
|
|
217
|
+
I measured the time it takes to send/receive data comparing the standard queue and this shared memory implementation:
|
|
218
|
+
|
|
219
|
+
> Note: The standard queue implementation uses a sender thread, that is why the dark blue bar for 'standard send' apparently has more throughput, since it took less time for each cycle, but the actual time until the data is transfered is longer behind the scenes, making this comparison only somewhat useful.
|
|
220
|
+
|
|
221
|
+

|
|
222
|
+
|
|
223
|
+
What we can see here is that starting at around 1MB sized packages, my `py-sharedmemory` implementation starts to outperform the standard implementation.
|
|
224
|
+
Furthermore, and this is most importantly, no I/O is produced. Also, the standard implementation crashes for 10GB packages.
|
|
225
|
+
|
|
226
|
+
Generally I noticed that my system runs much more smoothly and stable when using `py-sharedmemory` with consistent put/get durations and no application slowdowns, when comparing to the standard implementation.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from typing import NamedTuple
|
|
2
|
+
from multiprocessing.shared_memory import SharedMemory
|
|
3
|
+
import pickle
|
|
4
|
+
from pickle import PickleBuffer
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SMInfo(NamedTuple):
|
|
8
|
+
smh_name: str
|
|
9
|
+
total_bytes: int
|
|
10
|
+
buffer_lengths: list[int]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def data_to_smh(data: any) -> tuple[SharedMemory, SMInfo]:
|
|
14
|
+
buffers: list[PickleBuffer] = []
|
|
15
|
+
buffer_lengths: list[int] = []
|
|
16
|
+
|
|
17
|
+
def _append_buffer(buffer: PickleBuffer):
|
|
18
|
+
memview = buffer.raw()
|
|
19
|
+
buffers.append(memview)
|
|
20
|
+
buffer_lengths.append(len(memview))
|
|
21
|
+
|
|
22
|
+
main = pickle.dumps(data, pickle.HIGHEST_PROTOCOL, buffer_callback=_append_buffer)
|
|
23
|
+
|
|
24
|
+
len_main = len(main)
|
|
25
|
+
total_bytes = len_main + sum(buffer_lengths)
|
|
26
|
+
buffer_lengths.insert(0, len_main)
|
|
27
|
+
buffers.insert(0, main)
|
|
28
|
+
|
|
29
|
+
shm: SharedMemory = SharedMemory(create=True, size=total_bytes)
|
|
30
|
+
offset = 0
|
|
31
|
+
for length, buffer in zip(buffer_lengths, buffers):
|
|
32
|
+
shm.buf[offset : offset + length] = buffer
|
|
33
|
+
offset += length
|
|
34
|
+
|
|
35
|
+
info: SMInfo = SMInfo(shm.name, total_bytes, buffer_lengths)
|
|
36
|
+
return shm, info
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def data_from_smh(info: SMInfo) -> any:
|
|
40
|
+
shm: SharedMemory = SharedMemory(name=info.smh_name, size=info.total_bytes)
|
|
41
|
+
local_buffers: bytes = bytes(shm.buf[: info.total_bytes])
|
|
42
|
+
shm.close()
|
|
43
|
+
|
|
44
|
+
# unpack data
|
|
45
|
+
buffers = []
|
|
46
|
+
offset = 0
|
|
47
|
+
for length in info.buffer_lengths:
|
|
48
|
+
buffer = local_buffers[offset : offset + length]
|
|
49
|
+
buffers.append(buffer)
|
|
50
|
+
offset += length
|
|
51
|
+
|
|
52
|
+
main = buffers.pop(0)
|
|
53
|
+
return pickle.loads(main, buffers=buffers)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import multiprocessing as mp
|
|
2
|
+
|
|
3
|
+
from .receiver import SharedMemoryReceiver
|
|
4
|
+
from .sender import SharedMemorySender
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_shared_memory_pair(capacity):
|
|
8
|
+
data_queue = mp.Queue()
|
|
9
|
+
ack_queue = mp.Queue()
|
|
10
|
+
|
|
11
|
+
sender = SharedMemorySender(capacity, data_queue, ack_queue)
|
|
12
|
+
receiver = SharedMemoryReceiver(data_queue, ack_queue)
|
|
13
|
+
return sender, receiver
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from multiprocessing.queues import Queue
|
|
2
|
+
|
|
3
|
+
from .convert import SMInfo, data_from_smh
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class SharedMemoryReceiver:
|
|
7
|
+
def __init__(self, q_data_in: Queue, q_ack_out: Queue):
|
|
8
|
+
self.q_data_in: Queue = q_data_in
|
|
9
|
+
self.q_ack_out: Queue = q_ack_out
|
|
10
|
+
|
|
11
|
+
def _process_info(self, info: SMInfo):
|
|
12
|
+
assert info, "No info received"
|
|
13
|
+
data = data_from_smh(info)
|
|
14
|
+
self.q_ack_out.put(info.smh_name)
|
|
15
|
+
del info
|
|
16
|
+
return data
|
|
17
|
+
|
|
18
|
+
def get_nowait(self):
|
|
19
|
+
return self.get(block=False)
|
|
20
|
+
|
|
21
|
+
def get(self, block: bool = True, timeout: float = None):
|
|
22
|
+
info: SMInfo = self.q_data_in.get(block, timeout)
|
|
23
|
+
return self._process_info(info)
|