3232All entries in agent.tracking_data dict are extracted, supporting custom metrics
3333from different SKRL algorithms (PPO, SAC, TD3, DDPG, A2C, etc.).
3434
35+ **System Metrics:**
36+ Collected via psutil and pynvml with system/ prefix:
37+ - CPU: system/cpu_utilization_percentage
38+ - Memory: system/memory_used_megabytes, system/memory_percent, system/memory_available_megabytes
39+ - GPU: system/gpu_{i}_utilization_percentage, system/gpu_{i}_memory_percent, system/gpu_{i}_memory_used_megabytes, system/gpu_{i}_power_watts
40+ - Disk: system/disk_used_gigabytes, system/disk_percent, system/disk_available_gigabytes
41+
3542Metric Logging
3643--------------
3744Metrics are logged to MLflow after each agent._update() call, which is when SKRL
5764 agent=runner.agent,
5865 mlflow_module=mlflow,
5966 metric_filter=None,
67+ collect_gpu_metrics=True,
6068)
6169
6270# Monkey-patch the agent's _update method
7280import logging
7381from typing import Any , Callable , Protocol , runtime_checkable
7482
83+ from training .utils .metrics import (
84+ SystemMetricsCollector ,
85+ _STANDARD_METRIC_ATTRS ,
86+ _extract_from_tracking_data ,
87+ _extract_from_value ,
88+ )
89+
7590_LOGGER = logging .getLogger (__name__ )
7691
7792
@@ -87,161 +102,34 @@ class SkrlAgent(Protocol):
87102class MLflowModule (Protocol ):
88103 """Protocol defining the MLflow API used for logging metrics."""
89104
90- def log_metrics (self , metrics : dict [str , float ], step : int | None = None ) -> None : ...
91-
92-
93- def _is_tensor_scalar (value : Any ) -> bool :
94- return hasattr (value , "item" ) and hasattr (value , "numel" ) and value .numel () == 1
95-
96-
97- def _is_tensor_array (value : Any ) -> bool :
98- return hasattr (value , "item" ) and hasattr (value , "numel" ) and value .numel () > 1
99-
100-
101- def _is_numpy_array (value : Any ) -> bool :
102- return hasattr (value , "mean" ) and hasattr (value , "__len__" ) and len (value ) > 1
103-
104-
105- def _is_single_element_sequence (value : Any ) -> bool :
106- return hasattr (value , "__len__" ) and len (value ) == 1
107-
108-
109- def _extract_tensor_scalar (name : str , value : Any , metrics : dict [str , float ]) -> None :
110- metrics [name ] = float (value .item ())
111-
112-
113- def _extract_tensor_statistics (name : str , value : Any , metrics : dict [str , float ]) -> None :
114- if hasattr (value , "mean" ):
115- metrics [f"{ name } /mean" ] = float (value .mean ().item ())
116- if hasattr (value , "std" ):
117- metrics [f"{ name } /std" ] = float (value .std ().item ())
118- if hasattr (value , "min" ):
119- metrics [f"{ name } /min" ] = float (value .min ().item ())
120- if hasattr (value , "max" ):
121- metrics [f"{ name } /max" ] = float (value .max ().item ())
122-
123-
124- def _extract_numpy_statistics (name : str , value : Any , metrics : dict [str , float ]) -> None :
125- import numpy as np
126-
127- arr = np .asarray (value )
128- metrics [f"{ name } /mean" ] = float (np .mean (arr ))
129- metrics [f"{ name } /std" ] = float (np .std (arr ))
130- metrics [f"{ name } /min" ] = float (np .min (arr ))
131- metrics [f"{ name } /max" ] = float (np .max (arr ))
132-
133-
134- def _extract_from_value (name : str , value : int | float | Any , metrics : dict [str , float ]) -> None :
135- """Extract numeric value and add to metrics dict.
136-
137- Handles tensors, arrays, and numeric types. For tensors/arrays with multiple
138- elements, extracts mean, std, min, max statistics.
139-
140- Args:
141- name: Metric name
142- value: Value to extract (can be tensor, array, or numeric)
143- metrics: Output dictionary to populate
144- """
145- if value is None :
146- return
147-
148- try :
149- if _is_tensor_scalar (value ):
150- _extract_tensor_scalar (name , value , metrics )
151- elif _is_tensor_array (value ):
152- _extract_tensor_statistics (name , value , metrics )
153- elif hasattr (value , "item" ):
154- metrics [name ] = float (value .item ())
155- elif _is_numpy_array (value ):
156- _extract_numpy_statistics (name , value , metrics )
157- elif _is_single_element_sequence (value ):
158- metrics [name ] = float (value [0 ])
159- else :
160- metrics [name ] = float (value )
161- except (ValueError , TypeError , AttributeError , IndexError ) as exc :
162- _LOGGER .debug ("Could not convert %s to float: %s" , name , exc )
163-
164-
165- def _extract_from_tracking_data (
166- data : dict [str , Any ],
167- metrics : dict [str , float ],
168- prefix : str ,
169- max_depth : int = 2 ,
170- ) -> None :
171- """Recursively extract metrics from tracking_data dict.
172-
173- Args:
174- data: Dictionary to extract from (tracking_data or nested dict)
175- metrics: Output dictionary to populate with metrics
176- prefix: Metric name prefix for nested structures
177- max_depth: Maximum recursion depth to prevent infinite loops
178- """
179- if max_depth <= 0 :
180- return
181-
182- for key , value in data .items ():
183- metric_name = f"{ prefix } { key } " if prefix else key
184-
185- if isinstance (value , dict ):
186- _extract_from_tracking_data (value , metrics , f"{ metric_name } /" , max_depth - 1 )
187- else :
188- _extract_from_value (metric_name , value , metrics )
189-
190-
191- _STANDARD_METRIC_ATTRS = [
192- "episode_reward" ,
193- "episode_reward_mean" ,
194- "episode_length" ,
195- "episode_length_mean" ,
196- "cumulative_rewards" ,
197- "mean_rewards" ,
198- "episode_lengths" ,
199- "success_rate" ,
200- "policy_loss" ,
201- "value_loss" ,
202- "critic_loss" ,
203- "entropy" ,
204- "learning_rate" ,
205- "lr" ,
206- "grad_norm" ,
207- "gradient_norm" ,
208- "kl_divergence" ,
209- "kl" ,
210- "timesteps" ,
211- "timesteps_total" ,
212- "total_timesteps" ,
213- "iterations" ,
214- "iterations_total" ,
215- "fps" ,
216- "time_elapsed" ,
217- "epoch_time" ,
218- "rollout_time" ,
219- "learning_time" ,
220- ]
105+ def log_metrics (
106+ self ,
107+ metrics : dict [str , float ],
108+ step : int | None = None ,
109+ synchronous : bool = True ,
110+ ) -> None : ...
221111
222112
223113def _has_tracking_data (agent : SkrlAgent ) -> bool :
114+ """Check if agent has valid tracking_data dict."""
224115 return hasattr (agent , "tracking_data" ) and isinstance (agent .tracking_data , dict )
225116
226117
227118def _extract_metrics_from_agent (
228119 agent : SkrlAgent ,
229120 metric_filter : set [str ] | None = None ,
230121) -> dict [str , float ]:
231- """Extract metrics from the SKRL agent's internal state.
122+ """Extract metrics from SKRL agent's internal state.
232123
233- Extracts metrics from multiple sources:
234- - agent.tracking_data dict (SKRL's primary tracking mechanism)
235- - Direct agent attributes for common metrics
236- - Nested structures in tracking_data
237- - Statistical aggregations (mean, std, min, max) when available
124+ Extracts from agent.tracking_data dict, direct attributes, and nested
125+ structures. Multi-element values produce mean/std/min/max statistics.
238126
239127 Args:
240- agent: SKRL agent instance with tracking_data dict
241- metric_filter: Optional set of metric names to include
128+ agent: SKRL agent instance with tracking_data dict.
129+ metric_filter: Optional set of metric names to include.
242130
243131 Returns:
244- Dictionary of metric names to float values, filtered by metric_filter if set
132+ Dictionary of metric names to float values.
245133 """
246134 metrics : dict [str , float ] = {}
247135
@@ -262,48 +150,68 @@ def create_mlflow_logging_wrapper(
262150 agent : SkrlAgent ,
263151 mlflow_module : MLflowModule ,
264152 metric_filter : set [str ] | None = None ,
153+ collect_gpu_metrics : bool = True ,
265154) -> Callable [[int , int ], Any ]:
266155 """Create closure that wraps agent._update with MLflow logging.
267156
268- Returns a function suitable for monkey-patching agent._update. The returned
269- closure will call the original agent._update method and then extract and log
270- metrics to MLflow.
157+ Returns a function that calls the original agent._update method then
158+ extracts and logs metrics to MLflow.
271159
272160 Args:
273- agent: SKRL agent instance to extract metrics from
274- mlflow_module: MLflow module for logging metrics
275- metric_filter: Optional set of metric names to include
161+ agent: SKRL agent instance to extract metrics from.
162+ mlflow_module: MLflow module for logging metrics.
163+ metric_filter: Optional set of metric names to include.
164+ collect_gpu_metrics: Enable GPU metrics collection (default: True).
276165
277166 Returns:
278- Closure function with signature (timestep: int, timesteps: int) -> Any
279- that can be assigned to agent._update for monkey-patching
167+ Closure function suitable for monkey-patching agent._update.
168+
169+ Raises:
170+ AttributeError: If agent lacks tracking_data attribute.
280171
281172 Example:
282- >>> wrapper_func = create_mlflow_logging_wrapper(runner.agent, mlflow, None )
283- >>> runner.agent._update = wrapper_func
173+ >>> wrapper = create_mlflow_logging_wrapper(runner.agent, mlflow)
174+ >>> runner.agent._update = wrapper
284175 """
285176 if not _has_tracking_data (agent ):
286- if not hasattr (agent , "tracking_data" ):
287- raise AttributeError (
288- "Agent must have 'tracking_data' attribute for MLflow metric logging. "
289- f"Agent type { type (agent ).__name__ } does not support metric tracking."
290- )
291- raise TypeError (
292- f"Agent 'tracking_data' must be a dict, got { type (agent .tracking_data ).__name__ } . "
293- "Cannot extract metrics from non-dict tracking_data."
177+ raise AttributeError (
178+ "Agent must have 'tracking_data' attribute for MLflow metric logging. "
179+ f"Agent type { type (agent ).__name__ } does not support metric tracking."
294180 )
295181
182+ system_metrics_collector = SystemMetricsCollector (
183+ collect_gpu = collect_gpu_metrics ,
184+ collect_disk = True ,
185+ )
186+ _LOGGER .debug (
187+ "System metrics collector initialized (GPU: %s)" ,
188+ collect_gpu_metrics ,
189+ )
190+
296191 original_update = agent ._update
297192
298193 def mlflow_logging_update (timestep : int , timesteps : int ) -> Any :
299- """Wrapped _update that logs metrics to MLflow after each training update ."""
194+ """Call original _update and log metrics to MLflow."""
300195 result = original_update (timestep , timesteps )
301196
302197 try :
303- metrics = _extract_metrics_from_agent (agent , metric_filter )
304- if metrics and mlflow_module :
305- mlflow_module .log_metrics (metrics , step = timestep )
306- elif not metrics :
198+ training_metrics = _extract_metrics_from_agent (agent , metric_filter )
199+
200+ system_metrics = {}
201+ try :
202+ system_metrics = system_metrics_collector .collect_metrics ()
203+ _LOGGER .debug (
204+ "System metrics collected: %d metrics" ,
205+ len (system_metrics ),
206+ )
207+ except Exception as exc :
208+ _LOGGER .debug ("System metrics collection failed: %s" , exc )
209+
210+ all_metrics = {** training_metrics , ** system_metrics }
211+
212+ if all_metrics and mlflow_module :
213+ mlflow_module .log_metrics (all_metrics , step = timestep , synchronous = False )
214+ elif not all_metrics :
307215 _LOGGER .debug ("No metrics extracted at timestep %d" , timestep )
308216 except Exception as exc :
309217 _LOGGER .warning ("Failed to log metrics at timestep %d: %s" , timestep , exc )
0 commit comments