py-sharedmemory 1.0.0__tar.gz → 1.0.1__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.
@@ -0,0 +1,241 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-sharedmemory
3
+ Version: 1.0.1
4
+ Summary: A replacement for Python multiprocessing.Queue that moves data objects into shared memory as raw bytes and sends only minimal metadata between processes — reducing I/O bottlenecks and improving throughput for larger data objects (>1MB).
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
+ 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).
21
+
22
+ `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.
23
+
24
+ Install using:
25
+ ```
26
+ pip install py-sharedmemory
27
+ ```
28
+
29
+ ---
30
+
31
+ # Usage example
32
+ ```python
33
+ import multiprocessing as mp
34
+
35
+ from memory import create_shared_memory_pair, SharedMemorySender, SharedMemoryReceiver
36
+
37
+ def producer_sm(sender:SharedMemorySender):
38
+ your_data = "your data"
39
+
40
+ sender.put(your_data) # blocks until there is space
41
+ # ...
42
+
43
+ # or
44
+ if sender.has_space():
45
+ sender.put(your_data)
46
+ # ...
47
+
48
+ sender.wait_for_all_ack() # wait for all data to be received before closing the sender
49
+ # ...
50
+
51
+ def consumer_sm(receiver:SharedMemoryReceiver, loops, size):
52
+ data = receiver.get() # blocks
53
+ # ...
54
+
55
+ # or
56
+ data = receiver.get(timeout=3) # raises queue.Empty exception after 3s
57
+ # ...
58
+
59
+ # or
60
+ data = receiver.get_nowait()
61
+ if data is None:
62
+ # handle empty case
63
+ # ...
64
+
65
+ if __name__ == '__main__':
66
+ sender, receiver = create_shared_memory_pair(capacity=5)
67
+
68
+ mp.Process(target=producer_sm, args=(sender,)).start()
69
+ mp.Process(target=consumer_sm, args=(receiver,)).start()
70
+ ```
71
+
72
+ ---
73
+
74
+ # Supported Types
75
+
76
+ The following data types are currently supported:
77
+ ```
78
+ - bytes
79
+ - int
80
+ - float
81
+ - bool
82
+ - str
83
+ - np.ndarray
84
+ - np.dtype
85
+ ```
86
+
87
+ As well as any nested structures of the following types:
88
+ ```
89
+ - tuple/NamedTuple
90
+ - list
91
+ - dict
92
+ - set
93
+ ```
94
+
95
+ ---
96
+
97
+ # Explanation
98
+
99
+ Let's assume I have the following data:
100
+ ```python
101
+ my_data = {
102
+ "idx": 12345,
103
+ "source": "actor_3",
104
+ "data_batch": np.zeros((64, 80, 4, 84, 84, 3), dtype=np.float32)
105
+ }
106
+ ```
107
+ 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.
108
+
109
+ 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:
110
+ ```python
111
+ (
112
+ 'wnsm_1adcf1aa', # shared memory key
113
+ 'd', 1734082569, # total byte size
114
+ <class 'dict'>, [ # data structure information
115
+ ('i', 2, False),
116
+ ('s', 7),
117
+ ('n', 1734082560, dtype('float32'), (64, 80, 4, 84, 84, 3))
118
+ ], ['idx', 'source', 'data_batch']
119
+ )
120
+ ```
121
+
122
+ <details>
123
+
124
+ <summary>Large example</summary>
125
+
126
+ Assuming this is your data object:
127
+
128
+ ```python
129
+ from typing import NamedTuple
130
+ class TestNamedTuple(NamedTuple):
131
+ name: str
132
+ value: int
133
+
134
+ extensive_test_data = {
135
+ "test_id": 98765,
136
+ "description": "This is a comprehensive test data object.",
137
+ "is_active": True,
138
+ "parameters": {
139
+ "learning_rate": 0.001,
140
+ "batch_size": 128,
141
+ "optimizer": "adam",
142
+ "epsilon": 1e-8,
143
+ "byte_config": b'\x01\x02\x03\x04'
144
+ },
145
+ "data_samples": [
146
+ {
147
+ "sample_idx": 0,
148
+ "image_data": np.random.rand(32, 32, 3).astype(np.float32),
149
+ "label": 10,
150
+ "confidence": 0.95,
151
+ "flags": [True, False, True],
152
+ "metadata": ("image", "png", 32, 32)
153
+ },
154
+ {
155
+ "sample_idx": 1,
156
+ "image_data": np.zeros((16, 16, 1), dtype=np.uint8),
157
+ "label": 5,
158
+ "confidence": 0.88,
159
+ "flags": [False, False, True],
160
+ "metadata": ("image", "jpeg", 16, 16)
161
+ }
162
+ ],
163
+ "settings_list": [
164
+ 100,
165
+ "high",
166
+ False,
167
+ 0.5,
168
+ b'setting_bytes',
169
+ (1, 'a', True),
170
+ {'nested_list_dict': [1, 2, {'deep_key': 'deep_value'}]},
171
+ np.array([1.1, 2.2, 3.3])
172
+ ],
173
+ "unique_identifiers": {101, 202, 303, 101},
174
+ "status_codes": set([b'success', b'pending', b'failed']),
175
+ "data_types_used": [
176
+ np.dtype('float32'),
177
+ np.dtype('int64'),
178
+ np.dtype('uint8'),
179
+ np.dtype('bool_')
180
+ ],
181
+ "complex_nested_structure": {
182
+ "level1_dict": {
183
+ "level2_list": [
184
+ "string_in_list",
185
+ 12345,
186
+ True,
187
+ (10, 20, 30),
188
+ {"level3_dict": {"array_in_dict": np.arange(5)}},
189
+ TestNamedTuple(name='example', value=99)
190
+ ],
191
+ "level2_set": {9, 8, 7, "set_string"},
192
+ "level2_tuple": (
193
+ "tuple_item_1",
194
+ 56.78,
195
+ b'tuple_bytes',
196
+ [1, 2, 3],
197
+ {"nested_in_tuple": "hello"}
198
+ )
199
+ }
200
+ },
201
+ "empty_structures": {
202
+ "empty_list": [],
203
+ "empty_dict": {},
204
+ "empty_set": set(),
205
+ "empty_tuple": ()
206
+ },
207
+ "large_array_example": np.random.rand(64, 80, 4, 84, 84, 3).astype(np.float32)
208
+ }
209
+ ```
210
+
211
+ Then the meta-data produced for this object is this:
212
+
213
+ `('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'])`
214
+
215
+ </details>
216
+
217
+ ---
218
+
219
+ # Considerations
220
+ There is a certain overhead to the conversion process which is especially noticable for smaller objects and more complex data structures.
221
+ Use the following heuristic depending on the size of the data you are handling:
222
+
223
+ ||10B|100B|1KB|10KB|100KB|1MB|10MB|100MB|1GB|10GB|
224
+ |---|---|---|---|---|---|---|---|---|---|---|
225
+ |``mp.Queue()``|✅|✅|✅|✅|✅|❌|❌|❌|❌|❌|
226
+ |``py-sharedmemory``|❌|❌|❌|❌|❌|✅|✅|✅|✅|✅|
227
+
228
+ Actual performance heavily relies on your memory speed.
229
+
230
+ # Performance Testing
231
+
232
+ I measured the time it takes to send/receive data comparing the standard queue and this shared memory implementation:
233
+
234
+ > 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.
235
+
236
+ ![performance comparison](https://github.com/user-attachments/assets/2a7b4f93-7698-46d5-8bb8-9d5bc7863c8f)
237
+
238
+ What we can see here is that starting at around 1MB sized packages, my `py-sharedmemory` implementation starts to outperform the standard implementation.
239
+ Furthermore, and this is most importantly, no I/O is produced. Also, the standard implementation crashes for 10GB packages.
240
+
241
+ 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,224 @@
1
+ # py-sharedmemory: High-Performance Multiprocessing Queue Alternative for Large Data
2
+
3
+ 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).
4
+
5
+ `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.
6
+
7
+ Install using:
8
+ ```
9
+ pip install py-sharedmemory
10
+ ```
11
+
12
+ ---
13
+
14
+ # Usage example
15
+ ```python
16
+ import multiprocessing as mp
17
+
18
+ from memory import create_shared_memory_pair, SharedMemorySender, SharedMemoryReceiver
19
+
20
+ def producer_sm(sender:SharedMemorySender):
21
+ your_data = "your data"
22
+
23
+ sender.put(your_data) # blocks until there is space
24
+ # ...
25
+
26
+ # or
27
+ if sender.has_space():
28
+ sender.put(your_data)
29
+ # ...
30
+
31
+ sender.wait_for_all_ack() # wait for all data to be received before closing the sender
32
+ # ...
33
+
34
+ def consumer_sm(receiver:SharedMemoryReceiver, loops, size):
35
+ data = receiver.get() # blocks
36
+ # ...
37
+
38
+ # or
39
+ data = receiver.get(timeout=3) # raises queue.Empty exception after 3s
40
+ # ...
41
+
42
+ # or
43
+ data = receiver.get_nowait()
44
+ if data is None:
45
+ # handle empty case
46
+ # ...
47
+
48
+ if __name__ == '__main__':
49
+ sender, receiver = create_shared_memory_pair(capacity=5)
50
+
51
+ mp.Process(target=producer_sm, args=(sender,)).start()
52
+ mp.Process(target=consumer_sm, args=(receiver,)).start()
53
+ ```
54
+
55
+ ---
56
+
57
+ # Supported Types
58
+
59
+ The following data types are currently supported:
60
+ ```
61
+ - bytes
62
+ - int
63
+ - float
64
+ - bool
65
+ - str
66
+ - np.ndarray
67
+ - np.dtype
68
+ ```
69
+
70
+ As well as any nested structures of the following types:
71
+ ```
72
+ - tuple/NamedTuple
73
+ - list
74
+ - dict
75
+ - set
76
+ ```
77
+
78
+ ---
79
+
80
+ # Explanation
81
+
82
+ Let's assume I have the following data:
83
+ ```python
84
+ my_data = {
85
+ "idx": 12345,
86
+ "source": "actor_3",
87
+ "data_batch": np.zeros((64, 80, 4, 84, 84, 3), dtype=np.float32)
88
+ }
89
+ ```
90
+ 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.
91
+
92
+ 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:
93
+ ```python
94
+ (
95
+ 'wnsm_1adcf1aa', # shared memory key
96
+ 'd', 1734082569, # total byte size
97
+ <class 'dict'>, [ # data structure information
98
+ ('i', 2, False),
99
+ ('s', 7),
100
+ ('n', 1734082560, dtype('float32'), (64, 80, 4, 84, 84, 3))
101
+ ], ['idx', 'source', 'data_batch']
102
+ )
103
+ ```
104
+
105
+ <details>
106
+
107
+ <summary>Large example</summary>
108
+
109
+ Assuming this is your data object:
110
+
111
+ ```python
112
+ from typing import NamedTuple
113
+ class TestNamedTuple(NamedTuple):
114
+ name: str
115
+ value: int
116
+
117
+ extensive_test_data = {
118
+ "test_id": 98765,
119
+ "description": "This is a comprehensive test data object.",
120
+ "is_active": True,
121
+ "parameters": {
122
+ "learning_rate": 0.001,
123
+ "batch_size": 128,
124
+ "optimizer": "adam",
125
+ "epsilon": 1e-8,
126
+ "byte_config": b'\x01\x02\x03\x04'
127
+ },
128
+ "data_samples": [
129
+ {
130
+ "sample_idx": 0,
131
+ "image_data": np.random.rand(32, 32, 3).astype(np.float32),
132
+ "label": 10,
133
+ "confidence": 0.95,
134
+ "flags": [True, False, True],
135
+ "metadata": ("image", "png", 32, 32)
136
+ },
137
+ {
138
+ "sample_idx": 1,
139
+ "image_data": np.zeros((16, 16, 1), dtype=np.uint8),
140
+ "label": 5,
141
+ "confidence": 0.88,
142
+ "flags": [False, False, True],
143
+ "metadata": ("image", "jpeg", 16, 16)
144
+ }
145
+ ],
146
+ "settings_list": [
147
+ 100,
148
+ "high",
149
+ False,
150
+ 0.5,
151
+ b'setting_bytes',
152
+ (1, 'a', True),
153
+ {'nested_list_dict': [1, 2, {'deep_key': 'deep_value'}]},
154
+ np.array([1.1, 2.2, 3.3])
155
+ ],
156
+ "unique_identifiers": {101, 202, 303, 101},
157
+ "status_codes": set([b'success', b'pending', b'failed']),
158
+ "data_types_used": [
159
+ np.dtype('float32'),
160
+ np.dtype('int64'),
161
+ np.dtype('uint8'),
162
+ np.dtype('bool_')
163
+ ],
164
+ "complex_nested_structure": {
165
+ "level1_dict": {
166
+ "level2_list": [
167
+ "string_in_list",
168
+ 12345,
169
+ True,
170
+ (10, 20, 30),
171
+ {"level3_dict": {"array_in_dict": np.arange(5)}},
172
+ TestNamedTuple(name='example', value=99)
173
+ ],
174
+ "level2_set": {9, 8, 7, "set_string"},
175
+ "level2_tuple": (
176
+ "tuple_item_1",
177
+ 56.78,
178
+ b'tuple_bytes',
179
+ [1, 2, 3],
180
+ {"nested_in_tuple": "hello"}
181
+ )
182
+ }
183
+ },
184
+ "empty_structures": {
185
+ "empty_list": [],
186
+ "empty_dict": {},
187
+ "empty_set": set(),
188
+ "empty_tuple": ()
189
+ },
190
+ "large_array_example": np.random.rand(64, 80, 4, 84, 84, 3).astype(np.float32)
191
+ }
192
+ ```
193
+
194
+ Then the meta-data produced for this object is this:
195
+
196
+ `('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'])`
197
+
198
+ </details>
199
+
200
+ ---
201
+
202
+ # Considerations
203
+ There is a certain overhead to the conversion process which is especially noticable for smaller objects and more complex data structures.
204
+ Use the following heuristic depending on the size of the data you are handling:
205
+
206
+ ||10B|100B|1KB|10KB|100KB|1MB|10MB|100MB|1GB|10GB|
207
+ |---|---|---|---|---|---|---|---|---|---|---|
208
+ |``mp.Queue()``|✅|✅|✅|✅|✅|❌|❌|❌|❌|❌|
209
+ |``py-sharedmemory``|❌|❌|❌|❌|❌|✅|✅|✅|✅|✅|
210
+
211
+ Actual performance heavily relies on your memory speed.
212
+
213
+ # Performance Testing
214
+
215
+ I measured the time it takes to send/receive data comparing the standard queue and this shared memory implementation:
216
+
217
+ > 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.
218
+
219
+ ![performance comparison](https://github.com/user-attachments/assets/2a7b4f93-7698-46d5-8bb8-9d5bc7863c8f)
220
+
221
+ What we can see here is that starting at around 1MB sized packages, my `py-sharedmemory` implementation starts to outperform the standard implementation.
222
+ Furthermore, and this is most importantly, no I/O is produced. Also, the standard implementation crashes for 10GB packages.
223
+
224
+ 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.
@@ -91,7 +91,7 @@ class SharedMemorySender:
91
91
 
92
92
  def put(self, data):
93
93
  self.process_ack()
94
- while len(self.open_handles) >= self.capacity:
94
+ while self.capacity and len(self.open_handles) >= self.capacity:
95
95
  self.wait_for_ack()
96
96
 
97
97
  info, bytes_datas = self._prepare_binary(data)
@@ -121,7 +121,7 @@ class SharedMemorySender:
121
121
 
122
122
  def has_space(self):
123
123
  self.process_ack()
124
- return len(self.open_handles) < self.capacity
124
+ return not self.capacity or len(self.open_handles) < self.capacity
125
125
 
126
126
  def process_ack(self):
127
127
  try:
@@ -0,0 +1,241 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-sharedmemory
3
+ Version: 1.0.1
4
+ Summary: A replacement for Python multiprocessing.Queue that moves data objects into shared memory as raw bytes and sends only minimal metadata between processes — reducing I/O bottlenecks and improving throughput for larger data objects (>1MB).
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
+ 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).
21
+
22
+ `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.
23
+
24
+ Install using:
25
+ ```
26
+ pip install py-sharedmemory
27
+ ```
28
+
29
+ ---
30
+
31
+ # Usage example
32
+ ```python
33
+ import multiprocessing as mp
34
+
35
+ from memory import create_shared_memory_pair, SharedMemorySender, SharedMemoryReceiver
36
+
37
+ def producer_sm(sender:SharedMemorySender):
38
+ your_data = "your data"
39
+
40
+ sender.put(your_data) # blocks until there is space
41
+ # ...
42
+
43
+ # or
44
+ if sender.has_space():
45
+ sender.put(your_data)
46
+ # ...
47
+
48
+ sender.wait_for_all_ack() # wait for all data to be received before closing the sender
49
+ # ...
50
+
51
+ def consumer_sm(receiver:SharedMemoryReceiver, loops, size):
52
+ data = receiver.get() # blocks
53
+ # ...
54
+
55
+ # or
56
+ data = receiver.get(timeout=3) # raises queue.Empty exception after 3s
57
+ # ...
58
+
59
+ # or
60
+ data = receiver.get_nowait()
61
+ if data is None:
62
+ # handle empty case
63
+ # ...
64
+
65
+ if __name__ == '__main__':
66
+ sender, receiver = create_shared_memory_pair(capacity=5)
67
+
68
+ mp.Process(target=producer_sm, args=(sender,)).start()
69
+ mp.Process(target=consumer_sm, args=(receiver,)).start()
70
+ ```
71
+
72
+ ---
73
+
74
+ # Supported Types
75
+
76
+ The following data types are currently supported:
77
+ ```
78
+ - bytes
79
+ - int
80
+ - float
81
+ - bool
82
+ - str
83
+ - np.ndarray
84
+ - np.dtype
85
+ ```
86
+
87
+ As well as any nested structures of the following types:
88
+ ```
89
+ - tuple/NamedTuple
90
+ - list
91
+ - dict
92
+ - set
93
+ ```
94
+
95
+ ---
96
+
97
+ # Explanation
98
+
99
+ Let's assume I have the following data:
100
+ ```python
101
+ my_data = {
102
+ "idx": 12345,
103
+ "source": "actor_3",
104
+ "data_batch": np.zeros((64, 80, 4, 84, 84, 3), dtype=np.float32)
105
+ }
106
+ ```
107
+ 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.
108
+
109
+ 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:
110
+ ```python
111
+ (
112
+ 'wnsm_1adcf1aa', # shared memory key
113
+ 'd', 1734082569, # total byte size
114
+ <class 'dict'>, [ # data structure information
115
+ ('i', 2, False),
116
+ ('s', 7),
117
+ ('n', 1734082560, dtype('float32'), (64, 80, 4, 84, 84, 3))
118
+ ], ['idx', 'source', 'data_batch']
119
+ )
120
+ ```
121
+
122
+ <details>
123
+
124
+ <summary>Large example</summary>
125
+
126
+ Assuming this is your data object:
127
+
128
+ ```python
129
+ from typing import NamedTuple
130
+ class TestNamedTuple(NamedTuple):
131
+ name: str
132
+ value: int
133
+
134
+ extensive_test_data = {
135
+ "test_id": 98765,
136
+ "description": "This is a comprehensive test data object.",
137
+ "is_active": True,
138
+ "parameters": {
139
+ "learning_rate": 0.001,
140
+ "batch_size": 128,
141
+ "optimizer": "adam",
142
+ "epsilon": 1e-8,
143
+ "byte_config": b'\x01\x02\x03\x04'
144
+ },
145
+ "data_samples": [
146
+ {
147
+ "sample_idx": 0,
148
+ "image_data": np.random.rand(32, 32, 3).astype(np.float32),
149
+ "label": 10,
150
+ "confidence": 0.95,
151
+ "flags": [True, False, True],
152
+ "metadata": ("image", "png", 32, 32)
153
+ },
154
+ {
155
+ "sample_idx": 1,
156
+ "image_data": np.zeros((16, 16, 1), dtype=np.uint8),
157
+ "label": 5,
158
+ "confidence": 0.88,
159
+ "flags": [False, False, True],
160
+ "metadata": ("image", "jpeg", 16, 16)
161
+ }
162
+ ],
163
+ "settings_list": [
164
+ 100,
165
+ "high",
166
+ False,
167
+ 0.5,
168
+ b'setting_bytes',
169
+ (1, 'a', True),
170
+ {'nested_list_dict': [1, 2, {'deep_key': 'deep_value'}]},
171
+ np.array([1.1, 2.2, 3.3])
172
+ ],
173
+ "unique_identifiers": {101, 202, 303, 101},
174
+ "status_codes": set([b'success', b'pending', b'failed']),
175
+ "data_types_used": [
176
+ np.dtype('float32'),
177
+ np.dtype('int64'),
178
+ np.dtype('uint8'),
179
+ np.dtype('bool_')
180
+ ],
181
+ "complex_nested_structure": {
182
+ "level1_dict": {
183
+ "level2_list": [
184
+ "string_in_list",
185
+ 12345,
186
+ True,
187
+ (10, 20, 30),
188
+ {"level3_dict": {"array_in_dict": np.arange(5)}},
189
+ TestNamedTuple(name='example', value=99)
190
+ ],
191
+ "level2_set": {9, 8, 7, "set_string"},
192
+ "level2_tuple": (
193
+ "tuple_item_1",
194
+ 56.78,
195
+ b'tuple_bytes',
196
+ [1, 2, 3],
197
+ {"nested_in_tuple": "hello"}
198
+ )
199
+ }
200
+ },
201
+ "empty_structures": {
202
+ "empty_list": [],
203
+ "empty_dict": {},
204
+ "empty_set": set(),
205
+ "empty_tuple": ()
206
+ },
207
+ "large_array_example": np.random.rand(64, 80, 4, 84, 84, 3).astype(np.float32)
208
+ }
209
+ ```
210
+
211
+ Then the meta-data produced for this object is this:
212
+
213
+ `('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'])`
214
+
215
+ </details>
216
+
217
+ ---
218
+
219
+ # Considerations
220
+ There is a certain overhead to the conversion process which is especially noticable for smaller objects and more complex data structures.
221
+ Use the following heuristic depending on the size of the data you are handling:
222
+
223
+ ||10B|100B|1KB|10KB|100KB|1MB|10MB|100MB|1GB|10GB|
224
+ |---|---|---|---|---|---|---|---|---|---|---|
225
+ |``mp.Queue()``|✅|✅|✅|✅|✅|❌|❌|❌|❌|❌|
226
+ |``py-sharedmemory``|❌|❌|❌|❌|❌|✅|✅|✅|✅|✅|
227
+
228
+ Actual performance heavily relies on your memory speed.
229
+
230
+ # Performance Testing
231
+
232
+ I measured the time it takes to send/receive data comparing the standard queue and this shared memory implementation:
233
+
234
+ > 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.
235
+
236
+ ![performance comparison](https://github.com/user-attachments/assets/2a7b4f93-7698-46d5-8bb8-9d5bc7863c8f)
237
+
238
+ What we can see here is that starting at around 1MB sized packages, my `py-sharedmemory` implementation starts to outperform the standard implementation.
239
+ Furthermore, and this is most importantly, no I/O is produced. Also, the standard implementation crashes for 10GB packages.
240
+
241
+ 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.
@@ -8,20 +8,19 @@ packages = ['memory']
8
8
  [project]
9
9
  name = "py-sharedmemory"
10
10
  authors = [{name = "Tobias Würth"}]
11
- version = "1.0.0"
11
+ version = "1.0.1"
12
12
  requires-python = ">=3.9"
13
13
  dependencies = [
14
14
  "numpy>=1.26"
15
15
  ]
16
16
 
17
- description = "Shared Memory Pipe for Fast Multiprocessing Data Sharing of Large Objects (>1MB)"
18
- keywords = ["python", "python3", "multiprocessing", "sharedmemory", "queue", "pipe"]
17
+ description = "A replacement for Python multiprocessing.Queue that moves data objects into shared memory as raw bytes and sends only minimal metadata between processes — reducing I/O bottlenecks and improving throughput for larger data objects (>1MB)."
18
+ keywords = ["python", "performance", "queue", "multiprocessing", "numpy", "zero-copy", "data-transfer", "low-latency", "inter-process-communication", "shared-memory", "high-throughput", "parallel-processing", "large-data", "memoryview"]
19
19
  readme = {file = "README.md", content-type = "text/markdown"}
20
- license = {file = "LICENSE"}
20
+ license = "MIT"
21
21
 
22
22
  classifiers = [
23
23
  "Programming Language :: Python :: 3",
24
- "License :: OSI Approved :: MIT License",
25
24
  "Operating System :: OS Independent",
26
25
  ]
27
26
 
@@ -1,179 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: py-sharedmemory
3
- Version: 1.0.0
4
- Summary: Shared Memory Pipe for Fast Multiprocessing Data Sharing of Large Objects (>1MB)
5
- Author: Tobias Würth
6
- License: MIT License
7
-
8
- Copyright (c) 2024 товіаѕ
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
-
28
- Project-URL: Repository, https://github.com/tobiaswuerth/python_shared_memory_queue
29
- Project-URL: Bug Tracker, https://github.com/tobiaswuerth/python_shared_memory_queue/issues
30
- Keywords: python,python3,multiprocessing,sharedmemory,queue,pipe
31
- Classifier: Programming Language :: Python :: 3
32
- Classifier: License :: OSI Approved :: MIT License
33
- Classifier: Operating System :: OS Independent
34
- Requires-Python: >=3.9
35
- Description-Content-Type: text/markdown
36
- License-File: LICENSE
37
- Requires-Dist: numpy>=1.26
38
-
39
- # Shared Memory Pipe for Fast Multiprocessing Data Sharing of Large Objects (>1MB)
40
-
41
- Since [multiprocessing](https://docs.python.org/3/library/multiprocessing.html) [Queue](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue)s pickles the data, there is a certain performance decrease for larger data objects and huge amounts of I/O.
42
-
43
- This is the reason why I created a substitute which instead converts data objects into bytes (if needed), puts the bytes into shared memory and only transfers meta-data about the data structure via Queues to the receiving end.
44
-
45
- The following data types are currently supported:
46
- ```
47
- - bytes
48
- - int
49
- - float
50
- - bool
51
- - str
52
- - np.ndarray
53
- - np.dtype
54
- ```
55
-
56
- As well as any nested structures for the given types:
57
- ```
58
- - tuple/NamedTuple
59
- - list
60
- - dict
61
- - set
62
- ```
63
-
64
- # Considerations
65
- There is a certain overhead to the conversion process which is especially noticable for smaller objects and more complex data structures.
66
- Use the following approach depending on the size of the data you're handling:
67
-
68
- ||10B|100B|1KB|10KB|100KB|1MB|10MB|100MB|1GB|10GB|
69
- |---|---|---|---|---|---|---|---|---|---|---|
70
- |``mp.Queue()``|✅|✅|✅|✅|✅|❌|❌|❌|❌|❌|
71
- |``SharedMemory``|❌|❌|❌|❌|❌|✅|✅|✅|✅|✅|
72
-
73
- Actual performance heavily relies on your memory speed.
74
-
75
- # Usage example
76
- ```python
77
- import multiprocessing as mp
78
-
79
- from memory import create_shared_memory_pair, SharedMemorySender, SharedMemoryReceiver
80
-
81
- def producer_sm(sender:SharedMemorySender):
82
- your_data = "your data"
83
-
84
- # ...
85
- sender.put(your_data) # blocks until there is space
86
-
87
- # or
88
- if sender.has_space():
89
- sender.put(your_data)
90
- else:
91
- # your else case
92
-
93
- # ...
94
- sender.wait_for_all_ack() # wait for all data to be received before closing the sender
95
- # exit
96
-
97
- def consumer_sm(receiver:SharedMemoryReceiver, loops, size):
98
- # ...
99
- data = receiver.get() # blocks
100
-
101
- # or
102
- data = receiver.get(timeout=3) # raises queue.Empty exception after 3s
103
-
104
- # or
105
- data = receiver.get_nowait()
106
- if data is None:
107
- # handle empty case
108
-
109
- # ...
110
-
111
- if __name__ == '__main__':
112
- sender, receiver = create_shared_memory_pair(capacity=5)
113
-
114
- mp.Process(target=producer_sm, args=(sender,)).start()
115
- mp.Process(target=consumer_sm, args=(receiver,)).start()
116
-
117
- ```
118
-
119
- # Performance Testing
120
- Note that in this testing example Producer and Consumer where dependent upon each other (due to Queue capacity), that's why they take a similar amount of time. Your actual performance may vary depending on the data type, structure and overall system performance.
121
-
122
- Since there is no inter-process data transfer, I/O drops accordingly. E.g. in my case -2.4GB/s
123
-
124
- ```
125
- --------------------
126
- Bytes: 100 (100 B), loops: 100000, queue capacity: 1000
127
- Producer done in 0.78797s @ 12.10 MB/s
128
- Consumer done in 0.78698s @ 12.12 MB/s
129
- Producer done in SM 5.37207s @ 1.78 MB/s
130
- Consumer done in SM 5.37207s @ 1.78 MB/s ❌
131
- --------------------
132
- Bytes: 1000 (1000 B), loops: 100000, queue capacity: 1000
133
- Producer done in 0.92900s @ 102.66 MB/s
134
- Consumer done in 0.91599s @ 104.11 MB/s
135
- Producer done in SM 5.45683s @ 17.48 MB/s
136
- Consumer done in SM 5.45582s @ 17.48 MB/s ❌
137
- --------------------
138
- Bytes: 10000 (9.77 KB), loops: 100000, queue capacity: 1000
139
- Producer done in 2.16847s @ 439.79 MB/s
140
- Consumer done in 2.16147s @ 441.22 MB/s
141
- Producer done in SM 5.64625s @ 168.90 MB/s
142
- Consumer done in SM 5.64625s @ 168.90 MB/s ❌
143
- --------------------
144
- Bytes: 100000 (97.66 KB), loops: 100000, queue capacity: 1000
145
- Producer done in 2.80213s @ 3.32 GB/s
146
- Consumer done in 2.80013s @ 3.33 GB/s
147
- Producer done in SM 8.24400s @ 1.13 GB/s
148
- Consumer done in SM 8.24200s @ 1.13 GB/s ❌
149
- --------------------
150
- Bytes: 1000000 (976.56 KB), loops: 10000, queue capacity: 1000
151
- Producer done in 4.87300s @ 1.91 GB/s
152
- Consumer done in 4.87300s @ 1.91 GB/s
153
- Producer done in SM 4.01900s @ 2.32 GB/s
154
- Consumer done in SM 4.01800s @ 2.32 GB/s ✅
155
- --------------------
156
- Bytes: 10000000 (9.54 MB), loops: 1000, queue capacity: 1000
157
- Producer done in 6.25722s @ 1.49 GB/s
158
- Consumer done in 6.28221s @ 1.48 GB/s
159
- Producer done in SM 3.79851s @ 2.45 GB/s
160
- Consumer done in SM 3.80359s @ 2.45 GB/s ✅
161
- --------------------
162
- Bytes: 100000000 (95.37 MB), loops: 1000, queue capacity: 100
163
- Producer done in 64.91876s @ 1.43 GB/s
164
- Consumer done in 65.20476s @ 1.43 GB/s
165
- Producer done in SM 38.08093s @ 2.45 GB/s
166
- Consumer done in SM 38.17893s @ 2.44 GB/s ✅
167
- --------------------
168
- Bytes: 1000000000 (953.67 MB), loops: 100, queue capacity: 10
169
- Producer done in 63.22359s @ 1.47 GB/s
170
- Consumer done in 66.07801s @ 1.41 GB/s
171
- Producer done in SM 36.39488s @ 2.56 GB/s
172
- Consumer done in SM 37.61108s @ 2.48 GB/s ✅
173
- --------------------
174
- Bytes: 10000000000 (9.31 GB), loops: 10, queue capacity: 10
175
- Producer done in CRASHED
176
- Consumer done in CRASHED
177
- Producer done in SM 28.21499s @ 3.30 GB/s
178
- Consumer done in SM 34.32684s @ 2.71 GB/s ✅✅
179
- ```
@@ -1,141 +0,0 @@
1
- # Shared Memory Pipe for Fast Multiprocessing Data Sharing of Large Objects (>1MB)
2
-
3
- Since [multiprocessing](https://docs.python.org/3/library/multiprocessing.html) [Queue](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue)s pickles the data, there is a certain performance decrease for larger data objects and huge amounts of I/O.
4
-
5
- This is the reason why I created a substitute which instead converts data objects into bytes (if needed), puts the bytes into shared memory and only transfers meta-data about the data structure via Queues to the receiving end.
6
-
7
- The following data types are currently supported:
8
- ```
9
- - bytes
10
- - int
11
- - float
12
- - bool
13
- - str
14
- - np.ndarray
15
- - np.dtype
16
- ```
17
-
18
- As well as any nested structures for the given types:
19
- ```
20
- - tuple/NamedTuple
21
- - list
22
- - dict
23
- - set
24
- ```
25
-
26
- # Considerations
27
- There is a certain overhead to the conversion process which is especially noticable for smaller objects and more complex data structures.
28
- Use the following approach depending on the size of the data you're handling:
29
-
30
- ||10B|100B|1KB|10KB|100KB|1MB|10MB|100MB|1GB|10GB|
31
- |---|---|---|---|---|---|---|---|---|---|---|
32
- |``mp.Queue()``|✅|✅|✅|✅|✅|❌|❌|❌|❌|❌|
33
- |``SharedMemory``|❌|❌|❌|❌|❌|✅|✅|✅|✅|✅|
34
-
35
- Actual performance heavily relies on your memory speed.
36
-
37
- # Usage example
38
- ```python
39
- import multiprocessing as mp
40
-
41
- from memory import create_shared_memory_pair, SharedMemorySender, SharedMemoryReceiver
42
-
43
- def producer_sm(sender:SharedMemorySender):
44
- your_data = "your data"
45
-
46
- # ...
47
- sender.put(your_data) # blocks until there is space
48
-
49
- # or
50
- if sender.has_space():
51
- sender.put(your_data)
52
- else:
53
- # your else case
54
-
55
- # ...
56
- sender.wait_for_all_ack() # wait for all data to be received before closing the sender
57
- # exit
58
-
59
- def consumer_sm(receiver:SharedMemoryReceiver, loops, size):
60
- # ...
61
- data = receiver.get() # blocks
62
-
63
- # or
64
- data = receiver.get(timeout=3) # raises queue.Empty exception after 3s
65
-
66
- # or
67
- data = receiver.get_nowait()
68
- if data is None:
69
- # handle empty case
70
-
71
- # ...
72
-
73
- if __name__ == '__main__':
74
- sender, receiver = create_shared_memory_pair(capacity=5)
75
-
76
- mp.Process(target=producer_sm, args=(sender,)).start()
77
- mp.Process(target=consumer_sm, args=(receiver,)).start()
78
-
79
- ```
80
-
81
- # Performance Testing
82
- Note that in this testing example Producer and Consumer where dependent upon each other (due to Queue capacity), that's why they take a similar amount of time. Your actual performance may vary depending on the data type, structure and overall system performance.
83
-
84
- Since there is no inter-process data transfer, I/O drops accordingly. E.g. in my case -2.4GB/s
85
-
86
- ```
87
- --------------------
88
- Bytes: 100 (100 B), loops: 100000, queue capacity: 1000
89
- Producer done in 0.78797s @ 12.10 MB/s
90
- Consumer done in 0.78698s @ 12.12 MB/s
91
- Producer done in SM 5.37207s @ 1.78 MB/s
92
- Consumer done in SM 5.37207s @ 1.78 MB/s ❌
93
- --------------------
94
- Bytes: 1000 (1000 B), loops: 100000, queue capacity: 1000
95
- Producer done in 0.92900s @ 102.66 MB/s
96
- Consumer done in 0.91599s @ 104.11 MB/s
97
- Producer done in SM 5.45683s @ 17.48 MB/s
98
- Consumer done in SM 5.45582s @ 17.48 MB/s ❌
99
- --------------------
100
- Bytes: 10000 (9.77 KB), loops: 100000, queue capacity: 1000
101
- Producer done in 2.16847s @ 439.79 MB/s
102
- Consumer done in 2.16147s @ 441.22 MB/s
103
- Producer done in SM 5.64625s @ 168.90 MB/s
104
- Consumer done in SM 5.64625s @ 168.90 MB/s ❌
105
- --------------------
106
- Bytes: 100000 (97.66 KB), loops: 100000, queue capacity: 1000
107
- Producer done in 2.80213s @ 3.32 GB/s
108
- Consumer done in 2.80013s @ 3.33 GB/s
109
- Producer done in SM 8.24400s @ 1.13 GB/s
110
- Consumer done in SM 8.24200s @ 1.13 GB/s ❌
111
- --------------------
112
- Bytes: 1000000 (976.56 KB), loops: 10000, queue capacity: 1000
113
- Producer done in 4.87300s @ 1.91 GB/s
114
- Consumer done in 4.87300s @ 1.91 GB/s
115
- Producer done in SM 4.01900s @ 2.32 GB/s
116
- Consumer done in SM 4.01800s @ 2.32 GB/s ✅
117
- --------------------
118
- Bytes: 10000000 (9.54 MB), loops: 1000, queue capacity: 1000
119
- Producer done in 6.25722s @ 1.49 GB/s
120
- Consumer done in 6.28221s @ 1.48 GB/s
121
- Producer done in SM 3.79851s @ 2.45 GB/s
122
- Consumer done in SM 3.80359s @ 2.45 GB/s ✅
123
- --------------------
124
- Bytes: 100000000 (95.37 MB), loops: 1000, queue capacity: 100
125
- Producer done in 64.91876s @ 1.43 GB/s
126
- Consumer done in 65.20476s @ 1.43 GB/s
127
- Producer done in SM 38.08093s @ 2.45 GB/s
128
- Consumer done in SM 38.17893s @ 2.44 GB/s ✅
129
- --------------------
130
- Bytes: 1000000000 (953.67 MB), loops: 100, queue capacity: 10
131
- Producer done in 63.22359s @ 1.47 GB/s
132
- Consumer done in 66.07801s @ 1.41 GB/s
133
- Producer done in SM 36.39488s @ 2.56 GB/s
134
- Consumer done in SM 37.61108s @ 2.48 GB/s ✅
135
- --------------------
136
- Bytes: 10000000000 (9.31 GB), loops: 10, queue capacity: 10
137
- Producer done in CRASHED
138
- Consumer done in CRASHED
139
- Producer done in SM 28.21499s @ 3.30 GB/s
140
- Consumer done in SM 34.32684s @ 2.71 GB/s ✅✅
141
- ```
@@ -1,179 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: py-sharedmemory
3
- Version: 1.0.0
4
- Summary: Shared Memory Pipe for Fast Multiprocessing Data Sharing of Large Objects (>1MB)
5
- Author: Tobias Würth
6
- License: MIT License
7
-
8
- Copyright (c) 2024 товіаѕ
9
-
10
- Permission is hereby granted, free of charge, to any person obtaining a copy
11
- of this software and associated documentation files (the "Software"), to deal
12
- in the Software without restriction, including without limitation the rights
13
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
- copies of the Software, and to permit persons to whom the Software is
15
- furnished to do so, subject to the following conditions:
16
-
17
- The above copyright notice and this permission notice shall be included in all
18
- copies or substantial portions of the Software.
19
-
20
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
- SOFTWARE.
27
-
28
- Project-URL: Repository, https://github.com/tobiaswuerth/python_shared_memory_queue
29
- Project-URL: Bug Tracker, https://github.com/tobiaswuerth/python_shared_memory_queue/issues
30
- Keywords: python,python3,multiprocessing,sharedmemory,queue,pipe
31
- Classifier: Programming Language :: Python :: 3
32
- Classifier: License :: OSI Approved :: MIT License
33
- Classifier: Operating System :: OS Independent
34
- Requires-Python: >=3.9
35
- Description-Content-Type: text/markdown
36
- License-File: LICENSE
37
- Requires-Dist: numpy>=1.26
38
-
39
- # Shared Memory Pipe for Fast Multiprocessing Data Sharing of Large Objects (>1MB)
40
-
41
- Since [multiprocessing](https://docs.python.org/3/library/multiprocessing.html) [Queue](https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue)s pickles the data, there is a certain performance decrease for larger data objects and huge amounts of I/O.
42
-
43
- This is the reason why I created a substitute which instead converts data objects into bytes (if needed), puts the bytes into shared memory and only transfers meta-data about the data structure via Queues to the receiving end.
44
-
45
- The following data types are currently supported:
46
- ```
47
- - bytes
48
- - int
49
- - float
50
- - bool
51
- - str
52
- - np.ndarray
53
- - np.dtype
54
- ```
55
-
56
- As well as any nested structures for the given types:
57
- ```
58
- - tuple/NamedTuple
59
- - list
60
- - dict
61
- - set
62
- ```
63
-
64
- # Considerations
65
- There is a certain overhead to the conversion process which is especially noticable for smaller objects and more complex data structures.
66
- Use the following approach depending on the size of the data you're handling:
67
-
68
- ||10B|100B|1KB|10KB|100KB|1MB|10MB|100MB|1GB|10GB|
69
- |---|---|---|---|---|---|---|---|---|---|---|
70
- |``mp.Queue()``|✅|✅|✅|✅|✅|❌|❌|❌|❌|❌|
71
- |``SharedMemory``|❌|❌|❌|❌|❌|✅|✅|✅|✅|✅|
72
-
73
- Actual performance heavily relies on your memory speed.
74
-
75
- # Usage example
76
- ```python
77
- import multiprocessing as mp
78
-
79
- from memory import create_shared_memory_pair, SharedMemorySender, SharedMemoryReceiver
80
-
81
- def producer_sm(sender:SharedMemorySender):
82
- your_data = "your data"
83
-
84
- # ...
85
- sender.put(your_data) # blocks until there is space
86
-
87
- # or
88
- if sender.has_space():
89
- sender.put(your_data)
90
- else:
91
- # your else case
92
-
93
- # ...
94
- sender.wait_for_all_ack() # wait for all data to be received before closing the sender
95
- # exit
96
-
97
- def consumer_sm(receiver:SharedMemoryReceiver, loops, size):
98
- # ...
99
- data = receiver.get() # blocks
100
-
101
- # or
102
- data = receiver.get(timeout=3) # raises queue.Empty exception after 3s
103
-
104
- # or
105
- data = receiver.get_nowait()
106
- if data is None:
107
- # handle empty case
108
-
109
- # ...
110
-
111
- if __name__ == '__main__':
112
- sender, receiver = create_shared_memory_pair(capacity=5)
113
-
114
- mp.Process(target=producer_sm, args=(sender,)).start()
115
- mp.Process(target=consumer_sm, args=(receiver,)).start()
116
-
117
- ```
118
-
119
- # Performance Testing
120
- Note that in this testing example Producer and Consumer where dependent upon each other (due to Queue capacity), that's why they take a similar amount of time. Your actual performance may vary depending on the data type, structure and overall system performance.
121
-
122
- Since there is no inter-process data transfer, I/O drops accordingly. E.g. in my case -2.4GB/s
123
-
124
- ```
125
- --------------------
126
- Bytes: 100 (100 B), loops: 100000, queue capacity: 1000
127
- Producer done in 0.78797s @ 12.10 MB/s
128
- Consumer done in 0.78698s @ 12.12 MB/s
129
- Producer done in SM 5.37207s @ 1.78 MB/s
130
- Consumer done in SM 5.37207s @ 1.78 MB/s ❌
131
- --------------------
132
- Bytes: 1000 (1000 B), loops: 100000, queue capacity: 1000
133
- Producer done in 0.92900s @ 102.66 MB/s
134
- Consumer done in 0.91599s @ 104.11 MB/s
135
- Producer done in SM 5.45683s @ 17.48 MB/s
136
- Consumer done in SM 5.45582s @ 17.48 MB/s ❌
137
- --------------------
138
- Bytes: 10000 (9.77 KB), loops: 100000, queue capacity: 1000
139
- Producer done in 2.16847s @ 439.79 MB/s
140
- Consumer done in 2.16147s @ 441.22 MB/s
141
- Producer done in SM 5.64625s @ 168.90 MB/s
142
- Consumer done in SM 5.64625s @ 168.90 MB/s ❌
143
- --------------------
144
- Bytes: 100000 (97.66 KB), loops: 100000, queue capacity: 1000
145
- Producer done in 2.80213s @ 3.32 GB/s
146
- Consumer done in 2.80013s @ 3.33 GB/s
147
- Producer done in SM 8.24400s @ 1.13 GB/s
148
- Consumer done in SM 8.24200s @ 1.13 GB/s ❌
149
- --------------------
150
- Bytes: 1000000 (976.56 KB), loops: 10000, queue capacity: 1000
151
- Producer done in 4.87300s @ 1.91 GB/s
152
- Consumer done in 4.87300s @ 1.91 GB/s
153
- Producer done in SM 4.01900s @ 2.32 GB/s
154
- Consumer done in SM 4.01800s @ 2.32 GB/s ✅
155
- --------------------
156
- Bytes: 10000000 (9.54 MB), loops: 1000, queue capacity: 1000
157
- Producer done in 6.25722s @ 1.49 GB/s
158
- Consumer done in 6.28221s @ 1.48 GB/s
159
- Producer done in SM 3.79851s @ 2.45 GB/s
160
- Consumer done in SM 3.80359s @ 2.45 GB/s ✅
161
- --------------------
162
- Bytes: 100000000 (95.37 MB), loops: 1000, queue capacity: 100
163
- Producer done in 64.91876s @ 1.43 GB/s
164
- Consumer done in 65.20476s @ 1.43 GB/s
165
- Producer done in SM 38.08093s @ 2.45 GB/s
166
- Consumer done in SM 38.17893s @ 2.44 GB/s ✅
167
- --------------------
168
- Bytes: 1000000000 (953.67 MB), loops: 100, queue capacity: 10
169
- Producer done in 63.22359s @ 1.47 GB/s
170
- Consumer done in 66.07801s @ 1.41 GB/s
171
- Producer done in SM 36.39488s @ 2.56 GB/s
172
- Consumer done in SM 37.61108s @ 2.48 GB/s ✅
173
- --------------------
174
- Bytes: 10000000000 (9.31 GB), loops: 10, queue capacity: 10
175
- Producer done in CRASHED
176
- Consumer done in CRASHED
177
- Producer done in SM 28.21499s @ 3.30 GB/s
178
- Consumer done in SM 34.32684s @ 2.71 GB/s ✅✅
179
- ```
File without changes