4848
4949from __future__ import annotations
5050
51- import contextlib
51+ import inspect
5252import logging
53+ import statistics
5354import time
5455from collections .abc import Callable
55- from dataclasses import dataclass , field
56-
57- import numpy as np
58- import torch
56+ from dataclasses import dataclass
5957
6058from .benchmark_core import BaseIsaacLabBenchmark
6159from .measurements import StatisticalMeasurement
@@ -85,6 +83,14 @@ class MethodBenchmarkRunnerConfig:
8583 device : str = "cuda:0"
8684 mode : str | list [str ] = "all"
8785
86+ def __post_init__ (self ) -> None :
87+ """Validate benchmark workload sizes."""
88+ for field_name in ("num_iterations" , "num_instances" , "num_bodies" , "num_joints" ):
89+ if getattr (self , field_name ) <= 0 :
90+ raise ValueError (f"{ field_name } must be greater than zero" )
91+ if self .warmup_steps < 0 :
92+ raise ValueError ("warmup_steps must be non-negative" )
93+
8894
8995@dataclass
9096class MethodBenchmarkDefinition :
@@ -95,14 +101,12 @@ class MethodBenchmarkDefinition:
95101 method_name: Name of the method to benchmark on the target object.
96102 input_generators: Dict mapping mode names to input generator functions.
97103 category: Category for grouping results into phases.
98- dependencies: List of method names that must be called first.
99104 """
100105
101106 name : str
102107 method_name : str
103108 input_generators : dict [str , Callable ]
104109 category : str = "default"
105- dependencies : list [str ] = field (default_factory = list )
106110
107111
108112class MethodBenchmarkRunner (BaseIsaacLabBenchmark ):
@@ -171,17 +175,13 @@ def run_benchmarks(
171175 self ,
172176 benchmarks : list [MethodBenchmarkDefinition ],
173177 target_object : object ,
174- dependencies : dict [str , list [str ]] | None = None ,
175178 ) -> None :
176179 """Run all defined benchmarks on the target object.
177180
178181 Args:
179182 benchmarks: List of benchmark definitions to run.
180183 target_object: Object containing the methods to benchmark.
181- dependencies: Optional dict mapping method names to their dependencies.
182184 """
183- if dependencies is None :
184- dependencies = {}
185185
186186 print (f"\n Benchmarking { len (benchmarks )} methods..." )
187187 print (f"Config: { self ._config .num_iterations } iterations, { self ._config .warmup_steps } warmup steps" )
@@ -201,9 +201,6 @@ def run_benchmarks(
201201 current_modes = self ._modes_to_run if self ._modes_to_run is not None else available_modes
202202 current_modes = [m for m in current_modes if m in available_modes ]
203203
204- # Get dependencies for this method
205- method_deps = benchmark .dependencies or dependencies .get (benchmark .method_name , [])
206-
207204 for mode in current_modes :
208205 # Update manual recorders
209206 self .update_manual_recorders ()
@@ -216,7 +213,6 @@ def run_benchmarks(
216213 method = method ,
217214 method_name = bench_name ,
218215 generator = generator ,
219- dependencies = method_deps ,
220216 )
221217
222218 if result is None :
@@ -243,85 +239,97 @@ def _benchmark_method(
243239 method : Callable | None ,
244240 method_name : str ,
245241 generator : Callable ,
246- dependencies : list [str ],
247242 ) -> dict | None :
248243 """Benchmark a single method.
249244
250245 Args:
251246 method: The method to benchmark (or None if not found).
252247 method_name: Name of the method for reporting.
253248 generator: Function that generates input arguments.
254- dependencies: List of dependency method names to call first.
255249
256250 Returns:
257251 Dict with timing results, or None if method not found.
258252 """
259253 if method is None :
260254 return None
261255
262- # Try to call the method once to check for NotImplementedError
263- try :
256+ inputs : dict = {}
257+
258+ def prepare () -> None :
259+ nonlocal inputs
264260 inputs = generator (self ._config )
265- _ = method (** inputs )
261+
262+ def operation () -> object :
263+ return method (** inputs )
264+
265+ try :
266+ prepare ()
267+ operation ()
266268 except NotImplementedError as e :
267269 return {"skipped" : True , "skip_reason" : f"NotImplementedError: { e } " }
268270 except Exception as e :
269- return { "skipped" : True , "skip_reason" : f"Error: { type ( e ). __name__ } : { e } " }
271+ raise RuntimeError ( f" { method_name } failed during preflight" ) from e
270272
271- # Warmup phase
272- for _ in range (self ._config .warmup_steps ):
273+ return self ._collect_samples (method_name , prepare , operation )
274+
275+ def _collect_samples (
276+ self ,
277+ workload_name : str ,
278+ prepare : Callable [[], None ],
279+ operation : Callable [[], object ],
280+ ) -> dict :
281+ """Collect synchronized latency samples for one workload.
282+
283+ Args:
284+ workload_name: Name included in contextual failures.
285+ prepare: Untimed callable that prepares one operation.
286+ operation: Callable measured after preparation.
287+
288+ Returns:
289+ Timing statistics in microseconds.
290+ """
291+ for iteration in range (self ._config .warmup_steps ):
273292 try :
274- inputs = generator (self ._config )
275- _ = method (** inputs )
276- except Exception :
277- pass
278- # Sync GPU
293+ prepare ()
294+ operation ()
295+ except Exception as e :
296+ raise RuntimeError (f"{ workload_name } failed during warmup iteration { iteration } " ) from e
279297 if self ._config .device .startswith ("cuda" ):
280298 self ._sync_device ()
281299
282- # Timing phase
283- times = []
284- for _ in range (self ._config .num_iterations ):
285- inputs = generator (self ._config )
300+ times : list [float ] = []
301+ for iteration in range (self ._config .num_iterations ):
302+ try :
303+ prepare ()
304+ except Exception as e :
305+ raise RuntimeError (f"{ workload_name } failed during timed preparation iteration { iteration } " ) from e
286306
287- # Sync before timing
288307 if self ._config .device .startswith ("cuda" ):
289308 self ._sync_device ()
290309
291- # Time the method
292- start_time = time .perf_counter ()
310+ start_time = time .perf_counter_ns ()
293311 try :
294- _ = method ( ** inputs )
295- except Exception :
296- continue
312+ operation ( )
313+ except Exception as e :
314+ raise RuntimeError ( f" { workload_name } failed during timed iteration { iteration } " ) from e
297315
298- # Sync after to ensure kernel completion
299316 if self ._config .device .startswith ("cuda" ):
300317 self ._sync_device ()
301318
302- end_time = time .perf_counter ()
303- times .append ((end_time - start_time ) * 1e6 ) # Convert to microseconds
304-
305- if not times :
306- return {"skipped" : True , "skip_reason" : "No successful iterations" }
319+ end_time = time .perf_counter_ns ()
320+ times .append ((end_time - start_time ) / 1e3 )
307321
308322 return {
309- "mean" : float ( np .mean (times ) ),
310- "std" : float ( np . std (times )) ,
323+ "mean" : statistics .mean (times ),
324+ "std" : statistics . stdev (times ) if len ( times ) > 1 else 0.0 ,
311325 "n" : len (times ),
312326 }
313327
314328 def _sync_device (self ) -> None :
315329 """Synchronize GPU device."""
316- try :
317- import warp as wp
318-
319- wp .synchronize ()
320- except ImportError :
321- pass
330+ import warp as wp
322331
323- if torch .cuda .is_available ():
324- torch .cuda .synchronize ()
332+ wp .synchronize_device (self ._config .device )
325333
326334 def run_property_benchmarks (
327335 self ,
@@ -408,66 +416,23 @@ def _benchmark_property(
408416 Dict with timing results, or None if property not found.
409417 """
410418 # Check if property exists
411- if not hasattr (target_data , prop_name ) :
419+ if inspect . getattr_static (target_data , prop_name , None ) is None :
412420 return None
413421
414- # Try to access the property once to check for errors
422+ def prepare () -> None :
423+ gen_mock_data (self ._config )
424+ for dependency in dependencies :
425+ getattr (target_data , dependency )
426+
427+ def operation () -> object :
428+ return getattr (target_data , prop_name )
429+
415430 try :
416431 gen_mock_data (self ._config )
417- _ = getattr ( target_data , prop_name )
432+ operation ( )
418433 except NotImplementedError as e :
419434 return {"skipped" : True , "skip_reason" : f"NotImplementedError: { e } " }
420435 except Exception as e :
421- return {"skipped" : True , "skip_reason" : f"Error: { type (e ).__name__ } : { e } " }
422-
423- # Warmup phase
424- for _ in range (self ._config .warmup_steps ):
425- try :
426- gen_mock_data (self ._config )
427- # Access dependencies first
428- for dep in dependencies :
429- with contextlib .suppress (Exception ):
430- _ = getattr (target_data , dep )
431- _ = getattr (target_data , prop_name )
432- except Exception :
433- pass
434- # Sync GPU
435- if self ._config .device .startswith ("cuda" ):
436- self ._sync_device ()
437-
438- # Timing phase
439- times = []
440- for _ in range (self ._config .num_iterations ):
441- gen_mock_data (self ._config )
436+ raise RuntimeError (f"{ prop_name } failed during preflight" ) from e
442437
443- # Access dependencies first (not timed)
444- for dep in dependencies :
445- with contextlib .suppress (Exception ):
446- _ = getattr (target_data , dep )
447-
448- # Sync before timing
449- if self ._config .device .startswith ("cuda" ):
450- self ._sync_device ()
451-
452- # Time the property access
453- start_time = time .perf_counter ()
454- try :
455- _ = getattr (target_data , prop_name )
456- except Exception :
457- continue
458-
459- # Sync after
460- if self ._config .device .startswith ("cuda" ):
461- self ._sync_device ()
462-
463- end_time = time .perf_counter ()
464- times .append ((end_time - start_time ) * 1e6 ) # microseconds
465-
466- if not times :
467- return {"skipped" : True , "skip_reason" : "No successful iterations" }
468-
469- return {
470- "mean" : float (np .mean (times )),
471- "std" : float (np .std (times )),
472- "n" : len (times ),
473- }
438+ return self ._collect_samples (prop_name , prepare , operation )
0 commit comments