| 8 | |
| 9 | |
| 10 | class MultiProcessAdapter(logging.LoggerAdapter): |
| 11 | |
| 12 | @staticmethod |
| 13 | def _should_log(main_process_only): |
| 14 | """Check if log should be performed.""" |
| 15 | if not dist.is_available() or not dist.is_initialized(): |
| 16 | is_main_process = True |
| 17 | else: |
| 18 | is_main_process = dist.get_rank() == 0 |
| 19 | return not main_process_only or (main_process_only and is_main_process) |
| 20 | |
| 21 | def log(self, level, msg, *args, **kwargs): |
| 22 | """Delegates logger call after checking if we should log. |
| 23 | |
| 24 | Accepts a new kwarg of `main_process_only`, which will dictate whether it will be logged across all processes |
| 25 | or only the main executed one. Default is `True` if not passed |
| 26 | |
| 27 | Also accepts "in_order", which if `True` makes the processes log one by one, in order. This is much easier to |
| 28 | read, but comes at the cost of sometimes needing to wait for the other processes. Default is `False` to not |
| 29 | break with the previous behavior. |
| 30 | |
| 31 | `in_order` is ignored if `main_process_only` is passed. |
| 32 | """ |
| 33 | main_process_only = kwargs.pop('main_process_only', True) |
| 34 | in_order = kwargs.pop('in_order', False) |
| 35 | # set `stacklevel` to exclude ourself in `Logger.findCaller()` while respecting user's choice |
| 36 | kwargs.setdefault('stacklevel', 2) |
| 37 | |
| 38 | if self.isEnabledFor(level): |
| 39 | if self._should_log(main_process_only): |
| 40 | msg, kwargs = self.process(msg, kwargs) |
| 41 | self.logger.log(level, msg, *args, **kwargs) |
| 42 | |
| 43 | elif in_order: |
| 44 | if not dist.is_available() or not dist.is_initialized(): |
| 45 | msg, kwargs = self.process(msg, kwargs) |
| 46 | self.logger.log(level, msg, *args, **kwargs) |
| 47 | else: |
| 48 | rank = dist.get_rank() |
| 49 | num_processes = dist.get_world_size() |
| 50 | for i in range(num_processes): |
| 51 | if i == rank: |
| 52 | msg, kwargs = self.process(msg, kwargs) |
| 53 | self.logger.log(level, msg, *args, **kwargs) |
| 54 | dist.barrier() |
| 55 | |
| 56 | @functools.lru_cache(None) |
| 57 | def warning_once(self, *args, **kwargs): |
| 58 | """This method is identical to `logger.warning()`, but will emit the |
| 59 | warning with the same message only once. |
| 60 | |
| 61 | Note: The cache is for the function arguments, so 2 different callers using the same arguments will hit the |
| 62 | cache. The assumption here is that all warning messages are unique across the code. If they aren't then need to |
| 63 | switch to another type of cache that includes the caller frame information in the hashing function. |
| 64 | """ |
| 65 | self.warning(*args, **kwargs) |
| 66 | |
| 67 | |