Python Shared Memory Multiprocessing: Batch Features Without Pickle Blowups

Python Shared Memory Multiprocessing: Batch Features Without Pickle Blowups

Feature jobs that Pool.map multi-gigabyte NumPy arrays die on pickle: every worker gets a full copy, RSS explodes, and the parent stalls serializing. The unfair advantage is multiprocessing.shared_memory (or RawArray) with explicit lifetime and dtype contracts so workers see the same bytes without a second heap.

⚡ TL;DR: Put dense float32/int64 matrices in SharedMemory; pass only (name, shape, dtype) handles; never pickle the array body; unlink aggressively; pin worker count to cores × memory budget. Pair with NumPy Operator Fusion and Python Prefork Pitfalls.

Why pickle melts Batch

# ❌ Classic blow-up — each task pickles X
from multiprocessing import Pool
import numpy as np

def score_row(i_x):
    i, x = i_x
    return float(x[i].sum())

X = np.random.rand(2_000_000, 128).astype(np.float32)  # ~1 GB
with Pool(16) as pool:
    # pickle copies X into every task payload → disaster
    pool.map(score_row, [(i, X) for i in range(0, len(X), 10_000)])

SharedMemory pattern that survives

# ✅ Parent allocates once; workers attach by name
from multiprocessing import Pool, shared_memory
import numpy as np

def _init_worker(shm_name: str, shape: tuple[int, ...], dtype: str):
    global SHM, ARR
    SHM = shared_memory.SharedMemory(name=shm_name)
    ARR = np.ndarray(shape, dtype=np.dtype(dtype), buffer=SHM.buf)

def score_block(start_end: tuple[int, int]) -> float:
    start, end = start_end
    # ✅ read-only view into shared buffer — no copy
    return float(ARR[start:end].sum())

def run(X: np.ndarray, n_workers: int = 8) -> list[float]:
    shm = shared_memory.SharedMemory(create=True, size=X.nbytes)
    arr = np.ndarray(X.shape, dtype=X.dtype, buffer=shm.buf)
    arr[:] = X  # one copy into SHM
    blocks = [(i, min(i + 50_000, len(X))) for i in range(0, len(X), 50_000)]
    try:
        with Pool(
            n_workers,
            initializer=_init_worker,
            initargs=(shm.name, X.shape, X.dtype.str),
        ) as pool:
            return pool.map(score_block, blocks)
    finally:
        shm.close()
        shm.unlink()  # ✅ always unlink or you leak /dev/shm

Ownership and failure modes

Failure Symptom Fix
Forgot unlink /dev/shm fills; new jobs fail try/finally + job teardown hook
Wrote from many workers Data races / NaNs Single-writer or partition rows
Passed ndarray in args Silent pickle of view metadata + copy Pass (name, shape, dtype) only
Fork without re-attach Child sees closed buffer Use spawn + initializer
# ✅ Partition writes: each worker owns a disjoint row range
def fill_features(start_end: tuple[int, int]) -> None:
    start, end = start_end
    ARR[start:end, 0] = ARR[start:end, 1:5].mean(axis=1)

❌ Sharing Python list/dict graphs via managers for numeric work — you reintroduce serialization and lock contention.

AWS Batch sizing

Size tasks so X.nbytes + worker_overhead * n fits in the instance. Prefer one shared matrix per job over shipping shards through S3 for every subprocess when the matrix already fits in RAM.

Closing checklist

✅ Dos
– ✅ Pass SHM name + shape + dtype only
– ✅ unlink in finally and on SIGTERM handlers
– ✅ Use np.ndarray(..., buffer=shm.buf) views
– ✅ Prefer spawn start method for clarity on Linux containers
– ✅ Cap workers by memory, not just vCPU count

❌ Don’ts
– ❌ Don’t pickle multi-GB arrays into Pool.map args
– ❌ Don’t mutate overlapping ranges without barriers
– ❌ Don’t leave SHM segments for “debug later”
– ❌ Don’t mix managed dicts with dense numeric pipelines

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply