Source code for pimm.engines.hooks.resources
"""Garbage collection and resource utilization hooks."""
import gc
import torch
import pimm.utils.comm as comm
from pimm.observability import structured_logger as sl
from pimm.utils.comm import is_main_process
from .builder import HOOKS
from .default import HookBase
[docs]
@HOOKS.register_module()
class GarbageHandler(HookBase):
"""Control Python garbage collection (and CUDA cache) on a step cadence.
In ``before_train`` it optionally disables automatic garbage collection
(``disable_auto``) to avoid unpredictable GC pauses during training. It
resets a per-epoch counter in ``before_epoch`` and, in ``after_step``, runs
``gc.collect()`` every ``interval`` steps (also emptying the CUDA cache when
``empty_cache=True``). ``after_train`` performs a final collect and CUDA
cache empty. Registered as ``GarbageHandler``.
Args:
interval (int): Run a manual ``gc.collect()`` every this many steps.
Defaults to ``150``.
disable_auto (bool): Disable Python's automatic GC at train start so
collection happens only at the controlled interval. Defaults to
``True``.
empty_cache (bool): Also call ``torch.cuda.empty_cache()`` at each
interval collect. Defaults to ``False``.
Example:
Add to ``cfg.hooks``; it takes over garbage collection so it happens on a
fixed cadence rather than unpredictably mid-step:
.. code-block:: python
hooks = [dict(type="GarbageHandler", interval=150)]
# → disables automatic GC in before_train and runs gc.collect() every
# 150 steps (also torch.cuda.empty_cache() when empty_cache=True);
# final collect + cache empty in after_train
"""
def __init__(self, interval=150, disable_auto=True, empty_cache=False):
self.interval = interval
self.disable_auto = disable_auto
self.empty_cache = empty_cache
self.iter = 1
[docs]
def before_train(self):
if self.disable_auto:
gc.disable()
self.trainer.logger.info("Disable automatic garbage collection")
[docs]
def before_epoch(self):
self.iter = 1
[docs]
def after_step(self):
if self.iter % self.interval == 0:
sl.add_step_tag("garbage_collection")
with sl.log_trace_span("garbage_collection"):
gc.collect()
if self.empty_cache:
torch.cuda.empty_cache()
self.trainer.logger.info("Garbage collected")
self.iter += 1
[docs]
def after_train(self):
with sl.log_trace_span("garbage_collection"):
gc.collect()
torch.cuda.empty_cache()
[docs]
@HOOKS.register_module()
class ResourceUtilizationLogger(HookBase):
"""Log GPU and CPU/RAM utilization to the writer over the course of training.
In ``before_train`` it probes optional dependencies (``psutil`` for CPU/RAM,
``pynvml`` for system-wide GPU stats), warning/falling back to
``torch.cuda`` when unavailable. Runs in ``after_step`` every
``log_frequency`` steps on rank 0: collects GPU memory (and, system-wide,
utilization %) plus CPU and system/process memory metrics and writes them to
the writer under ``{prefix}/...``. ``after_train`` shuts down NVML if it was
initialized. Registered as ``ResourceUtilizationLogger``.
Args:
log_frequency (int): Log metrics every this many steps. Defaults to
``10``.
prefix (str): Namespace prefix for the logged keys. Defaults to
``"resources"``.
log_per_gpu (bool): In system-wide mode, log metrics for every visible
GPU instead of just the local one. Defaults to ``False``.
log_cpu (bool): Log CPU utilization metrics. Defaults to ``True``.
log_system_memory (bool): Log RAM usage metrics. Defaults to ``True``.
per_process (bool): If ``True``, report only this process's CPU/RAM and
``torch.cuda`` GPU memory (recommended on shared nodes); if
``False``, report system-wide metrics (using ``pynvml`` for GPU when
available). Defaults to ``True``.
Note:
Logs on rank 0 only and no-ops when ``trainer.writer`` is absent/``None``.
``per_process=True`` ignores ``log_per_gpu``/``pynvml`` and uses
``torch.cuda`` memory counters for this process.
Example:
Add to ``cfg.hooks``; every ``log_frequency`` steps (rank 0) it writes
GPU/CPU/RAM utilization to the writer:
.. code-block:: python
hooks = [dict(type="ResourceUtilizationLogger", log_frequency=50)]
# → every 50 steps writes "resources/gpu_memory_allocated_gb",
# "resources/gpu_memory_reserved_gb", "resources/process_cpu_percent",
# "resources/process_rss_gb", … to the writer (per-process by default)
"""
def __init__(
self,
log_frequency=10,
prefix="resources",
log_per_gpu=False,
log_cpu=True,
log_system_memory=True,
per_process=True,
):
self.log_frequency = log_frequency
self.prefix = prefix
self.log_per_gpu = log_per_gpu
self.log_cpu = log_cpu
self.log_system_memory = log_system_memory
self.per_process = per_process
self.step_count = 0
self._pynvml_available = False
self._psutil_available = False
self._nvml_initialized = False
self._process = None
[docs]
def before_train(self):
# try importing optional dependencies
try:
import psutil
self._psutil_available = True
if self.per_process:
self._process = psutil.Process()
# prime cpu_percent so the first real call returns meaningful data
self._process.cpu_percent(interval=None)
except ImportError:
self.trainer.logger.warning(
"psutil not available - CPU metrics will not be logged"
)
try:
import pynvml
pynvml.nvmlInit()
self._pynvml_available = True
self._nvml_initialized = True
except (ImportError, Exception):
self.trainer.logger.info(
"pynvml not available - using torch.cuda for GPU metrics"
)
if self.per_process:
self.trainer.logger.info(
"ResourceUtilizationLogger: per_process=True - reporting "
"only this process's CPU/RAM and torch.cuda GPU memory"
)
[docs]
def after_train(self):
if self._nvml_initialized:
try:
import pynvml
pynvml.nvmlShutdown()
except Exception:
pass
def _get_gpu_metrics(self):
metrics = {}
if not torch.cuda.is_available():
return metrics
local_rank = comm.get_local_rank()
if self.per_process:
# Per-process GPU metrics via torch.cuda (only our allocations)
metrics["gpu_memory_allocated_gb"] = torch.cuda.memory_allocated(local_rank) / 1e9
metrics["gpu_memory_reserved_gb"] = torch.cuda.memory_reserved(local_rank) / 1e9
metrics["gpu_memory_max_allocated_gb"] = torch.cuda.max_memory_allocated(local_rank) / 1e9
metrics["gpu_memory_max_reserved_gb"] = torch.cuda.max_memory_reserved(local_rank) / 1e9
return metrics
# System-wide GPU metrics (original behavior)
if self._pynvml_available:
try:
import pynvml
if self.log_per_gpu:
device_count = torch.cuda.device_count()
for i in range(device_count):
handle = pynvml.nvmlDeviceGetHandleByIndex(i)
mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
util = pynvml.nvmlDeviceGetUtilizationRates(handle)
metrics[f"gpu{i}_memory_used_gb"] = mem_info.used / 1e9
metrics[f"gpu{i}_memory_total_gb"] = mem_info.total / 1e9
metrics[f"gpu{i}_memory_pct"] = 100.0 * mem_info.used / mem_info.total
metrics[f"gpu{i}_utilization_pct"] = util.gpu
else:
handle = pynvml.nvmlDeviceGetHandleByIndex(local_rank)
mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle)
util = pynvml.nvmlDeviceGetUtilizationRates(handle)
metrics["gpu_memory_used_gb"] = mem_info.used / 1e9
metrics["gpu_memory_total_gb"] = mem_info.total / 1e9
metrics["gpu_memory_pct"] = 100.0 * mem_info.used / mem_info.total
metrics["gpu_utilization_pct"] = util.gpu
except Exception:
pass
# fallback or additional torch.cuda metrics
if self.log_per_gpu:
device_count = torch.cuda.device_count()
for i in range(device_count):
if f"gpu{i}_memory_used_gb" not in metrics:
metrics[f"gpu{i}_memory_allocated_gb"] = torch.cuda.memory_allocated(i) / 1e9
metrics[f"gpu{i}_memory_reserved_gb"] = torch.cuda.memory_reserved(i) / 1e9
else:
if "gpu_memory_used_gb" not in metrics:
metrics["gpu_memory_allocated_gb"] = torch.cuda.memory_allocated(local_rank) / 1e9
metrics["gpu_memory_reserved_gb"] = torch.cuda.memory_reserved(local_rank) / 1e9
# max memory for peak tracking
metrics["gpu_memory_max_allocated_gb"] = torch.cuda.max_memory_allocated(local_rank) / 1e9
return metrics
def _get_cpu_metrics(self):
metrics = {}
if not self._psutil_available:
return metrics
import os
import psutil
if self.per_process and self._process is not None:
# Per-process metrics
if self.log_cpu:
# cpu_percent returns total across all cores for this process
# e.g. 400% means 4 cores fully used
proc_cpu = self._process.cpu_percent(interval=None)
metrics["process_cpu_percent"] = proc_cpu
metrics["process_num_threads"] = self._process.num_threads()
# Approximate number of cores used by this process
metrics["process_cpu_cores"] = proc_cpu / 100.0
if self.log_system_memory:
mem_info = self._process.memory_info()
metrics["process_rss_gb"] = mem_info.rss / 1e9
metrics["process_vms_gb"] = mem_info.vms / 1e9
return metrics
# System-wide metrics (original behavior)
if self.log_cpu:
# overall cpu percent (averaged across all cores)
metrics["cpu_percent"] = psutil.cpu_percent(interval=None)
# load average - more useful for HPC nodes
# load avg / num_cpus gives utilization ratio (>1 means oversubscribed)
load1, load5, load15 = os.getloadavg()
num_cpus = psutil.cpu_count(logical=True)
metrics["load_avg_1min"] = load1
metrics["load_avg_5min"] = load5
metrics["load_avg_15min"] = load15
metrics["load_per_cpu_1min"] = load1 / num_cpus # ~1.0 = fully utilized
metrics["num_cpus"] = num_cpus
# per-cpu utilization stats to see distribution
per_cpu = psutil.cpu_percent(interval=None, percpu=True)
if per_cpu:
metrics["cpu_max_core_pct"] = max(per_cpu)
metrics["cpu_min_core_pct"] = min(per_cpu)
active_cores = [c for c in per_cpu if c > 5.0] # cores > 5% usage
metrics["cpu_active_cores"] = len(active_cores)
if active_cores:
metrics["cpu_active_avg_pct"] = sum(active_cores) / len(active_cores)
if self.log_system_memory:
mem = psutil.virtual_memory()
metrics["system_memory_used_gb"] = mem.used / 1e9
metrics["system_memory_total_gb"] = mem.total / 1e9
metrics["system_memory_pct"] = mem.percent
return metrics
[docs]
def after_step(self):
self.step_count += 1
if self.step_count % self.log_frequency != 0:
return
if not is_main_process():
return
if not hasattr(self.trainer, 'writer') or self.trainer.writer is None:
return
current_iter = self.trainer.comm_info.get("iter", 0) + 1
current_epoch = self.trainer.epoch + 1
global_step = (current_epoch - 1) * len(self.trainer.train_loader) + current_iter
# collect metrics
metrics = {}
metrics.update(self._get_gpu_metrics())
metrics.update(self._get_cpu_metrics())
# log to writer
for key, value in metrics.items():
self.trainer.writer.add_scalar(
f"{self.prefix}/{key}",
value,
global_step
)
def __repr__(self):
return (
f"{self.__class__.__name__}("
f"log_frequency={self.log_frequency}, "
f"prefix='{self.prefix}', "
f"log_per_gpu={self.log_per_gpu}, "
f"log_cpu={self.log_cpu}, "
f"log_system_memory={self.log_system_memory}, "
f"per_process={self.per_process})"
)