py-sharedmemory 4.2.0__tar.gz → 4.2.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.
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/CHANGELOG.md +10 -0
- {py_sharedmemory-4.2.0/py_sharedmemory.egg-info → py_sharedmemory-4.2.1}/PKG-INFO +8 -6
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/README.md +7 -5
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/py_sharedmemory/__init__.py +1 -1
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/py_sharedmemory/convert.py +23 -10
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/py_sharedmemory/sender.py +3 -2
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1/py_sharedmemory.egg-info}/PKG-INFO +8 -6
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/tests/test_convert.py +89 -8
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/LICENSE +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/MANIFEST.in +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/benchmarks/__init__.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/benchmarks/benchmark.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/benchmarks/plot.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/benchmarks/results/windows-numpy.json +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/benchmarks/results/windows-numpy.png +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/examples/basic.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/examples/numpy_batches.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/examples/worker_pool.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/py_sharedmemory/factory.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/py_sharedmemory/py.typed +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/py_sharedmemory/receiver.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/py_sharedmemory.egg-info/SOURCES.txt +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/py_sharedmemory.egg-info/dependency_links.txt +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/py_sharedmemory.egg-info/requires.txt +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/py_sharedmemory.egg-info/top_level.txt +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/pyproject.toml +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/setup.cfg +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/tests/__init__.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/tests/conftest.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/tests/helpers.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/tests/test_benchmark.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/tests/test_objects.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/tests/test_package.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/tests/test_processes.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/tests/test_queue.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/tests/test_recycling.py +0 -0
- {py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/tests/test_threads.py +0 -0
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.2.1
|
|
4
|
+
|
|
5
|
+
- `put` copies large `bytes` and `bytearray` objects (64 KiB and up) straight into shared
|
|
6
|
+
memory, wherever they appear in a message. It used to build the complete pickle in
|
|
7
|
+
memory first: an extra copy of the payload that briefly needed up to 1.5 times its size
|
|
8
|
+
(48 MiB for a 32 MiB `bytearray`). Strided NumPy arrays, which NumPy pickles as `bytes`,
|
|
9
|
+
skip that copy too. The message format is unchanged.
|
|
10
|
+
- Resizing a `bytearray` while its message is being pickled, from a `__reduce__` method,
|
|
11
|
+
now raises `BufferError` instead of risking a corrupted message.
|
|
12
|
+
|
|
3
13
|
## 4.2.0
|
|
4
14
|
|
|
5
15
|
- Reuse received shared-memory blocks instead of freeing them. Writing into fresh pages
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py-sharedmemory
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.1
|
|
4
4
|
Summary: Fast multiprocessing.Queue alternative that moves large Python objects and NumPy arrays through shared memory
|
|
5
5
|
Author: Tobias Würth
|
|
6
6
|
License-Expression: MIT
|
|
@@ -68,8 +68,9 @@ images, and simulation states arrive up to 2× faster, and transfers too large f
|
|
|
68
68
|
|
|
69
69
|
- **Familiar API:** `put` / `get` with `block`, `timeout`, `put_nowait` / `get_nowait`,
|
|
70
70
|
`queue.Full`, and `queue.Empty`.
|
|
71
|
-
- **Any picklable object:** NumPy arrays
|
|
72
|
-
|
|
71
|
+
- **Any picklable object:** NumPy arrays, other protocol-5 buffers, and large `bytes` and
|
|
72
|
+
`bytearray` objects are copied straight into shared memory, without an intermediate
|
|
73
|
+
pickle copy.
|
|
73
74
|
- **Bounded memory:** a capacity limit applies backpressure, and received blocks are
|
|
74
75
|
reused for later messages, so shared memory stays within `capacity × message size`.
|
|
75
76
|
- **One producer, many consumers:** consumer processes and threads share one receiver.
|
|
@@ -161,9 +162,10 @@ pages on every `put`.
|
|
|
161
162
|
|
|
162
163
|
## How it works
|
|
163
164
|
|
|
164
|
-
1. `put()` pickles the object with protocol 5. Large buffers, such as NumPy array data
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
1. `put()` pickles the object with protocol 5. Large buffers, such as NumPy array data and
|
|
166
|
+
`bytes` or `bytearray` objects of 64 KiB or more, never enter the pickle; everything is copied once,
|
|
167
|
+
from the original objects into one shared-memory block: a block from an earlier,
|
|
168
|
+
received message when one fits, otherwise a new one.
|
|
167
169
|
2. Only the block's name, buffer sizes, and a sequence number travel through a
|
|
168
170
|
`multiprocessing.Queue`.
|
|
169
171
|
3. `get()` attaches to the block, copies the data into local memory, unpickles it, and
|
|
@@ -18,8 +18,9 @@ images, and simulation states arrive up to 2× faster, and transfers too large f
|
|
|
18
18
|
|
|
19
19
|
- **Familiar API:** `put` / `get` with `block`, `timeout`, `put_nowait` / `get_nowait`,
|
|
20
20
|
`queue.Full`, and `queue.Empty`.
|
|
21
|
-
- **Any picklable object:** NumPy arrays
|
|
22
|
-
|
|
21
|
+
- **Any picklable object:** NumPy arrays, other protocol-5 buffers, and large `bytes` and
|
|
22
|
+
`bytearray` objects are copied straight into shared memory, without an intermediate
|
|
23
|
+
pickle copy.
|
|
23
24
|
- **Bounded memory:** a capacity limit applies backpressure, and received blocks are
|
|
24
25
|
reused for later messages, so shared memory stays within `capacity × message size`.
|
|
25
26
|
- **One producer, many consumers:** consumer processes and threads share one receiver.
|
|
@@ -111,9 +112,10 @@ pages on every `put`.
|
|
|
111
112
|
|
|
112
113
|
## How it works
|
|
113
114
|
|
|
114
|
-
1. `put()` pickles the object with protocol 5. Large buffers, such as NumPy array data
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
1. `put()` pickles the object with protocol 5. Large buffers, such as NumPy array data and
|
|
116
|
+
`bytes` or `bytearray` objects of 64 KiB or more, never enter the pickle; everything is copied once,
|
|
117
|
+
from the original objects into one shared-memory block: a block from an earlier,
|
|
118
|
+
received message when one fits, otherwise a new one.
|
|
117
119
|
2. Only the block's name, buffer sizes, and a sequence number travel through a
|
|
118
120
|
`multiprocessing.Queue`.
|
|
119
121
|
3. `get()` attaches to the block, copies the data into local memory, unpickles it, and
|
|
@@ -25,5 +25,5 @@ from .factory import create_shared_memory_pair
|
|
|
25
25
|
from .receiver import SharedMemoryReceiver
|
|
26
26
|
from .sender import SharedMemorySender
|
|
27
27
|
|
|
28
|
-
__version__ = "4.2.
|
|
28
|
+
__version__ = "4.2.1"
|
|
29
29
|
__all__ = ["SharedMemoryReceiver", "SharedMemorySender", "__version__", "create_shared_memory_pair"]
|
|
@@ -37,6 +37,16 @@ def release_shared_memory(allocation: SharedMemory) -> None:
|
|
|
37
37
|
allocation.unlink()
|
|
38
38
|
|
|
39
39
|
|
|
40
|
+
class _PickleChunks(list[memoryview]):
|
|
41
|
+
"""Pickle output kept as views of what pickle wrote rather than one joined copy."""
|
|
42
|
+
|
|
43
|
+
def write(self, data: Any) -> None:
|
|
44
|
+
# Pickle passes bytes and bytearrays of 64 KiB or more as the objects themselves.
|
|
45
|
+
# A view also stops a bytearray from being resized before it is copied, which would
|
|
46
|
+
# leave its length header in the stream wrong.
|
|
47
|
+
self.append(memoryview(data))
|
|
48
|
+
|
|
49
|
+
|
|
40
50
|
def serialize(
|
|
41
51
|
data: Any,
|
|
42
52
|
sequence: int = 0,
|
|
@@ -44,8 +54,11 @@ def serialize(
|
|
|
44
54
|
) -> tuple[SharedMemory, SharedMemoryMessage]:
|
|
45
55
|
"""Copy a pickle and its out-of-band buffers into an owned block of at least that size.
|
|
46
56
|
|
|
47
|
-
|
|
57
|
+
Large bytes, bytearrays, and contiguous arrays are copied once, straight from the
|
|
58
|
+
original objects into shared memory. `allocate` returns a new or reused block; this
|
|
59
|
+
function frees it if copying fails.
|
|
48
60
|
"""
|
|
61
|
+
chunks = _PickleChunks()
|
|
49
62
|
buffers: list[memoryview] = []
|
|
50
63
|
allocation = None
|
|
51
64
|
|
|
@@ -53,23 +66,23 @@ def serialize(
|
|
|
53
66
|
buffers.append(buffer.raw())
|
|
54
67
|
|
|
55
68
|
try:
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
pickle.Pickler(chunks, protocol=5, buffer_callback=append_buffer).dump(data)
|
|
70
|
+
pickle_size = sum(chunk.nbytes for chunk in chunks)
|
|
71
|
+
buffer_sizes = (pickle_size, *(buffer.nbytes for buffer in buffers))
|
|
58
72
|
allocation = allocate(sum(buffer_sizes))
|
|
59
73
|
shared = cast(memoryview, allocation.buf)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
offset += buffer.nbytes
|
|
74
|
+
offset = 0
|
|
75
|
+
for view in (*chunks, *buffers):
|
|
76
|
+
shared[offset : offset + view.nbytes] = view
|
|
77
|
+
offset += view.nbytes
|
|
65
78
|
return allocation, SharedMemoryMessage(allocation.name, buffer_sizes, sequence)
|
|
66
79
|
except BaseException:
|
|
67
80
|
if allocation is not None:
|
|
68
81
|
release_shared_memory(allocation)
|
|
69
82
|
raise
|
|
70
83
|
finally:
|
|
71
|
-
for
|
|
72
|
-
|
|
84
|
+
for view in (*chunks, *buffers):
|
|
85
|
+
view.release()
|
|
73
86
|
|
|
74
87
|
|
|
75
88
|
def deserialize(message: SharedMemoryMessage) -> Any:
|
|
@@ -104,8 +104,9 @@ class SharedMemorySender:
|
|
|
104
104
|
"""Send a picklable object through shared memory.
|
|
105
105
|
|
|
106
106
|
Args:
|
|
107
|
-
data: Any object that pickle protocol 5 can serialize. NumPy arrays
|
|
108
|
-
|
|
107
|
+
data: Any object that pickle protocol 5 can serialize. NumPy arrays, other
|
|
108
|
+
buffers, and large bytes-like objects are copied without an extra
|
|
109
|
+
pickle copy.
|
|
109
110
|
block: Wait for capacity when `capacity` messages are outstanding.
|
|
110
111
|
timeout: Maximum seconds to wait for capacity; `None` waits forever.
|
|
111
112
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py-sharedmemory
|
|
3
|
-
Version: 4.2.
|
|
3
|
+
Version: 4.2.1
|
|
4
4
|
Summary: Fast multiprocessing.Queue alternative that moves large Python objects and NumPy arrays through shared memory
|
|
5
5
|
Author: Tobias Würth
|
|
6
6
|
License-Expression: MIT
|
|
@@ -68,8 +68,9 @@ images, and simulation states arrive up to 2× faster, and transfers too large f
|
|
|
68
68
|
|
|
69
69
|
- **Familiar API:** `put` / `get` with `block`, `timeout`, `put_nowait` / `get_nowait`,
|
|
70
70
|
`queue.Full`, and `queue.Empty`.
|
|
71
|
-
- **Any picklable object:** NumPy arrays
|
|
72
|
-
|
|
71
|
+
- **Any picklable object:** NumPy arrays, other protocol-5 buffers, and large `bytes` and
|
|
72
|
+
`bytearray` objects are copied straight into shared memory, without an intermediate
|
|
73
|
+
pickle copy.
|
|
73
74
|
- **Bounded memory:** a capacity limit applies backpressure, and received blocks are
|
|
74
75
|
reused for later messages, so shared memory stays within `capacity × message size`.
|
|
75
76
|
- **One producer, many consumers:** consumer processes and threads share one receiver.
|
|
@@ -161,9 +162,10 @@ pages on every `put`.
|
|
|
161
162
|
|
|
162
163
|
## How it works
|
|
163
164
|
|
|
164
|
-
1. `put()` pickles the object with protocol 5. Large buffers, such as NumPy array data
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
1. `put()` pickles the object with protocol 5. Large buffers, such as NumPy array data and
|
|
166
|
+
`bytes` or `bytearray` objects of 64 KiB or more, never enter the pickle; everything is copied once,
|
|
167
|
+
from the original objects into one shared-memory block: a block from an earlier,
|
|
168
|
+
received message when one fits, otherwise a new one.
|
|
167
169
|
2. Only the block's name, buffer sizes, and a sequence number travel through a
|
|
168
170
|
`multiprocessing.Queue`.
|
|
169
171
|
3. `get()` attaches to the block, copies the data into local memory, unpickles it, and
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import pickle
|
|
2
2
|
import sys
|
|
3
|
+
import tracemalloc
|
|
3
4
|
from multiprocessing.shared_memory import SharedMemory
|
|
4
5
|
from unittest.mock import Mock
|
|
5
6
|
|
|
@@ -155,26 +156,106 @@ def test_protocol_5_buffer_mutability_and_empty_buffers(payload):
|
|
|
155
156
|
assert payload[0] != 0
|
|
156
157
|
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
MiB = 2**20
|
|
160
|
+
# Pickle hands bytes-like objects from 64 KiB up to its output file by reference.
|
|
161
|
+
LARGE = 2**17
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@pytest.mark.parametrize(
|
|
165
|
+
"payload",
|
|
166
|
+
[
|
|
167
|
+
pytest.param(lambda: bytes(32 * MiB), id="bytes"),
|
|
168
|
+
pytest.param(lambda: bytearray(32 * MiB), id="bytearray"),
|
|
169
|
+
pytest.param(
|
|
170
|
+
lambda: {"step": 7, "frames": bytearray(32 * MiB), "tags": ["train"] * 100},
|
|
171
|
+
id="nested",
|
|
172
|
+
),
|
|
173
|
+
],
|
|
174
|
+
)
|
|
175
|
+
def test_large_bytes_reach_shared_memory_without_an_intermediate_copy(payload):
|
|
176
|
+
original = payload()
|
|
177
|
+
tracemalloc.start()
|
|
178
|
+
try:
|
|
179
|
+
handle, info = serialize(original)
|
|
180
|
+
peak = tracemalloc.get_traced_memory()[1]
|
|
181
|
+
finally:
|
|
182
|
+
tracemalloc.stop()
|
|
183
|
+
try:
|
|
184
|
+
# pickle.dumps would allocate the whole payload again, and more while growing.
|
|
185
|
+
assert peak < MiB
|
|
186
|
+
assert deserialize(info) == original
|
|
187
|
+
finally:
|
|
188
|
+
release_shared_memory(handle)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class ResizesEarlierPayload:
|
|
192
|
+
def __init__(self, payload):
|
|
193
|
+
self.payload = payload
|
|
194
|
+
|
|
195
|
+
def __reduce__(self):
|
|
196
|
+
self.payload.extend(b"more")
|
|
197
|
+
return int, ()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_bytearray_cannot_be_resized_before_it_is_copied():
|
|
201
|
+
# A resize after pickle wrote the length header would corrupt the stream.
|
|
202
|
+
payload = bytearray(LARGE)
|
|
203
|
+
with pytest.raises(BufferError):
|
|
204
|
+
serialize([payload, ResizesEarlierPayload(payload)])
|
|
163
205
|
payload.extend(b"more")
|
|
206
|
+
assert len(payload) == LARGE + 4
|
|
164
207
|
|
|
165
208
|
|
|
166
|
-
|
|
167
|
-
payload
|
|
209
|
+
@pytest.mark.parametrize(
|
|
210
|
+
("payload", "wrap"),
|
|
211
|
+
[
|
|
212
|
+
pytest.param(bytearray(b"data"), FailsAfterBuffer, id="out-of-band"),
|
|
213
|
+
pytest.param(bytearray(LARGE), lambda payload: [payload, Unpicklable()], id="in-band"),
|
|
214
|
+
],
|
|
215
|
+
)
|
|
216
|
+
def test_pickle_failure_releases_exported_views(payload, wrap):
|
|
217
|
+
with pytest.raises(ValueError, match="Cannot serialize"):
|
|
218
|
+
serialize(wrap(payload))
|
|
219
|
+
# Resizing would raise BufferError if a view of the payload were retained.
|
|
220
|
+
payload.extend(b"more")
|
|
221
|
+
|
|
168
222
|
|
|
223
|
+
@pytest.mark.parametrize(
|
|
224
|
+
("payload", "wrap"),
|
|
225
|
+
[
|
|
226
|
+
pytest.param(bytearray(b"data"), Buffered, id="out-of-band"),
|
|
227
|
+
pytest.param(bytearray(LARGE), list, id="in-band"),
|
|
228
|
+
],
|
|
229
|
+
)
|
|
230
|
+
def test_allocation_failure_releases_exported_views(monkeypatch, payload, wrap):
|
|
169
231
|
def fail_allocation(**kwargs):
|
|
170
232
|
raise MemoryError("No shared memory")
|
|
171
233
|
|
|
172
234
|
monkeypatch.setattr(convert, "SharedMemory", fail_allocation)
|
|
173
235
|
with pytest.raises(MemoryError):
|
|
174
|
-
serialize(
|
|
236
|
+
serialize(wrap(payload))
|
|
175
237
|
payload.extend(b"more")
|
|
176
238
|
|
|
177
239
|
|
|
240
|
+
def test_success_releases_exported_views():
|
|
241
|
+
payload = bytearray(LARGE)
|
|
242
|
+
handle, info = serialize({"payload": payload})
|
|
243
|
+
try:
|
|
244
|
+
payload.extend(b"more")
|
|
245
|
+
assert deserialize(info) == {"payload": bytearray(LARGE)}
|
|
246
|
+
finally:
|
|
247
|
+
release_shared_memory(handle)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_payload_spanning_many_pickle_frames_round_trips():
|
|
251
|
+
original = {"numbers": list(range(200_000)), "blob": b"x" * LARGE, "text": "y" * LARGE}
|
|
252
|
+
handle, info = serialize(original)
|
|
253
|
+
try:
|
|
254
|
+
assert deserialize(info) == original
|
|
255
|
+
finally:
|
|
256
|
+
release_shared_memory(handle)
|
|
257
|
+
|
|
258
|
+
|
|
178
259
|
def test_copy_failure_unlinks_new_allocation(monkeypatch):
|
|
179
260
|
created = []
|
|
180
261
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{py_sharedmemory-4.2.0 → py_sharedmemory-4.2.1}/py_sharedmemory.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|