Source code for streaming.base.shared.barrier

# Copyright 2022-2024 MosaicML Streaming authors
# SPDX-License-Identifier: Apache-2.0

"""Barrier that lives in shared memory.

Implemented with shared array and a filelock.
"""

import os
from time import sleep
from typing import Any, Optional

import numpy as np
from filelock import FileLock

from streaming.base.constant import TICK
from streaming.base.shared.array import SharedArray

# Time out to wait before raising exception
TIMEOUT = 60


[docs] class SharedBarrier: """A barrier that works inter-process using a filelock and shared memory. We set the number of processes (and thereby initialize num_exit) on the first time this object is called. This is because the object is created in a per-rank process, and called by worker processes. Args: filelock_path (str): Path to lock file on local filesystem. shm_name (str): Shared memory object name in /dev/shm. """ def __init__(self, filelock_path: str, shm_name: str) -> None: # The lock itself is created per process by the ``lock`` property. self.filelock_path = filelock_path self._lock: Optional[FileLock] = None self._lock_pid: Optional[int] = None # Create three int32 fields in shared memory: num_enter, num_exit, flag. self._arr = SharedArray(3, np.int32, shm_name) self.num_enter = 0 self.num_exit = -1 self.flag = True @property def lock(self) -> FileLock: """Get the file lock of the current process. A FileLock holds a threading lock, so it cannot be pickled, and filelock 3.31+ refuses to acquire a lock in any process other than the one that created it. So each process creates its own lock on first use, and a lock inherited across fork is replaced. Returns: FileLock: The file lock. """ pid = os.getpid() if self._lock is None or self._lock_pid != pid: self._lock = FileLock(self.filelock_path) self._lock_pid = pid return self._lock def __getstate__(self) -> dict[str, Any]: """Get the state to pickle, which leaves out the unpicklable file lock. Returns: Dict[str, Any]: The picklable state. """ state = self.__dict__.copy() state['_lock'] = None state['_lock_pid'] = None return state @property def num_enter(self) -> int: """Get property num_enter. Returns: int: Number of processes that have entered the barrier. """ return self._arr[0] @num_enter.setter def num_enter(self, num_enter: int) -> None: """Set property num_enter. Args: num_enter (int): Number of processes that have entered the barrier. """ self._arr[0] = num_enter @property def num_exit(self) -> int: """Get property num_exit. Returns: int: Number of processes that have exited the barrier. """ return self._arr[1] @num_exit.setter def num_exit(self, num_exit: int) -> None: """Set property num_exit. Args: num_exit (int): Number of processes that have exited the barrier. """ self._arr[1] = num_exit @property def flag(self) -> bool: """Get property flag. Returns: bool: The flag value. """ return bool(self._arr[2]) @flag.setter def flag(self, flag: bool) -> None: """Set property flag. Args: flag (bool): The flag value. """ self._arr[2] = bool(flag) def __call__(self, num_procs: int) -> None: """A set number of processes enter, wait, and exit the barrier. Args: num_procs (int): How many processes are sharing this barrier. """ # Initialize num_exit to the number of processes. with self.lock: if self.num_exit == -1: self.num_exit = num_procs # If we are the first to arrive, wait for everyone to exit, then set flag to "don't go". self.lock.acquire() if not self.num_enter: self.lock.release() while self.num_exit != num_procs: sleep(TICK) self.lock.acquire() self.flag = False # Note that we entered. self.num_enter += 1 # If we are the last to arrive, reset `enter` and `exit`, and set flag to "go". if self.num_enter == num_procs: self.num_enter = 0 self.num_exit = 0 self.flag = True self.lock.release() # Everybody waits until the flag is set to "go". while not self.flag: sleep(TICK) # Note that we exited. with self.lock: self.num_exit += 1 if self.num_exit == num_procs: self.num_exit = -1