项目文件夹

文件
2026-07-13 13:22:34 +08:00

217 行
9.4 KiB
Python

"""Class for monitoring system stats."""
import logging
import threading
from mlflow.environment_variables import (
MLFLOW_SYSTEM_METRICS_NODE_ID,
MLFLOW_SYSTEM_METRICS_SAMPLES_BEFORE_LOGGING,
MLFLOW_SYSTEM_METRICS_SAMPLING_INTERVAL,
)
from mlflow.exceptions import MlflowException
from mlflow.system_metrics.metrics.base_metrics_monitor import BaseMetricsMonitor
from mlflow.system_metrics.metrics.cpu_monitor import CPUMonitor
from mlflow.system_metrics.metrics.disk_monitor import DiskMonitor
from mlflow.system_metrics.metrics.gpu_monitor import GPUMonitor
from mlflow.system_metrics.metrics.network_monitor import NetworkMonitor
from mlflow.system_metrics.metrics.rocm_monitor import ROCMMonitor
_logger = logging.getLogger(__name__)
class SystemMetricsMonitor:
"""Class for monitoring system stats.
This class is used for pulling system metrics and logging them to MLflow. Calling `start()` will
spawn a thread that logs system metrics periodically. Calling `finish()` will stop the thread.
Logging is done on a different frequency from pulling metrics, so that the metrics are
aggregated over the period. Users can change the logging frequency by setting
`MLFLOW_SYSTEM_METRICS_SAMPLING_INTERVAL` and `MLFLOW_SYSTEM_METRICS_SAMPLES_BEFORE_LOGGING`
environment variables, e.g., run `export MLFLOW_SYSTEM_METRICS_SAMPLING_INTERVAL=10` in terminal
will set the sampling interval to 10 seconds.
System metrics are logged with a prefix "system/", e.g., "system/cpu_utilization_percentage".
Args:
run_id: string, the MLflow run ID.
sampling_interval: float, default to 10. The interval (in seconds) at which to pull system
metrics. Will be overridden by `MLFLOW_SYSTEM_METRICS_SAMPLING_INTERVAL` environment
variable.
samples_before_logging: int, default to 1. The number of samples to aggregate before
logging. Will be overridden by `MLFLOW_SYSTEM_METRICS_SAMPLES_BEFORE_LOGGING`
evnironment variable.
resume_logging: bool, default to False. If True, we will resume the system metrics logging
from the `run_id`, and the first step to log will be the last step of `run_id` + 1, if
False, system metrics logging will start from step 0.
node_id: string, default to None. The node ID of the machine where the metrics are
collected. Will be overridden by `MLFLOW_SYSTEM_METRICS_NODE_ID`
evnironment variable. This is useful in multi-node training to distinguish the metrics
from different nodes. For example, if you set node_id to "node_0", the system metrics
getting logged will be of format "system/node_0/cpu_utilization_percentage".
tracking_uri: string, default to None. The tracking URI of the MLflow server, or `None` to
use whatever is set via `mlflow.set_tracking_uri()`.
"""
def __init__(
self,
run_id,
sampling_interval=10,
samples_before_logging=1,
resume_logging=False,
node_id=None,
tracking_uri=None,
):
from mlflow.tracking import get_tracking_uri
from mlflow.utils.autologging_utils import BatchMetricsLogger
# Instantiate default monitors.
self.monitors = [CPUMonitor(), DiskMonitor(), NetworkMonitor()]
if gpu_monitor := self._initialize_gpu_monitor():
self.monitors.append(gpu_monitor)
self.sampling_interval = MLFLOW_SYSTEM_METRICS_SAMPLING_INTERVAL.get() or sampling_interval
self.samples_before_logging = (
MLFLOW_SYSTEM_METRICS_SAMPLES_BEFORE_LOGGING.get() or samples_before_logging
)
self._run_id = run_id
self._shutdown_event = threading.Event()
self._process = None
self._metrics_prefix = "system/"
self.node_id = MLFLOW_SYSTEM_METRICS_NODE_ID.get() or node_id
self._tracking_uri = tracking_uri or get_tracking_uri()
self._logging_step = self._get_next_logging_step(run_id) if resume_logging else 0
self.mlflow_logger = BatchMetricsLogger(self._run_id, tracking_uri=self._tracking_uri)
def _get_next_logging_step(self, run_id):
from mlflow.tracking.client import MlflowClient
client = MlflowClient(self._tracking_uri)
try:
run = client.get_run(run_id)
except MlflowException:
return 0
system_metric_name = None
for metric_name in run.data.metrics.keys():
if metric_name.startswith(self._metrics_prefix):
system_metric_name = metric_name
break
if system_metric_name is None:
return 0
metric_history = client.get_metric_history(run_id, system_metric_name)
return metric_history[-1].step + 1
def start(self):
"""Start monitoring system metrics."""
try:
self._process = threading.Thread(
target=self.monitor,
daemon=True,
name="SystemMetricsMonitor",
)
self._process.start()
_logger.info("Started monitoring system metrics.")
except Exception as e:
_logger.warning(f"Failed to start monitoring system metrics: {e}")
self._process = None
def monitor(self):
"""Main monitoring loop, which consistently collect and log system metrics."""
from mlflow.tracking.client import MlflowClient
_logger.debug(
f"Monitor loop started (interval={self.sampling_interval}s, "
f"samples={self.samples_before_logging})"
)
while not self._shutdown_event.is_set():
for sample_idx in range(self.samples_before_logging):
self.collect_metrics()
_logger.debug(f"Collected sample {sample_idx + 1}/{self.samples_before_logging}")
self._shutdown_event.wait(self.sampling_interval)
try:
# Get the MLflow run to check if the run is not RUNNING.
run = MlflowClient(self._tracking_uri).get_run(self._run_id)
except Exception as e:
_logger.warning(f"Failed to get mlflow run: {e}.")
return
if run.info.status != "RUNNING" or self._shutdown_event.is_set():
# If the mlflow run is terminated or receives the shutdown signal, stop
# monitoring.
_logger.debug(f"Monitor loop exiting (status={run.info.status})")
return
metrics = self.aggregate_metrics()
_logger.debug(f"Aggregated {len(metrics)} metrics")
try:
self.publish_metrics(metrics)
_logger.debug(
f"Queued metrics at step {self._logging_step - 1} "
f"(training_time={self.mlflow_logger.total_training_time:.3f}s, "
f"log_batch_time={self.mlflow_logger.total_log_batch_time:.3f}s, "
f"batch_size={len(self.mlflow_logger.data)})"
)
except Exception as e:
_logger.warning(
f"Failed to log system metrics: {e}, this is expected if the experiment/run is "
"already terminated."
)
return
def collect_metrics(self):
"""Collect system metrics."""
metrics = {}
for monitor in self.monitors:
monitor.collect_metrics()
metrics.update(monitor._metrics)
return metrics
def aggregate_metrics(self):
"""Aggregate collected metrics."""
metrics = {}
for monitor in self.monitors:
metrics.update(monitor.aggregate_metrics())
return metrics
def publish_metrics(self, metrics):
"""Log collected metrics to MLflow."""
# Add prefix "system/" to the metrics name for grouping. If `self.node_id` is not None, also
# add it to the metrics name.
prefix = self._metrics_prefix + (self.node_id + "/" if self.node_id else "")
metrics = {prefix + k: v for k, v in metrics.items()}
self.mlflow_logger.record_metrics(metrics, self._logging_step)
self._logging_step += 1
for monitor in self.monitors:
monitor.clear_metrics()
def finish(self):
"""Stop monitoring system metrics."""
if self._process is None:
return
_logger.info("Stopping system metrics monitoring...")
self._shutdown_event.set()
try:
self._process.join()
_logger.debug(f"Flushing {len(self.mlflow_logger.data)} metrics")
self.mlflow_logger.flush()
_logger.info("Successfully terminated system metrics monitoring!")
except Exception as e:
_logger.error(f"Error terminating system metrics monitoring process: {e}.")
self._process = None
def _initialize_gpu_monitor(self) -> BaseMetricsMonitor | None:
# NVIDIA GPU
try:
return GPUMonitor()
except Exception:
_logger.debug("Failed to initialize GPU monitor for NVIDIA GPU.", exc_info=True)
# Falling back to pyrocml (AMD/HIP GPU)
try:
return ROCMMonitor()
except Exception:
_logger.debug("Failed to initialize GPU monitor for AMD/HIP GPU.", exc_info=True)
_logger.info("Skip logging GPU metrics. Set logger level to DEBUG for more details.")
return None