@@ -104,6 +104,19 @@ def pairwise_slopes(values, cp):
104104 return [abs (float (values [i + 1 ] - values [i ]) / float (cp [i + 1 ] - cp [i ])) for i in range (len (values )- 1 )]
105105
106106
107+ def last_n_percent_runs (values , n = 0.1 ):
108+ assert 0.0 < n <= 1.0
109+ end_runs_idx = len (values ) - int (len (values ) * n )
110+ end_runs_idx = len (values ) - 1 if end_runs_idx >= len (values ) else end_runs_idx
111+ return values [end_runs_idx :], list (range (end_runs_idx , len (values )))
112+
113+
114+ def first_n_percent_runs (values , n = 0.1 ):
115+ assert 0.0 < n <= 1.0
116+ first_run_idx = int (len (values ) * n )
117+ return first_run_idx - 1 if first_run_idx == len (values ) else first_run_idx
118+
119+
107120def detect_warmup (values , cp_threshold = 0.03 , stability_slope_grade = 0.01 ):
108121 """
109122 detect the point of warmup point (iteration / run)
@@ -122,19 +135,22 @@ def detect_warmup(values, cp_threshold=0.03, stability_slope_grade=0.01):
122135 cp = cusum (values , threshold = cp_threshold )
123136 rolling_avg = [avg (values [i :]) for i in cp ]
124137
138+ def warmup (cp_index ):
139+ val_idx = cp [cp_index ] + 1
140+ return val_idx if val_idx < len (values ) else - 1
141+
125142 # find the point where the duration avg is below the cp threshold
126143 for i , d in enumerate (rolling_avg ):
127144 if d <= cp_threshold :
128- return cp [ i ] + 1
145+ return warmup ( i )
129146
130147 # could not find something below the CP threshold (noise in the data), use the stabilisation of slopes
131- end_runs_idx = len (values ) - int (len (values ) * 0.1 )
132- end_runs_idx = len (values ) - 1 if end_runs_idx >= len (values ) else end_runs_idx
133- slopes = pairwise_slopes (rolling_avg + values [end_runs_idx :], cp + list (range (end_runs_idx , len (values ))))
148+ last_n_vals , last_n_idx = last_n_percent_runs (values , 0.1 )
149+ slopes = pairwise_slopes (rolling_avg + last_n_vals , cp + last_n_idx )
134150
135151 for i , d in enumerate (slopes ):
136152 if d <= stability_slope_grade :
137- return cp [ i ] + 1
153+ return warmup ( i )
138154
139155 return - 1
140156 except Exception as e :
@@ -176,7 +192,14 @@ def _as_int(value):
176192
177193
178194class BenchRunner (object ):
179- def __init__ (self , bench_file , bench_args = None , iterations = 1 , warmup = 0 ):
195+ def __init__ (self , bench_file , bench_args = None , iterations = 1 , warmup = - 1 , warmup_runs = 0 ):
196+ assert isinstance (iterations , int ), \
197+ "BenchRunner iterations argument must be an int, got %s instead" % iterations
198+ assert isinstance (warmup , int ), \
199+ "BenchRunner warmup argument must be an int, got %s instead" % warmup
200+ assert isinstance (warmup_runs , int ), \
201+ "BenchRunner warmup_runs argument must be an int, got %s instead" % warmup_runs
202+
180203 if bench_args is None :
181204 bench_args = []
182205 self .bench_module = BenchRunner .get_bench_module (bench_file )
@@ -185,10 +208,8 @@ def __init__(self, bench_file, bench_args=None, iterations=1, warmup=0):
185208 _iterations = _as_int (iterations )
186209 self ._run_once = _iterations <= 1
187210 self .iterations = 1 if self ._run_once else _iterations
188-
189- assert isinstance (self .iterations , int )
190- self .warmup = _as_int (warmup )
191- assert isinstance (self .warmup , int )
211+ self .warmup_runs = warmup_runs if warmup_runs > 0 else 0
212+ self .warmup = warmup if warmup > 0 else - 1
192213
193214 @staticmethod
194215 def get_bench_module (bench_file ):
@@ -226,9 +247,10 @@ def _call_attr(self, attr_name, *args):
226247
227248 def run (self ):
228249 if self ._run_once :
229- print ("### %s, exactly one iteration (no warmup curves)" % ( self .bench_module .__name__ ) )
250+ print ("### %s, exactly one iteration (no warmup curves)" % self .bench_module .__name__ )
230251 else :
231- print ("### %s, %s warmup iterations, %s bench iterations " % (self .bench_module .__name__ , self .warmup , self .iterations ))
252+ print ("### %s, %s warmup iterations, %s bench iterations " % (self .bench_module .__name__ ,
253+ self .warmup_runs , self .iterations ))
232254
233255 # process the args if the processor function is defined
234256 args = self ._call_attr (ATTR_PROCESS_ARGS , * self .bench_args )
@@ -246,9 +268,9 @@ def run(self):
246268 bench_func = self ._get_attr (ATTR_BENCHMARK )
247269 durations = []
248270 if bench_func and hasattr (bench_func , '__call__' ):
249- if self .warmup :
250- print ("### warming up for %s iterations ... " % self .warmup )
251- for _ in range (self .warmup ):
271+ if self .warmup_runs :
272+ print ("### (pre) warming up for %s iterations ... " % self .warmup_runs )
273+ for _ in range (self .warmup_runs ):
252274 bench_func (* args )
253275
254276 for iteration in range (self .iterations ):
@@ -260,28 +282,37 @@ def run(self):
260282 if self ._run_once :
261283 print ("@@@ name=%s, duration=%s" % (self .bench_module .__name__ , duration_str ))
262284 else :
263- print ("### iteration=%s, name=%s, duration=%s" % (iteration , self .bench_module .__name__ , duration_str ))
285+ print ("### iteration=%s, name=%s, duration=%s" % (iteration , self .bench_module .__name__ ,
286+ duration_str ))
264287
265288 print (_HRULE )
266289 print ("### teardown ... " )
267290 self ._call_attr (ATTR_TEARDOWN )
268- warmup_iter = detect_warmup (durations )
291+ warmup_iter = self .warmup if self .warmup > 0 else detect_warmup (durations )
292+ # if we cannot detect a warmup starting point but we performed some pre runs, we take a starting point
293+ # after the 10% of the first runs ...
294+ if warmup_iter < 0 and self .warmup_runs > 0 :
295+ print ("### warmup could not be detected, but %s pre-runs were executed.\n "
296+ "### we assume the benchmark is warmed up and pick an iteration "
297+ "in the first 10%% of the runs" % self .warmup_runs )
298+ warmup_iter = first_n_percent_runs (durations , 0.1 )
269299 print ("### benchmark complete" )
270300 print (_HRULE )
271301 print ("### BEST duration: %.3f s" % min (durations ))
272302 print ("### WORST duration: %.3f s" % max (durations ))
273303 print ("### AVG (all runs) duration: %.3f s" % (sum (durations ) / len (durations )))
274304 if warmup_iter > 0 :
275- print ("### WARMUP detected at iteration: %d" % warmup_iter )
305+ print ("### WARMUP %s at iteration: %d" % ( "specified" if self . warmup > 0 else "detected" , warmup_iter ) )
276306 no_warmup_durations = durations [warmup_iter :]
277307 print ("### AVG (no warmup) duration: %.3f s" % (sum (no_warmup_durations ) / len (no_warmup_durations )))
278308 else :
279- print ("### WARMUP could not be detected" )
309+ print ("### WARMUP iteration not specified or could not be detected" )
280310 print (_HRULE )
281311
282312
283313def run_benchmark (args ):
284- warmup = 0
314+ warmup = - 1
315+ warmup_runs = 0
285316 iterations = 1
286317 bench_file = None
287318 bench_args = []
@@ -302,6 +333,12 @@ def run_benchmark(args):
302333 elif arg .startswith ("--warmup" ):
303334 warmup = _as_int (arg .split ("=" )[1 ])
304335
336+ elif arg == '-r' :
337+ i += 1
338+ warmup_runs = _as_int (args [i ])
339+ elif arg .startswith ("--warmup-runs" ):
340+ warmup_runs = _as_int (arg .split ("=" )[1 ])
341+
305342 elif arg == '-p' :
306343 i += 1
307344 paths = args [i ].split ("," )
@@ -323,7 +360,7 @@ def run_benchmark(args):
323360 else :
324361 print ("### no extra module search paths specified" )
325362
326- BenchRunner (bench_file , bench_args = bench_args , iterations = iterations , warmup = warmup ).run ()
363+ BenchRunner (bench_file , bench_args = bench_args , iterations = iterations , warmup = warmup , warmup_runs = warmup_runs ).run ()
327364
328365
329366if __name__ == '__main__' :
0 commit comments