Question: Trying to learn about the built-in multiprocessing and Pytorch's multiprocessing packages, I have observed a different behavior between both. I find this to be strange since Pytorch's package is fully-compatible with the built-in package. Concretely, I'm refering to the way variables are shared between processes. In Pytorch, tensor's are moved to shared_memory via the inplace operation share_memory_(). On the other hand, we can get the same result with the built-in package by using the shared_memory module. The difference between both that I'm struggling to understand is that, with the built-in version, we have to explicitely access the shared memory-block inside the launched process. However, we don't need to do that with the Pytorch version. Here is a Pytorch's toy example showing this: import time import torch # the same behavior happens when importing: # import multiprocessing as mp import torch.multiprocessing as mp def get_time(s): return round(time.time() - s, 1) def foo(a): # wait ~1sec to print the value of the tensor. time.sleep(1.0) with lock: #------------------------------------------------------------------- # WITHOUT explicitely accessing the shared memory block, we can observe # that the tensor has changed: #------------------------------------------------------------------- print(f"{__name__}\t{get_time(s)}\t\t{a}") # global variables. lock = mp.Lock() s = time.time() if __name__ == '__main__': print("Module\t\tTime\t\tValue") print("-"*50) # create tensor and assign it to shared memory. a = torch.zeros(2).share_memory_() print(f"{__name__}\t{get_time(s)}\t\t{a}") # start child process. p0 = mp.Process(target=foo, args=(a,)) p0.start() # modify the value of the tensor after ~0.5sec. time.sleep(0.5) with lock: a[0] = 1.0 print(f"{__name__}\t{get_time(s)}\t\t{a}") time.sleep(1.5) p0.join()  which outputs (as expected): Module Time Value -------------------------------------------------- __main__ 0.0 tensor([0., 0.]) __main__ 0.5 tensor([1., 0.]) __mp_main__ 1.0 tensor([1., 0.])  And here is a toy example with the built-in package: import time import multiprocessing as mp from multiprocessing import shared_memory import numpy as np def get_time(s): return round(time.time() - s, 1) def foo(shm_name, shape, type_): #------------------------------------------------------------------- # WE NEED TO explicitely access the shared memory block to observe # that the array has changed: #------------------------------------------------------------------- existing_shm = shared_memory.SharedMemory(name=shm_name) a = np.ndarray(shape, type_, buffer=existing_shm.buf) # wait ~1sec to print the value. time.sleep(1.0) with lock: print(f"{__name__}\t{get_time(s)}\t\t{a}") # global variables. lock = mp.Lock() s = time.time() if __name__ == '__main__': print("Module\t\tTime\t\tValue") print("-"*35) # create numpy array and shared memory block. a = np.zeros(2,) shm = shared_memory.SharedMemory(create=True, size=a.nbytes) a_shared = np.ndarray(a.shape, a.dtype, buffer=shm.buf) a_shared[:] = a[:] print(f"{__name__}\t{get_time(s)}\t\t{a_shared}") # start child process. p0 = mp.Process(target=foo, args=(shm.name, a.shape, a.dtype)) p0.start() # modify the value of the vaue after ~0.5sec. time.sleep(0.5) with lock: a_shared[0] = 1.0 print(f"{__name__}\t{get_time(s)}\t\t{a_shared}") time.sleep(1.5) p0.join()  which equivalently outputs, as expected: Module Time Value ----------------------------------- __main__ 0.0 [0. 0.] __main__ 0.5 [1. 0.] __mp_main__ 1.0 [1. 0.]  So what I'm strugging to understand is why we don't need to follow the same steps in both versions, built-in and Pytorch's, i.e. how Pytorch is able to avoid the need to explicitely access the shared memory-block? P.S. I'm using a Windows OS and Python 3.9

Answer: pytorch has a simple wrapper around shared memory, python's shared memory module is only a wrapper around the underlying OS dependent functions. the way it can be done is that you don't serialize the array or the shared memory themselves, and only serialize what's needed to create them by using the __getstate__ and __setstate__ methods from the docs, so that your object acts as both a proxy and a container at the same time. the following bar class can double for a proxy and a container this way, which is useful if the user shouldn't have to worry about the shared memory part. import time import multiprocessing as mp from multiprocessing import shared_memory import numpy as np class bar: def __init__(self): self._size = 10 self._type = np.uint8 self.shm = shared_memory.SharedMemory(create=True, size=self._size) self._mem_name = self.shm.name self.arr = np.ndarray([self._size], self._type, buffer=self.shm.buf) def __getstate__(self): """Return state values to be pickled.""" return (self._mem_name, self._size, self._type) def __setstate__(self, state): """Restore state from the unpickled state values.""" self._mem_name, self._size, self._type = state self.shm = shared_memory.SharedMemory(self._mem_name) self.arr = np.ndarray([self._size], self._type, buffer=self.shm.buf) def get_time(s): return round(time.time() - s, 1) def foo(shm, lock): # ------------------------------------------------------------------- # without explicitely access the shared memory block we observe # that the array has changed: # ------------------------------------------------------------------- a = shm # wait ~1sec to print the value. time.sleep(1.0) with lock: print(f"{__name__}\t{get_time(s)}\t\t{a.arr}") # global variables. s = time.time() if __name__ == '__main__': lock = mp.Lock() # to work on windows/mac. print("Module\t\tTime\t\tValue") print("-" * 35) # create numpy array and shared memory block. a = bar() print(f"{__name__}\t{get_time(s)}\t\t{a.arr}") # start child process. p0 = mp.Process(target=foo, args=(a, lock)) p0.start() # modify the value of the vaue after ~0.5sec. time.sleep(0.5) with lock: a.arr[0] = 1.0 print(f"{__name__}\t{get_time(s)}\t\t{a.arr}") time.sleep(1.5) p0.join()  python just makes it much easier to hide such details inside the class without bothering the user with such details. Edit: i wish they'd make locks non-inheritable so your code can raise an error on the lock, instead you'll find out one day that it doesn't actually lock ... After it crashes your application in production.