Skip to main content

Efficient NumPy array sharing across processes using shared memory

Project description

Shared Memory Array

A lightweight Python library for efficiently sharing NumPy arrays across multiple processes using shared memory, eliminating expensive data copying in parallel workloads.

The Problem

When processing large NumPy arrays in parallel, you typically face these trade-offs:

  • Sequential operations: Simple but slow, defeats the purpose of parallelization
  • Reloading arrays per process: Expensive in both memory and I/O time
  • Passing arrays to workers: Each process gets a copy via pickle, multiplying memory usage
  • Multi-threading with GIL: Limited by Python's Global Interpreter Lock, unpredictable performance
  • This library: Share arrays in memory across processes with zero copying :+1:

Installation

pip install shared-memory-array

Or install from source:

git clone https://github.com/PCMGF-Limited/shared-memory-array.git
cd shared-memory-array
pip install -e .

Quick Start

Basic Example: Parallel Row Summation

import numpy as np
from multiprocessing import Pool
from multiprocessing.managers import SharedMemoryManager
from shared_memory_array import SharedMemoryArray


def sum_row(args):
    """Sum a single row - runs in parallel process."""
    shared_array, row_idx = args
    arr = shared_array.as_array()
    return arr[row_idx].sum()


# Create large array
data = np.random.rand(1000, 1000)

# Share it across processes
with SharedMemoryManager() as manager:
    shared = SharedMemoryArray.copy(manager, data)

    with Pool() as pool:
        # Each worker accesses the SAME memory - no copying!
        results = pool.map(sum_row, [(shared, i) for i in range(1000)])

    print(f"Total sum: {sum(results)}")

With Automatic Cleanup (Context Manager)

with SharedMemoryManager() as manager:
    # Automatically closes/unlinks on exit
    with SharedArray.allocate(manager, shape=(10000, 10000), dtype='float64').managed() as shared:
        arr = shared.as_array()
        arr[:] = 42.0
        # Process array in parallel...
    # Memory automatically cleaned up here

Attach from Multiple Processes

# Process 1: Create and populate
with SharedMemoryManager() as manager:
    owner = SharedArray.allocate(manager, shape=(1000, 1000), dtype='float64')
    arr = owner.as_array()
    arr[:] = np.random.rand(1000, 1000)
    
    name = owner.name  # Share this name with other processes
    
    # Process 2: Attach by name
    client = SharedArray.attach(name=name, shape=(1000, 1000), dtype='float64')
    client_arr = client.as_array()
    # Both processes see the same data!
    
    client.close()  # Non-owner: closes handle but doesn't unlink
    owner.close()   # Owner: closes and unlinks memory

Core API

Creating Shared Arrays

# Allocate new shared array
sa = SharedArray.allocate(manager, shape=(100, 200), dtype='float64')

# Allocate matching existing array
template = np.zeros((50, 50), dtype='int32')
sa = SharedArray.allocate_like(manager, template)

# Copy existing array into shared memory
data = np.random.rand(1000, 1000)
sa = SharedArray.copy(manager, data)

Accessing Arrays

# Get numpy array view (no copying!)
arr = sa.as_array()
arr[0, 0] = 42.0  # Modifications visible to all processes

# Access metadata
print(sa.name)   # Shared memory block name
print(sa.shape)  # Array shape
print(sa.dtype)  # Array dtype
print(sa.owner)  # Whether this instance owns the memory

Cleanup

# Manual cleanup
sa.close()  # Closes handle; unlinks if owner=True

# Automatic cleanup with context manager
with sa.managed() as managed_sa:
    arr = managed_sa.as_array()
    # Use array...
# Automatically closed here

Attaching to Existing Memory

# Attach to shared memory by name (e.g., from another process)
client = SharedArray.attach(
    name="psm_12345",
    shape=(100, 100),
    dtype='float64'
)
# owner=False by default (won't unlink on close)

Safe vs Unsafe Methods

For performance-critical code where you control all inputs, use _unsafe variants that skip validation:

# Safe (default): validates shape, dtype, buffer sizes
sa_safe = SharedArray.allocate(manager, shape=(100,), dtype='float64')

# Unsafe: skips validation for performance
sa_unsafe = SharedArray.allocate_unsafe(manager, shape=(100,), dtype='float64')

# Also available: copy_unsafe, allocate_like_unsafe, attach_unsafe

:warning: Warning: Unsafe methods can cause segfaults or data corruption if used incorrectly. Only use when you've validated inputs yourself.

