This thing works
def np_after(gpu, array, num_workers):
    dataset = SharedDataset1(array)
    dataloader = torch.utils.data.DataLoader(dataset, batch_size=10, num_workers=num_workers)

    for ii, img in enumerate(dataloader):
        print(ii)
        c = img+0.1
        time.sleep(1)
    return 0

class SharedDataset2(Dataset):
    def __init__(self, shared_mem):
        super(SharedDataset2, self).__init__()
        with shared_mem.get_lock():
            self.shared_mem = np.ctypeslib.as_array(shared_mem.get_obj())

    def __len__(self):
        return 10000

    def __getitem__(self, idx):
        return torch.randn(3, 32, 32)

I think it might have to do with what Python pickles - the array/shared_mem is a SynchronizedArray object which Python knows no to pickle but share, while in the np_before function it is now an np.ndarray which Python tries to pickle.
Let me now try and fix the PIL issue. I will benchmark it to see if it actually speed up data-loading.