Features

  • Zero-copy sharing: Multiple processes access the same memory
  • NumPy integration: Returns standard numpy arrays
  • Context managers: Automatic cleanup on exit
  • Type safety: Full validation of shapes, dtypes, and buffer sizes
  • Non-contiguous arrays: Handles slices and transposed arrays correctly
  • Ownership tracking: Distinguishes between owners and clients
  • Performance mode: Unsafe variants skip validation overhead

Performance Comparison

import time
import numpy as np
from multiprocessing import Pool
from shared_memory_array import SharedMemoryArray

data = np.random.rand(10000, 10000)  # ~800MB array


# Traditional approach: pickle copies data to each worker
def traditional():
    with Pool() as pool:
        results = pool.map(process_chunk, [(data, i) for i in range(100)])
    # Total memory: 800MB × num_workers 😱


# With shmanager: zero copying
def with_shmanager():
    with SharedMemoryManager() as manager:
        shared = SharedMemoryArray.copy(manager, data)
        with Pool() as pool:
            results = pool.map(process_chunk_shared, [(shared, i) for i in range(100)])
    # Total memory: 800MB regardless of workers 🎉

Complete Example: Image Processing Pipeline

import numpy as np
from multiprocessing import Pool
from multiprocessing.managers import SharedMemoryManager
from shared_memory_array import SharedMemoryArray
from tqdm import tqdm


def process_tile(args):
    """Process one tile of a large image."""
    shared_img, y_start, y_end = args
    img = shared_img.as_array()
    tile = img[y_start:y_end, :]
    # Apply expensive operation
    return tile.mean(), tile.std()


def parallel_image_analysis(image, n_tiles=10):
    """Analyze large image in parallel without copying."""
    tile_height = image.shape[0] // n_tiles

    with SharedMemoryManager() as manager:
        # Share image once
        shared = SharedMemoryArray.copy(manager, image)

        # Create tile boundaries
        tiles = [(shared, i * tile_height, (i + 1) * tile_height)
                 for i in range(n_tiles)]

        # Process in parallel
        with Pool() as pool:
            results = list(tqdm(pool.imap(process_tile, tiles),
                                total=n_tiles))

        return results


# Usage
large_image = np.random.rand(10000, 10000)
stats = parallel_image_analysis(large_image)
print(f"Processed {len(stats)} tiles")

Notes

Shared memory persists until explicitly unlinked!

Always use context managers or call .close() to prevent memory leaks!

Requirements

  • Python 3.8+
  • NumPy
  • multiprocessing (standard library)

Contributing

Contributions welcome! Please see CONTRIBUTING.md for guidelines.

License

MIT License - see LICENSE for details.

Credits

Built on Python's multiprocessing.shared_memory module (Python 3.8+).


PCMGF Limited

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

shared_memory_array-0.1.1.tar.gz (10.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

shared_memory_array-0.1.1-py3-none-any.whl (9.3 kB view details)

Uploaded Python 3

File details

Details for the file shared_memory_array-0.1.1.tar.gz.

File metadata

  • Download URL: shared_memory_array-0.1.1.tar.gz
  • Upload date:
  • Size: 10.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for shared_memory_array-0.1.1.tar.gz
Algorithm Hash digest
SHA256 1baee94c2c2dd4d2031f04a1cf02d8ed4e7158e0c6a02ff24eb0d4f4d1b3c7f9
MD5 345f30c3a5f4e24dffe2e793f803d847
BLAKE2b-256 9663bebc8b6f0b280b25a3f87737726dd1bc97e7b23c00cd9e00ea446d9cf307

See more details on using hashes here.

Provenance

The following attestation bundles were made for shared_memory_array-0.1.1.tar.gz:

Publisher: publish_pypi_release.yml on PCMGF-Limited/shared-memory-array

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file shared_memory_array-0.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for shared_memory_array-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 06d79ed1651e65e89cf9a2c71868894e3d8ac2adbb473da967d221dfcf2e84a4
MD5 ee30338f7b4632d17452732d3ff6d912
BLAKE2b-256 886ce3316b35c50d6154b27c0a25e2afd0f04e39c1d4b5e4d86982e1633c9fae

See more details on using hashes here.

Provenance

The following attestation bundles were made for shared_memory_array-0.1.1-py3-none-any.whl:

Publisher: publish_pypi_release.yml on PCMGF-Limited/shared-memory-array

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page